mirror of
https://github.com/Hestia-Homes/assessment-model.git
synced 2026-06-08 11:37:25 +00:00
Merge pull request #26 from Hestia-Homes/settings-api
adding the basline for the setting api
This commit is contained in:
commit
5974fad886
4 changed files with 585 additions and 159 deletions
80
src/app/api/portfolio/[portfolioId]/permissions/route.ts
Normal file
80
src/app/api/portfolio/[portfolioId]/permissions/route.ts
Normal file
|
|
@ -0,0 +1,80 @@
|
|||
import { db } from "@/app/db/db";
|
||||
import { NextRequest, NextResponse } from "next/server";
|
||||
import { portfolioUsers } from "@/app/db/schema/portfolio";
|
||||
import { and, eq } from "drizzle-orm";
|
||||
import { z } from "zod";
|
||||
|
||||
const PermissionsBodySchema = z.object({
|
||||
userId: z.string(),
|
||||
action: z.enum(["delete", "update"]),
|
||||
});
|
||||
|
||||
export async function POST(
|
||||
request: NextRequest,
|
||||
{ params }: { params: { portfolioId: string } }
|
||||
) {
|
||||
// This endpoint lives at portfolio/{portfolioId}/permissions and will return the permissions level for a given portfolio
|
||||
// Call this endpoint with a) userId, b) portfolioId, c) an action and this api will tell you if that person can do that thing
|
||||
|
||||
const body = await request.json();
|
||||
let validatedBody;
|
||||
|
||||
try {
|
||||
validatedBody = PermissionsBodySchema.parse(body);
|
||||
} catch (error) {
|
||||
console.error("Invalid input: ", error);
|
||||
return new NextResponse(JSON.stringify({ msg: "Invalid input" }), {
|
||||
status: 400,
|
||||
});
|
||||
}
|
||||
|
||||
const action = validatedBody.action;
|
||||
const userId = validatedBody.userId;
|
||||
const portfolioId = params.portfolioId;
|
||||
|
||||
const existingPortfolioPermissions = await db.query.portfolioUsers.findFirst({
|
||||
where: and(
|
||||
eq(portfolioUsers.portfolioId, BigInt(portfolioId)),
|
||||
eq(portfolioUsers.userId, BigInt(userId))
|
||||
),
|
||||
});
|
||||
|
||||
if (!existingPortfolioPermissions) {
|
||||
return new NextResponse(
|
||||
JSON.stringify({ error: "Portfolio not found or unauthorized" }),
|
||||
{ status: 404 }
|
||||
);
|
||||
}
|
||||
|
||||
const role = existingPortfolioPermissions.role;
|
||||
|
||||
let permitted;
|
||||
|
||||
// If the action is a delete, we only allow an admin or creator to delete
|
||||
if (action === "delete") {
|
||||
if (role === "admin" || role === "creator") {
|
||||
permitted = true;
|
||||
} else {
|
||||
permitted = false;
|
||||
}
|
||||
}
|
||||
|
||||
// If the action is an update, we allow an individual admin, creator or write role to update
|
||||
if (action === "update") {
|
||||
if (role === "admin" || role === "creator" || role === "write") {
|
||||
permitted = true;
|
||||
} else {
|
||||
permitted = false;
|
||||
}
|
||||
}
|
||||
|
||||
// Returning a successful response
|
||||
return new NextResponse(
|
||||
JSON.stringify({
|
||||
permitted: permitted,
|
||||
}),
|
||||
{
|
||||
status: 200,
|
||||
}
|
||||
);
|
||||
}
|
||||
167
src/app/api/portfolio/[portfolioId]/route.ts
Normal file
167
src/app/api/portfolio/[portfolioId]/route.ts
Normal file
|
|
@ -0,0 +1,167 @@
|
|||
import { db } from "@/app/db/db";
|
||||
import { NextRequest, NextResponse } from "next/server";
|
||||
import { portfolio, portfolioUsers } from "@/app/db/schema/portfolio";
|
||||
import {
|
||||
recommendation,
|
||||
recommendationMaterials,
|
||||
planRecommendations,
|
||||
plan,
|
||||
scenario,
|
||||
} from "@/app/db/schema/recommendations";
|
||||
import {
|
||||
propertyTargets,
|
||||
propertyDetailsEpc,
|
||||
property,
|
||||
} from "@/app/db/schema/property";
|
||||
import { eq, inArray, Name } from "drizzle-orm";
|
||||
import { z } from "zod";
|
||||
|
||||
const UpdateBodySchema = z.object({
|
||||
name: z.optional(z.string()),
|
||||
budget: z.optional(z.number()),
|
||||
goal: z.optional(z.string()),
|
||||
status: z.optional(z.string()),
|
||||
});
|
||||
|
||||
export async function PUT(
|
||||
request: NextRequest,
|
||||
{ params }: { params: { portfolioId: string } }
|
||||
) {
|
||||
const body = await request.json();
|
||||
let validatedBody;
|
||||
|
||||
try {
|
||||
validatedBody = UpdateBodySchema.parse(body);
|
||||
} catch (error) {
|
||||
console.error("Invalid input: ", error);
|
||||
return new NextResponse(JSON.stringify({ msg: "Invalid input" }), {
|
||||
status: 400,
|
||||
});
|
||||
}
|
||||
|
||||
const portfolioId = params.portfolioId;
|
||||
const name = validatedBody.name;
|
||||
const budget = validatedBody.budget;
|
||||
const goal = validatedBody.goal;
|
||||
const status = validatedBody.status;
|
||||
|
||||
|
||||
await db
|
||||
.update(portfolio)
|
||||
.set(body)
|
||||
.where(eq(portfolio.id, BigInt(portfolioId)));
|
||||
|
||||
// Returning a successful response
|
||||
return new NextResponse(JSON.stringify({}), {
|
||||
status: 200,
|
||||
});
|
||||
}
|
||||
|
||||
export async function DELETE(
|
||||
request: NextRequest,
|
||||
{ params }: { params: { portfolioId: string } }
|
||||
) {
|
||||
try {
|
||||
const portfolioId = params.portfolioId;
|
||||
|
||||
// 1) Fetch the portfolio ids
|
||||
// 2) Fetch the recommendation ids
|
||||
// 3) Delete all entries from RecommendationMaterials for these recommendationsP
|
||||
// 4) Delete all entries from PlanRecommendations that reference plans in the portfolio
|
||||
// 5) Delete all Plans associated with the portfolio
|
||||
// 6) Delete all Scenarios associated with the portfolio
|
||||
// 7) Delete all Recommendations associated with the properties
|
||||
// 8) Delete PropertyTargetsModel, PropertyDetailsEpcModel, and PropertyModel
|
||||
// 9) Delete portfolioUsers
|
||||
// 10) Then, we finally delete the portfolio!!!
|
||||
|
||||
// Step 1) Fetch the property ids for the portfolio
|
||||
const propertyIdsResponse = await db.query.property.findMany({
|
||||
columns: { id: true },
|
||||
where: eq(property.portfolioId, BigInt(portfolioId)),
|
||||
});
|
||||
const propertyIds = propertyIdsResponse.map((property) => property.id);
|
||||
|
||||
// Step 2) Fetch the recommendation ids - filter the recommendation table, where propertyId is in the propertyIds
|
||||
// if there are no prpoperty Ids, we make recommendationIds an empty array
|
||||
let recommendationIds: bigint[] = [];
|
||||
if (propertyIds.length) {
|
||||
const recommendations = await db.query.recommendation.findMany({
|
||||
columns: { id: true },
|
||||
where: inArray(recommendation.propertyId, propertyIds),
|
||||
});
|
||||
recommendationIds = recommendations.map(
|
||||
(recommendation) => recommendation.id
|
||||
);
|
||||
}
|
||||
|
||||
// Step 3) Delete all entries from RecommendationMaterials for these recommendations
|
||||
if (recommendationIds.length) {
|
||||
await db
|
||||
.delete(recommendationMaterials)
|
||||
.where(
|
||||
inArray(recommendationMaterials.recommendationId, recommendationIds)
|
||||
);
|
||||
}
|
||||
|
||||
// Step 4) Delete all entries from PlanRecommendations that reference plans in the portfolio
|
||||
// Get the plan ids
|
||||
const plans = await db.query.plan.findMany({
|
||||
columns: { id: true },
|
||||
where: eq(plan.portfolioId, BigInt(portfolioId)),
|
||||
});
|
||||
const planIds = plans.map((plan) => plan.id);
|
||||
|
||||
if (planIds.length) {
|
||||
await db
|
||||
.delete(planRecommendations)
|
||||
.where(inArray(planRecommendations.planId, planIds));
|
||||
}
|
||||
|
||||
// Step 5) Delete all Plans associated with the portfolio
|
||||
await db.delete(plan).where(eq(plan.portfolioId, BigInt(portfolioId)));
|
||||
|
||||
// Step 6) Delete all Scenarios associated with the portfolio
|
||||
await db
|
||||
.delete(scenario)
|
||||
.where(eq(scenario.portfolioId, BigInt(portfolioId)));
|
||||
|
||||
// Step 7) Delete all Recommendations associated with the properties
|
||||
if (propertyIds.length) {
|
||||
await db
|
||||
.delete(recommendation)
|
||||
.where(inArray(recommendation.propertyId, propertyIds));
|
||||
}
|
||||
|
||||
// Step 8) Delete PropertyTargets, PropertyDetailsEpc, and Property
|
||||
await db
|
||||
.delete(propertyTargets)
|
||||
.where(eq(propertyTargets.portfolioId, BigInt(portfolioId)));
|
||||
|
||||
await db
|
||||
.delete(propertyDetailsEpc)
|
||||
.where(eq(propertyDetailsEpc.portfolioId, BigInt(portfolioId)));
|
||||
|
||||
await db
|
||||
.delete(property)
|
||||
.where(eq(property.portfolioId, BigInt(portfolioId)));
|
||||
|
||||
await db
|
||||
.delete(portfolioUsers)
|
||||
.where(eq(portfolioUsers.portfolioId, BigInt(portfolioId)));
|
||||
|
||||
await db.delete(portfolio).where(eq(portfolio.id, BigInt(portfolioId)));
|
||||
|
||||
// Return success response
|
||||
return new NextResponse(
|
||||
JSON.stringify({ message: "Portfolio successfully deleted" }),
|
||||
{ status: 200 }
|
||||
);
|
||||
} catch (error) {
|
||||
console.error("Error deleting portfolio:", error);
|
||||
return new NextResponse(
|
||||
JSON.stringify({ error: "Your API has Failed to delete the portfolio" }),
|
||||
{ status: 500 }
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
@ -1,6 +1,7 @@
|
|||
"use client";
|
||||
|
||||
import { useState } from "react";
|
||||
import { useMutation } from "@tanstack/react-query";
|
||||
import { PortfolioSettingsType } from "../../utils";
|
||||
import { Button } from "@/app/shadcn_components/ui/button";
|
||||
import { Input } from "@/app/shadcn_components/ui/input";
|
||||
|
|
@ -21,8 +22,18 @@ import {
|
|||
DialogTitle,
|
||||
DialogFooter,
|
||||
} from "@/app/shadcn_components/ui/dialog";
|
||||
import {
|
||||
Table,
|
||||
TableBody,
|
||||
TableCell,
|
||||
TableHead,
|
||||
TableHeader,
|
||||
TableRow,
|
||||
} from "@/app/shadcn_components/ui/table";
|
||||
import { PortfolioStatus as PortfolioStatusOptions } from "@/app/db/schema/portfolio";
|
||||
import { PortfolioGoal as PortfolioGoalOptions } from "@/app/db/schema/portfolio";
|
||||
import { useSession } from "next-auth/react";
|
||||
import PortfolioPlanTable from "@/app/components/portfolio/plan/PlanTable";
|
||||
|
||||
// dropdown selection component for both goal and status
|
||||
|
||||
|
|
@ -30,10 +41,12 @@ export function SettingsDropdown({
|
|||
startingValue,
|
||||
options,
|
||||
setOption,
|
||||
className,
|
||||
}: {
|
||||
startingValue: string;
|
||||
options: string[];
|
||||
setOption: (option: string) => void;
|
||||
className?: string;
|
||||
}) {
|
||||
function handleValueChange(newValue: string) {
|
||||
setOption(newValue);
|
||||
|
|
@ -41,7 +54,7 @@ export function SettingsDropdown({
|
|||
|
||||
return (
|
||||
<Select onValueChange={(newValue) => handleValueChange(newValue)}>
|
||||
<SelectTrigger className="w-[180px]">
|
||||
<SelectTrigger className="w-full">
|
||||
<SelectValue placeholder={startingValue} />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
|
|
@ -57,6 +70,149 @@ export function SettingsDropdown({
|
|||
);
|
||||
}
|
||||
|
||||
type updateSettingsArgs = {
|
||||
userId: bigint;
|
||||
portfolioId: string;
|
||||
name: string | null;
|
||||
budget: number | string | undefined | null;
|
||||
goal: (typeof PortfolioGoalOptions)[number] | null;
|
||||
status: (typeof PortfolioStatusOptions)[number] | null;
|
||||
};
|
||||
|
||||
type bodyType = {
|
||||
name?: string;
|
||||
budget?: number | string;
|
||||
goal?: string;
|
||||
status?: string;
|
||||
};
|
||||
|
||||
const updateSettings = async ({
|
||||
userId,
|
||||
portfolioId,
|
||||
name,
|
||||
budget,
|
||||
goal,
|
||||
status,
|
||||
}: updateSettingsArgs) => {
|
||||
const permissionsReponse = await fetch(
|
||||
`/api/portfolio/${portfolioId}/permissions`,
|
||||
{
|
||||
method: "POST",
|
||||
headers: {
|
||||
"Content-Type": "application/json",
|
||||
},
|
||||
body: JSON.stringify({
|
||||
userId: userId.toString(),
|
||||
action: "update",
|
||||
}),
|
||||
}
|
||||
);
|
||||
|
||||
const permissionsData = await permissionsReponse.json();
|
||||
const permitted = permissionsData.permitted;
|
||||
console.log("USER IS PERMITTED TO DO THIS!!!!")
|
||||
// If the user is not permitted to delete the portfolio, we'll throw an error
|
||||
if (!permitted) {
|
||||
throw new Error("User is not permitted to update this portfolio");
|
||||
}
|
||||
// We convert the the bigint to a string since big ints are not serialisable and we don't want to loose precision
|
||||
|
||||
// We will create a js object with the starting values
|
||||
// We will then update the values that are not null
|
||||
|
||||
const body: bodyType = {}
|
||||
|
||||
if (name) {
|
||||
body.name = name;
|
||||
}
|
||||
|
||||
if (budget) {
|
||||
body.budget = budget;
|
||||
}
|
||||
|
||||
if (goal) {
|
||||
body.goal = goal;
|
||||
}
|
||||
|
||||
if (status) {
|
||||
body.status = status;
|
||||
}
|
||||
|
||||
const requestBody = JSON.stringify(body);
|
||||
|
||||
const response = await fetch(`/api/portfolio/${portfolioId}`, {
|
||||
method: "PUT",
|
||||
headers: {
|
||||
"Content-Type": "application/json",
|
||||
},
|
||||
body: requestBody,
|
||||
|
||||
|
||||
});
|
||||
|
||||
if (!response.ok) {
|
||||
throw new Error("Network response was not ok");
|
||||
}
|
||||
|
||||
return response.json();
|
||||
};
|
||||
|
||||
async function deletePortfolio({
|
||||
userId,
|
||||
portfolioId,
|
||||
}: {
|
||||
userId: bigint;
|
||||
portfolioId: string;
|
||||
}) {
|
||||
try {
|
||||
console.log("Attempting to DELETE portfolio by calling API:", {
|
||||
userId,
|
||||
portfolioId,
|
||||
});
|
||||
|
||||
// We'll check if the user is authorized to delete this portfolio
|
||||
const permissionsReponse = await fetch(
|
||||
`/api/portfolio/${portfolioId}/permissions`,
|
||||
{
|
||||
method: "POST",
|
||||
headers: {
|
||||
"Content-Type": "application/json",
|
||||
},
|
||||
body: JSON.stringify({
|
||||
userId: userId.toString(),
|
||||
action: "delete",
|
||||
}),
|
||||
}
|
||||
);
|
||||
|
||||
const permissionsData = await permissionsReponse.json();
|
||||
const permitted = permissionsData.permitted;
|
||||
|
||||
// If the user is not permitted to delete the portfolio, we'll throw an error
|
||||
if (!permitted) {
|
||||
throw new Error("User is not permitted to delete this portfolio");
|
||||
}
|
||||
|
||||
const response = await fetch(`/api/portfolio/${portfolioId}`, {
|
||||
method: "DELETE",
|
||||
headers: {
|
||||
"Content-Type": "application/json",
|
||||
},
|
||||
});
|
||||
|
||||
if (!response.ok) {
|
||||
throw new Error(
|
||||
"deletePortfolio has been called into action but utterly failed to do the API handoff"
|
||||
);
|
||||
}
|
||||
|
||||
return await response.json();
|
||||
} catch (error) {
|
||||
console.error("Error after failing to the try to get a response:", error);
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
export default function PortfolioSettings({
|
||||
portfolioId,
|
||||
portfolioSettingsData,
|
||||
|
|
@ -64,12 +220,33 @@ export default function PortfolioSettings({
|
|||
portfolioId: string;
|
||||
portfolioSettingsData: PortfolioSettingsType;
|
||||
}) {
|
||||
// Running in the client
|
||||
// This is a client component so we can access the session directly
|
||||
const session = useSession();
|
||||
const router = useRouter();
|
||||
|
||||
// Set up state for portfolioName, portfolioBudget, portfolioGoal and portfolioStatus
|
||||
const { mutate, isLoading } = useMutation(updateSettings, {
|
||||
onSuccess: () => {
|
||||
router.refresh();
|
||||
},
|
||||
onError: (error) => {
|
||||
// handle error
|
||||
console.log(error);
|
||||
},
|
||||
});
|
||||
|
||||
const { mutate: mutateDelete } = useMutation(deletePortfolio, {
|
||||
onSuccess: () => {
|
||||
setIsDeleteModalOpen(false);
|
||||
router.push("/home");
|
||||
},
|
||||
onError: (error) => {
|
||||
console.error(
|
||||
"Because the API hand off failed, we're right back here at the mutation station",
|
||||
error
|
||||
);
|
||||
},
|
||||
});
|
||||
|
||||
// Syntax const [variable, function whos only job is to update the value of variable] = useState(initial value)
|
||||
const [portfolioName, setPortfolioName] = useState(
|
||||
portfolioSettingsData.name
|
||||
);
|
||||
|
|
@ -92,22 +269,27 @@ export default function PortfolioSettings({
|
|||
|
||||
const [deleteConfirmationByName, setDeleteConfirmationByName] = useState("");
|
||||
|
||||
if (!session.data) {
|
||||
// The user is not logged in, redirect them to sign in
|
||||
router.push("/");
|
||||
return null;
|
||||
}
|
||||
|
||||
const userId = session.data.user.dbId;
|
||||
|
||||
function handleOpenDeleteModal() {
|
||||
setDeleteConfirmationByName("");
|
||||
setIsDeleteModalOpen(true);
|
||||
}
|
||||
|
||||
function handleDeleteConfirmation() {
|
||||
console.log("we be deletin stuff");
|
||||
// if (deleteConfirmationByName === portfolioName) {
|
||||
// //apiDeletePortfolio(portfolioId)
|
||||
// router.refresh();
|
||||
// setIsDeleteModalOpen(false);
|
||||
// } else {
|
||||
// // Error if the names don't match
|
||||
// console.log("Portfolio name does not match");
|
||||
// }
|
||||
router.push("/home");
|
||||
if (deleteConfirmationByName === portfolioSettingsData.name) {
|
||||
mutateDelete({
|
||||
userId,
|
||||
portfolioId,
|
||||
});
|
||||
console.log("succesfully called the mututate function");
|
||||
}
|
||||
}
|
||||
|
||||
// RENAMING FUNCTIONS
|
||||
|
|
@ -120,10 +302,15 @@ export default function PortfolioSettings({
|
|||
|
||||
// The onClick function called to update the NAME in the DB
|
||||
|
||||
function handleRenameDb() {
|
||||
// apiRanameFunction(portfolioSettingsData.name)
|
||||
// Update portfolioName
|
||||
router.refresh();
|
||||
function handleRename() {
|
||||
mutate({
|
||||
userId,
|
||||
portfolioId,
|
||||
name: portfolioName,
|
||||
budget: null,
|
||||
goal: null,
|
||||
status: null,
|
||||
});
|
||||
}
|
||||
|
||||
// BUDGET CHANGING FUNCTIONS
|
||||
|
|
@ -136,148 +323,156 @@ export default function PortfolioSettings({
|
|||
|
||||
// The onClick function called to update the BUDGET in the DB
|
||||
|
||||
function handleBudgetUpdateDb() {
|
||||
// apiBudgetChangeFunction(portfolioSettingsData.budget)
|
||||
// Update portfolioBudget
|
||||
router.refresh();
|
||||
function handleBudgetUpdate() {
|
||||
mutate({
|
||||
userId,
|
||||
portfolioId,
|
||||
name: null,
|
||||
budget: portfolioBudget,
|
||||
goal: null,
|
||||
status: null,
|
||||
});
|
||||
}
|
||||
|
||||
// CHANGING GOAL AND STATUS FUNCTIONALITY
|
||||
|
||||
// The onClick function called to update the GOAL in the DB
|
||||
|
||||
function handleGoalUpdateDb() {
|
||||
// apiGoalChangeFunction(portfolioSettingsData.goal)
|
||||
// Update portfolioGoal
|
||||
router.refresh();
|
||||
function handleGoalUpdate() {
|
||||
mutate({
|
||||
userId,
|
||||
portfolioId,
|
||||
name: null,
|
||||
budget: null,
|
||||
goal: portfolioGoal,
|
||||
status: null,
|
||||
});
|
||||
}
|
||||
|
||||
// The onClick function called to update the BUDGET in the DB
|
||||
|
||||
function handleStatusUpdateDb() {
|
||||
// apiStatusChangeFunction(portfolioSettingsData.status)
|
||||
// Update portfolioStatus
|
||||
router.refresh();
|
||||
function handleStatusUpdate() {
|
||||
mutate({
|
||||
userId,
|
||||
portfolioId,
|
||||
name: null,
|
||||
budget: null,
|
||||
goal: null,
|
||||
status: portfolioStatus,
|
||||
});
|
||||
}
|
||||
|
||||
// HTML to render the page
|
||||
|
||||
// TODO: 1) Set up the useMutate hook
|
||||
// 2) Set up the api functions
|
||||
// 3) add the call to mutate() so that when we submit the form, the data is updated in the DB
|
||||
// 4) Create the API
|
||||
|
||||
return (
|
||||
<>
|
||||
<div className="p-4 mt-5 flex justify-center max-w-8xl w-8xl pt-5 bg-gray-50 rounded-lg text-brandblue">
|
||||
<div className="grid grid-cols-[max-content_1fr_min-content] gap-x-[5px] gap-y-4">
|
||||
{/* Row 1: Name */}
|
||||
<div className="flex items-center">
|
||||
<span>Name:</span>
|
||||
</div>
|
||||
<div className="flex items-center">
|
||||
<Input value={portfolioName} onChange={handlePortfolioNameChange} />
|
||||
</div>
|
||||
<div className="flex items-center">
|
||||
<Button className="w-full" onClick={handleRenameDb}>
|
||||
Rename
|
||||
</Button>
|
||||
</div>
|
||||
|
||||
{/* Row 2: Budget */}
|
||||
<div className="flex items-center">
|
||||
<span>Budget:</span>
|
||||
</div>
|
||||
<div className="flex items-center max-w-8xl">
|
||||
<Input
|
||||
type="number"
|
||||
value={portfolioBudget ?? undefined}
|
||||
onChange={handlePortfolioBudgetUpdate}
|
||||
onKeyDown={(e) => handleNumericKeyDown(e)}
|
||||
/>
|
||||
</div>
|
||||
<div className="flex items-center">
|
||||
<Button className="w-full" onClick={handleBudgetUpdateDb}>
|
||||
Update
|
||||
</Button>
|
||||
</div>
|
||||
|
||||
{/* Row 3: Goal */}
|
||||
<div className="flex items-center">
|
||||
<span>Goal:</span>
|
||||
</div>
|
||||
|
||||
<div className="flex items-center">
|
||||
<SettingsDropdown
|
||||
startingValue={portfolioGoal}
|
||||
options={PortfolioGoalOptions}
|
||||
setOption={setPortfolioGoal}
|
||||
/>
|
||||
</div>
|
||||
<Button className="w-full" onClick={handleGoalUpdateDb}>
|
||||
Update
|
||||
</Button>
|
||||
|
||||
{/* Row 4: Status */}
|
||||
<div className="flex items-center">
|
||||
<span>Status:</span>
|
||||
</div>
|
||||
<div className="flex items-center">
|
||||
<SettingsDropdown
|
||||
startingValue={portfolioStatus}
|
||||
options={PortfolioStatusOptions}
|
||||
setOption={setPortfolioStatus}
|
||||
/>
|
||||
</div>
|
||||
<Button className="w-full" onClick={handleStatusUpdateDb}>
|
||||
Update
|
||||
</Button>
|
||||
|
||||
<div className="col-span-3"> Portfolio Name: {portfolioName}</div>
|
||||
<div className="col-span-3"> Portfolio Budget: {portfolioBudget}</div>
|
||||
<div className="col-span-3"> Goal value: {portfolioGoal}</div>
|
||||
<div> Status value: {portfolioStatus}</div>
|
||||
|
||||
{/* Row 5: Delete */}
|
||||
<div className="col-span-2"></div>
|
||||
<div className="flex items-center">
|
||||
|
||||
<div className="w-auto mt-4 p-4 bg-gray-50 rounded-lg text-brandblue">
|
||||
<div className="rounded-md border border-gray-700">
|
||||
<Table>
|
||||
<TableBody>
|
||||
<TableRow>
|
||||
<TableHead className="text-brandblue">
|
||||
Rename the Portfolio:<p className="text-xs text-gray-500">Permanently change the name of your portfolio</p>
|
||||
</TableHead>
|
||||
<TableCell>
|
||||
<Input value={portfolioName} onChange={handlePortfolioNameChange} />
|
||||
</TableCell>
|
||||
<TableCell>
|
||||
<Button className="w-28" onClick={handleRename}>Rename</Button>
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
<TableRow>
|
||||
<TableHead className="text-brandblue">
|
||||
Change the Portfolio Budget:<p className="text-xs text-gray-500">The total budget across ALL properties. Works aim to stay within this budget</p>
|
||||
</TableHead>
|
||||
<TableCell>
|
||||
<Input type="number" value={portfolioBudget ?? undefined} onChange={handlePortfolioBudgetUpdate} onKeyDown={(e) => handleNumericKeyDown(e)}/>
|
||||
</TableCell>
|
||||
<TableCell>
|
||||
<Button className="w-28" onClick={handleBudgetUpdate}>Update</Button>
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
<TableRow>
|
||||
<TableHead className="text-brandblue">
|
||||
Change the Portfolio Goal:<p className="text-xs text-gray-500">Adjust the overall aim of the works conducted on this portfolio</p>
|
||||
</TableHead>
|
||||
<TableCell>
|
||||
<SettingsDropdown className="w-full" startingValue={portfolioGoal} options={PortfolioGoalOptions} setOption={setPortfolioGoal}/>
|
||||
</TableCell>
|
||||
<TableCell>
|
||||
<Button className="w-28" onClick={handleGoalUpdate}>Update</Button>
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
<TableRow>
|
||||
<TableHead className="text-brandblue">
|
||||
Change the Status of the Portfolio:<p className="text-xs text-gray-500">Adjust where the portfolio stands in the works pipeline</p>
|
||||
</TableHead>
|
||||
<TableCell>
|
||||
<SettingsDropdown className="w-full" startingValue={portfolioStatus} options={PortfolioStatusOptions} setOption={setPortfolioStatus}/>
|
||||
</TableCell>
|
||||
<TableCell>
|
||||
<Button className="w-28" onClick={handleStatusUpdate}>Update</Button>
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
</TableBody>
|
||||
</Table>
|
||||
</div>
|
||||
<div className="rounded-md border border-red-500 mt-2">
|
||||
<Table>
|
||||
<TableHead className="text-lg text-brandblue">Danger Zone:</TableHead>
|
||||
<TableBody>
|
||||
<TableRow>
|
||||
<TableHead className="text-brandblue">
|
||||
Delete the Portfolio:<p className="text-xs text-gray-500">Permanently delete the portfolio and all property data assigned to this portfolio</p>
|
||||
</TableHead>
|
||||
<TableCell className="flex justify-end">
|
||||
<Button className="bg-red-700 w-42" onClick={handleOpenDeleteModal}>Delete Portfolio</Button>
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
</TableBody>
|
||||
</Table>
|
||||
<Dialog open={isDeleteModalOpen} onOpenChange={setIsDeleteModalOpen}>
|
||||
<DialogContent>
|
||||
<DialogTitle>Are you sure?</DialogTitle>
|
||||
<p>
|
||||
To confirm, please type the name of the portfolio (
|
||||
<strong>{portfolioSettingsData.name}</strong>)
|
||||
</p>
|
||||
<input
|
||||
type="text"
|
||||
value={deleteConfirmationByName}
|
||||
onChange={(e) => setDeleteConfirmationByName(e.target.value)}
|
||||
placeholder="Type portfolio name"
|
||||
/>
|
||||
<DialogFooter>
|
||||
<Button
|
||||
className="max-width: 100% bg-red-700"
|
||||
onClick={handleOpenDeleteModal}
|
||||
className="bg-green-600"
|
||||
onClick={() => setIsDeleteModalOpen(false)}
|
||||
>
|
||||
Delete Portfolio
|
||||
Cancel
|
||||
</Button>
|
||||
</div>
|
||||
{/* Delete portfolio modal */}
|
||||
<Dialog open={isDeleteModalOpen} onOpenChange={setIsDeleteModalOpen}>
|
||||
<DialogContent>
|
||||
<DialogTitle>Are you sure?</DialogTitle>
|
||||
<p>
|
||||
To confirm, please type the name of the portfolio (
|
||||
<strong>{portfolioSettingsData.name}</strong>)
|
||||
</p>
|
||||
<input
|
||||
type="text"
|
||||
value={deleteConfirmationByName}
|
||||
onChange={(e) => setDeleteConfirmationByName(e.target.value)}
|
||||
placeholder="Type portfolio name"
|
||||
/>
|
||||
<DialogFooter>
|
||||
<Button
|
||||
className="bg-green-600"
|
||||
onClick={() => setIsDeleteModalOpen(false)}
|
||||
>
|
||||
Cancel
|
||||
</Button>
|
||||
<Button
|
||||
className="bg-red-700"
|
||||
onClick={handleDeleteConfirmation}
|
||||
disabled={
|
||||
deleteConfirmationByName !== portfolioSettingsData.name
|
||||
}
|
||||
>
|
||||
Delete
|
||||
</Button>
|
||||
</DialogFooter>
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
</div>
|
||||
</div>
|
||||
</>
|
||||
<Button
|
||||
className="bg-red-700"
|
||||
onClick={handleDeleteConfirmation}
|
||||
disabled={deleteConfirmationByName !== portfolioSettingsData.name}
|
||||
>
|
||||
Delete
|
||||
</Button>
|
||||
</DialogFooter>
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
);
|
||||
|
||||
|
||||
}
|
||||
|
||||
|
||||
|
||||
|
|
@ -7,24 +7,8 @@ export default async function PortfolioSettingsPage({
|
|||
params: { slug: string };
|
||||
}) {
|
||||
const portfolioId = params.slug;
|
||||
// fetch data securely on the server
|
||||
// Stef's page!!!!
|
||||
// 1) Rename
|
||||
// 2) Update budget, status, goal
|
||||
// 3) Delete - much harder
|
||||
|
||||
// fetch data in the server - name, budget, goal,
|
||||
// pass it to a client component to render and take user input
|
||||
|
||||
const portfolioSettingsData = await getPortfolioSettings(portfolioId);
|
||||
|
||||
// Get goal options and status options by importing something like this:
|
||||
// import {
|
||||
// PortfolioStatus,
|
||||
// PortfolioGoal,
|
||||
// } from "./../../db/schema/portfolio";
|
||||
// and then pass them to the portfoliosettings component
|
||||
|
||||
return (
|
||||
<>
|
||||
<div className="flex justify-center max-w-8xl w-8xl">
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue