rebuilding the settings page and including logs

This commit is contained in:
Khalim Conn-Kowlessar 2026-04-04 20:44:44 +00:00
parent b76861b9b8
commit a849731848
6 changed files with 6 additions and 1311 deletions

View file

@ -1,146 +0,0 @@
"use client";
import { useState, useEffect } from "react";
import TaskList from "./TaskList";
import SubtaskDetails from "./SubtaskDetails";
export interface Task {
id: string;
taskSource: string;
jobStarted: string | null;
jobCompleted: string | null;
status: string;
service: string | null;
updatedAt: string;
}
export interface SubTask {
id: string;
taskId: string;
jobStarted: string | null;
jobCompleted: string | null;
status: string;
inputs: string | null;
outputs: string | null;
cloudLogsURL: string | null;
updatedAt: string;
}
interface TasksResponse {
tasks: Task[];
total: number;
limit: number;
offset: number;
}
export default function IQDashboard() {
const [tasks, setTasks] = useState<Task[]>([]);
const [selectedTaskId, setSelectedTaskId] = useState<string | null>(null);
const [subtasks, setSubtasks] = useState<SubTask[]>([]);
const [loading, setLoading] = useState(true);
const [error, setError] = useState<string | null>(null);
const [total, setTotal] = useState(0);
const [limit, setLimit] = useState(20);
const [offset, setOffset] = useState(0);
const [loadingMore, setLoadingMore] = useState(false);
// Fetch tasks with pagination
const fetchTasks = async (newLimit: number, newOffset: number) => {
try {
if (newOffset === 0) setLoading(true);
else setLoadingMore(true);
const response = await fetch(
`/api/tasks?limit=${newLimit}&offset=${newOffset}`
);
if (!response.ok) throw new Error("Failed to fetch tasks");
const data: TasksResponse = await response.json();
if (newOffset === 0) {
setTasks(data.tasks);
} else {
setTasks((prev) => [...prev, ...data.tasks]);
}
setTotal(data.total);
setLimit(data.limit);
setOffset(newOffset + data.tasks.length);
setError(null);
} catch (err) {
setError(err instanceof Error ? err.message : "An error occurred");
if (newOffset === 0) setTasks([]);
} finally {
setLoading(false);
setLoadingMore(false);
}
};
// Initial load (first 20)
useEffect(() => {
fetchTasks(20, 0);
}, []);
const handleLoadMore = () => {
fetchTasks(20, offset);
};
const handleRefresh = () => {
setOffset(0);
fetchTasks(20, 0);
};
// Fetch subtasks when a task is selected
useEffect(() => {
if (!selectedTaskId) {
setSubtasks([]);
return;
}
const fetchSubtasks = async () => {
try {
const response = await fetch(`/api/tasks/${selectedTaskId}`);
if (!response.ok) throw new Error("Failed to fetch subtasks");
const data = await response.json();
setSubtasks(data);
} catch (err) {
setSubtasks([]);
}
};
fetchSubtasks();
}, [selectedTaskId]);
return (
<div className="flex h-screen bg-gray-50">
{/* Left sidebar - Task list */}
<div className="w-1/3 border-r border-gray-200 bg-white">
<TaskList
tasks={tasks}
selectedTaskId={selectedTaskId}
onSelectTask={setSelectedTaskId}
loading={loading}
loadingMore={loadingMore}
error={error}
total={total}
onLoadMore={handleLoadMore}
onRefresh={handleRefresh}
/>
</div>
{/* Right side - Subtask details */}
<div className="flex-1 overflow-auto">
{selectedTaskId ? (
<SubtaskDetails
selectedTaskId={selectedTaskId}
subtasks={subtasks}
task={tasks.find((t) => t.id === selectedTaskId)}
/>
) : (
<div className="flex items-center justify-center h-full text-gray-500">
Select a task to view its subtasks
</div>
)}
</div>
</div>
);
}

View file

@ -1,289 +0,0 @@
"use client";
import { useState } from "react";
import { SubTask, Task } from "./IQDashboard";
import { Badge } from "@/app/shadcn_components/ui/badge";
import { ScrollArea } from "@/app/shadcn_components/ui/scroll-area";
import { Card } from "@/app/shadcn_components/ui/card";
import { Button } from "@/app/shadcn_components/ui/button";
import { ChevronDown } from "lucide-react";
interface SubtaskDetailsProps {
selectedTaskId: string;
subtasks: SubTask[];
task?: Task;
}
function getStatusColor(
status: string
): "default" | "secondary" | "destructive" | "outline" {
switch (status.toLowerCase()) {
case "completed":
return "default";
case "in progress":
return "secondary";
case "failed":
return "destructive";
default:
return "outline";
}
}
function formatJson(jsonString: string | null): string {
if (!jsonString) return "N/A";
try {
return JSON.stringify(JSON.parse(jsonString), null, 2);
} catch {
return jsonString;
}
}
function CopyableCodeBlock({
content,
label,
}: {
content: string;
label: string;
}) {
const [copied, setCopied] = useState(false);
const handleCopy = async () => {
try {
await navigator.clipboard.writeText(content);
setCopied(true);
setTimeout(() => setCopied(false), 2000);
} catch (err) {
console.error("Failed to copy:", err);
}
};
return (
<div>
<div className="flex items-center justify-between mb-2">
<p className="text-sm font-medium text-gray-900">{label}</p>
<Button
variant="ghost"
size="sm"
onClick={handleCopy}
className="h-6 px-2 text-xs"
>
{copied ? "✓ Copied" : "Copy"}
</Button>
</div>
<pre className="bg-gray-50 p-3 rounded text-xs overflow-x-auto border border-gray-200 text-gray-700">
{content}
</pre>
</div>
);
}
interface ExpandedSubtask {
[key: string]: boolean;
}
function ExpandableSubtaskTile({
subtask,
index,
isExpanded,
onToggle,
}: {
subtask: SubTask;
index: number;
isExpanded: boolean;
onToggle: () => void;
}) {
return (
<Card className="overflow-hidden">
{/* Tile Header */}
<button
onClick={onToggle}
className="w-full p-4 flex items-center justify-between hover:bg-gray-50 transition-colors"
>
<div className="flex-1 text-left">
<p className="text-sm font-semibold text-gray-900">
Subtask {index + 1}
</p>
<code className="text-xs font-mono text-gray-600">
{subtask.id}
</code>
</div>
<div className="flex items-center gap-3">
<Badge variant={getStatusColor(subtask.status)}>
{subtask.status}
</Badge>
<ChevronDown
size={20}
className={`text-gray-500 transition-transform ${
isExpanded ? "rotate-180" : ""
}`}
/>
</div>
</button>
{/* Expanded Content */}
{isExpanded && (
<div className="border-t border-gray-200 p-4 space-y-4 bg-gray-50">
{/* Timeline */}
{(subtask.jobStarted || subtask.jobCompleted) && (
<div className="grid grid-cols-2 gap-3 text-sm">
{subtask.jobStarted && (
<div>
<p className="text-gray-600 text-xs font-medium">Started</p>
<p className="text-gray-900 text-xs">
{new Date(subtask.jobStarted).toLocaleString()}
</p>
</div>
)}
{subtask.jobCompleted && (
<div>
<p className="text-gray-600 text-xs font-medium">Completed</p>
<p className="text-gray-900 text-xs">
{new Date(subtask.jobCompleted).toLocaleString()}
</p>
</div>
)}
</div>
)}
{/* Inputs */}
{subtask.inputs && (
<CopyableCodeBlock
content={formatJson(subtask.inputs)}
label="Inputs"
/>
)}
{/* Outputs */}
{subtask.outputs && (
<CopyableCodeBlock
content={formatJson(subtask.outputs)}
label="Outputs"
/>
)}
{/* Cloud Logs */}
{subtask.cloudLogsURL && (
<div>
<p className="text-sm font-medium text-gray-900 mb-2">
Cloud Logs
</p>
<a
href={subtask.cloudLogsURL}
target="_blank"
rel="noopener noreferrer"
className="text-blue-600 hover:text-blue-800 text-xs break-all"
>
{subtask.cloudLogsURL}
</a>
</div>
)}
{/* Updated */}
<p className="text-xs text-gray-500">
Updated: {new Date(subtask.updatedAt).toLocaleString()}
</p>
</div>
)}
</Card>
);
}
export default function SubtaskDetails({
selectedTaskId,
subtasks,
task,
}: SubtaskDetailsProps) {
const [expandedSubtasks, setExpandedSubtasks] = useState<ExpandedSubtask>({});
const toggleSubtask = (subtaskId: string) => {
setExpandedSubtasks((prev) => ({
...prev,
[subtaskId]: !prev[subtaskId],
}));
};
return (
<div className="flex flex-col h-full">
{/* Task Header */}
{task && (
<div className="p-6 border-b border-gray-200 bg-white">
<div className="space-y-3">
<div className="flex items-center justify-between">
<h2 className="text-xl font-semibold text-gray-900">
{task.taskSource}
</h2>
<Badge variant={getStatusColor(task.status)}>
{task.status}
</Badge>
</div>
<div className="grid grid-cols-2 gap-4 text-sm">
<div>
<p className="text-gray-600">Task ID</p>
<code className="text-xs font-mono text-gray-900 break-all">
{task.id}
</code>
</div>
{task.service && (
<div>
<p className="text-gray-600">Service</p>
<p className="text-gray-900">{task.service}</p>
</div>
)}
{task.jobStarted && (
<div>
<p className="text-gray-600">Job Started</p>
<p className="text-gray-900 text-xs">
{new Date(task.jobStarted).toLocaleString()}
</p>
</div>
)}
{task.jobCompleted && (
<div>
<p className="text-gray-600">Job Completed</p>
<p className="text-gray-900 text-xs">
{new Date(task.jobCompleted).toLocaleString()}
</p>
</div>
)}
<div>
<p className="text-gray-600">Updated</p>
<p className="text-gray-900 text-xs">
{new Date(task.updatedAt).toLocaleString()}
</p>
</div>
</div>
</div>
</div>
)}
{/* Subtasks List */}
<ScrollArea className="flex-1 p-6">
<div className="space-y-6">
<div>
<h3 className="text-lg font-semibold text-gray-900 mb-4">
Subtasks ({subtasks.length})
</h3>
{subtasks.length === 0 && (
<div className="text-center py-8 text-gray-500">
No subtasks found
</div>
)}
<div className="space-y-3">
{subtasks.map((subtask, index) => (
<ExpandableSubtaskTile
key={subtask.id}
subtask={subtask}
index={index}
isExpanded={expandedSubtasks[subtask.id] || false}
onToggle={() => toggleSubtask(subtask.id)}
/>
))}
</div>
</div>
</div>
</ScrollArea>
</div>
);
}

View file

@ -1,293 +0,0 @@
"use client";
import { useState, useMemo } from "react";
import { Task } from "./IQDashboard";
import { Badge } from "@/app/shadcn_components/ui/badge";
import { ScrollArea } from "@/app/shadcn_components/ui/scroll-area";
import { Button } from "@/app/shadcn_components/ui/button";
import { Input } from "@/app/shadcn_components/ui/input";
import {
Select,
SelectContent,
SelectItem,
SelectTrigger,
SelectValue,
} from "@/app/shadcn_components/ui/select";
interface TaskListProps {
tasks: Task[];
selectedTaskId: string | null;
onSelectTask: (taskId: string) => void;
loading: boolean;
loadingMore: boolean;
error: string | null;
total: number;
onLoadMore: () => void;
onRefresh: () => void;
}
type SortOption = "recent" | "oldest" | "status" | "service";
function getStatusColor(
status: string
): "default" | "secondary" | "destructive" | "outline" {
switch (status.toLowerCase()) {
case "completed":
case "in progress":
return "default";
case "failed":
return "destructive";
default:
return "secondary";
}
}
export default function TaskList({
tasks,
selectedTaskId,
onSelectTask,
loading,
loadingMore,
error,
total,
onLoadMore,
onRefresh,
}: TaskListProps) {
const [searchQuery, setSearchQuery] = useState("");
const [statusFilter, setStatusFilter] = useState<string>("all");
const [serviceFilter, setServiceFilter] = useState<string>("all");
const [sortBy, setSortBy] = useState<SortOption>("recent");
// Get unique statuses and services for filter options
const uniqueStatuses = useMemo(
() => Array.from(new Set(tasks.map((t) => t.status))).sort(),
[tasks]
);
const uniqueServices = useMemo(
() =>
Array.from(new Set(tasks.map((t) => t.service).filter(Boolean))).sort() as string[],
[tasks]
);
// Filter and sort tasks
const filteredTasks = useMemo(() => {
let result = tasks;
// Status filter
if (statusFilter !== "all") {
result = result.filter((t) => t.status === statusFilter);
}
// Service filter
if (serviceFilter !== "all") {
result = result.filter((t) => t.service === serviceFilter);
}
// Search query
if (searchQuery) {
const query = searchQuery.toLowerCase();
result = result.filter(
(t) =>
t.id.toLowerCase().includes(query) ||
t.taskSource.toLowerCase().includes(query) ||
(t.service?.toLowerCase().includes(query) ?? false)
);
}
// Sort
const sorted = [...result];
switch (sortBy) {
case "recent":
sorted.sort(
(a, b) =>
new Date(b.updatedAt).getTime() - new Date(a.updatedAt).getTime()
);
break;
case "oldest":
sorted.sort(
(a, b) =>
new Date(a.updatedAt).getTime() - new Date(b.updatedAt).getTime()
);
break;
case "status":
sorted.sort((a, b) => a.status.localeCompare(b.status));
break;
case "service":
sorted.sort((a, b) =>
(a.service ?? "").localeCompare(b.service ?? "")
);
break;
}
return sorted;
}, [tasks, statusFilter, serviceFilter, searchQuery, sortBy]);
return (
<div className="flex flex-col h-full">
{/* Header */}
<div className="p-6 border-b border-gray-200 space-y-3">
<div className="flex items-center justify-between">
<div>
<h2 className="text-xl font-semibold text-gray-900">Tasks</h2>
<p className="text-sm text-gray-600 mt-1">
{filteredTasks.length} of {tasks.length} (Total: {total})
</p>
</div>
<Button
variant="outline"
size="sm"
onClick={onRefresh}
disabled={loading}
className="text-xs"
>
Refresh
</Button>
</div>
</div>
{/* Filters */}
<div className="p-4 border-b border-gray-200 space-y-3 bg-gray-50">
{/* Search */}
<Input
placeholder="Search by ID, source, or service..."
value={searchQuery}
onChange={(e) => setSearchQuery(e.target.value)}
className="text-sm"
/>
{/* Sort */}
<Select value={sortBy} onValueChange={(value) => setSortBy(value as SortOption)}>
<SelectTrigger className="text-sm">
<SelectValue />
</SelectTrigger>
<SelectContent>
<SelectItem value="recent">Most Recent</SelectItem>
<SelectItem value="oldest">Oldest First</SelectItem>
<SelectItem value="status">By Status</SelectItem>
<SelectItem value="service">By Service</SelectItem>
</SelectContent>
</Select>
{/* Status Filter */}
{uniqueStatuses.length > 0 && (
<Select value={statusFilter} onValueChange={setStatusFilter}>
<SelectTrigger className="text-sm">
<SelectValue />
</SelectTrigger>
<SelectContent>
<SelectItem value="all">All Statuses</SelectItem>
{uniqueStatuses.map((status) => (
<SelectItem key={status} value={status}>
{status}
</SelectItem>
))}
</SelectContent>
</Select>
)}
{/* Service Filter */}
{uniqueServices.length > 0 && (
<Select value={serviceFilter} onValueChange={setServiceFilter}>
<SelectTrigger className="text-sm">
<SelectValue />
</SelectTrigger>
<SelectContent>
<SelectItem value="all">All Services</SelectItem>
{uniqueServices.map((service) => (
<SelectItem key={service} value={service}>
{service}
</SelectItem>
))}
</SelectContent>
</Select>
)}
{/* Reset Filters */}
{(searchQuery || statusFilter !== "all" || serviceFilter !== "all") && (
<Button
variant="outline"
size="sm"
onClick={() => {
setSearchQuery("");
setStatusFilter("all");
setServiceFilter("all");
}}
className="w-full text-xs"
>
Clear Filters
</Button>
)}
</div>
{/* Content */}
<ScrollArea className="flex-1">
{error && (
<div className="p-4 m-4 bg-red-50 border border-red-200 rounded text-red-700">
{error}
</div>
)}
{loading && (
<div className="p-4 text-center text-gray-500">
Loading tasks...
</div>
)}
{!loading && !error && tasks.length === 0 && (
<div className="p-4 text-center text-gray-500">
No tasks found
</div>
)}
<div className="divide-y divide-gray-200 pb-4">
{filteredTasks.map((task) => (
<button
key={task.id}
onClick={() => onSelectTask(task.id)}
className={`w-full text-left p-4 transition-colors hover:bg-gray-50 ${
selectedTaskId === task.id ? "bg-blue-50 border-l-4 border-blue-500" : ""
}`}
>
<div className="space-y-2">
<div className="flex items-center justify-between">
<code className="text-xs font-mono text-gray-600 truncate">
{task.id}
</code>
<Badge variant={getStatusColor(task.status)}>
{task.status}
</Badge>
</div>
<div className="text-sm">
<p className="font-medium text-gray-900 truncate">
{task.taskSource}
</p>
{task.service && (
<p className="text-xs text-gray-500">{task.service}</p>
)}
</div>
<p className="text-xs text-gray-400">
{new Date(task.updatedAt).toLocaleString()}
</p>
</div>
</button>
))}
{/* Load More Button */}
{tasks.length < total && (
<div className="p-4 flex justify-center border-t border-gray-200">
<Button
variant="outline"
size="sm"
onClick={onLoadMore}
disabled={loadingMore}
className="text-xs"
>
{loadingMore ? "Loading..." : "Load More"}
</Button>
</div>
)}
</div>
</ScrollArea>
</div>
);
}

View file

@ -1,5 +0,0 @@
import IQDashboard from "./IQDashboard";
export default function IQPage() {
return <IQDashboard />;
}

View file

@ -1,548 +0,0 @@
"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";
import { useRouter } from "next/navigation";
import { handleNumericKeyDown } from "@/app/utils";
import {
Select,
SelectContent,
SelectGroup,
SelectItem,
SelectLabel,
SelectTrigger,
SelectValue,
} from "@/app/shadcn_components/ui/select";
import {
Dialog,
DialogContent,
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/measures/PlanTable";
import { UsersPermissionsCard } from "./UsersPermissionsCard";
import OrganisationLinkCard from "./OrganisationLinkCard";
// dropdown selection component for both goal and status
export function SettingsDropdown({
startingValue,
options,
setOption,
className,
}: {
startingValue: string;
options: string[];
setOption: (option: string) => void;
className?: string;
}) {
function handleValueChange(newValue: string) {
setOption(newValue);
}
return (
<Select onValueChange={(newValue) => handleValueChange(newValue)}>
<SelectTrigger className="w-full">
<SelectValue placeholder={startingValue} />
</SelectTrigger>
<SelectContent>
<SelectGroup>
{options.map((option, idx) => (
<SelectItem value={option} key={idx}>
{option}
</SelectItem>
))}
</SelectGroup>
</SelectContent>
</Select>
);
}
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,
isDomnaUser = false,
}: {
portfolioId: string;
portfolioSettingsData: PortfolioSettingsType;
isDomnaUser?: boolean;
}) {
// This is a client component so we can access the session directly
const session = useSession();
const router = useRouter();
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,
);
},
});
const [portfolioName, setPortfolioName] = useState(
portfolioSettingsData.name,
);
const [portfolioBudget, setPortfolioBudget] = useState<
number | string | null
>(portfolioSettingsData.budget);
const [portfolioGoal, setPortfolioGoal] = useState(
portfolioSettingsData.goal,
);
const [portfolioStatus, setPortfolioStatus] = useState(
portfolioSettingsData.status,
);
// Set up state for deleteModal and deleteConfirmation
const [isDeleteModalOpen, setIsDeleteModalOpen] = useState(false);
const [deleteConfirmationByName, setDeleteConfirmationByName] = useState("");
if (session.status === "loading") {
// You can return a loading spinner or placeholder here
return <div>Loading...</div>;
}
if (!session.data) {
// The user is not logged in, redirect them to sign in
return null;
}
const userId = session.data.user.dbId;
function handleOpenDeleteModal() {
setDeleteConfirmationByName("");
setIsDeleteModalOpen(true);
}
async function handleDeleteConfirmation() {
if (deleteConfirmationByName !== portfolioSettingsData.name) {
console.warn("Delete confirmation name does not match");
return;
}
try {
console.log("[DELETE] starting delete mutation");
await mutateDelete({
userId,
portfolioId,
});
console.log("[DELETE] mutation completed successfully");
// Refresh table / page data
router.refresh();
} catch (err) {
console.error("[DELETE] mutation failed", err);
}
}
// Change NAME functionality - changing state
function handlePortfolioNameChange(e: React.ChangeEvent<HTMLInputElement>) {
setPortfolioName(e.target.value);
}
// The onClick function called to update the NAME in the DB
function handleRename() {
mutate({
userId,
portfolioId,
name: portfolioName,
budget: null,
goal: null,
status: null,
});
}
// BUDGET CHANGING FUNCTIONS
// Change BUDGET functionality - changing state
function handlePortfolioBudgetUpdate(e: React.ChangeEvent<HTMLInputElement>) {
setPortfolioBudget(Number(e.target.value));
}
// The onClick function called to update the BUDGET in the DB
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 handleGoalUpdate() {
mutate({
userId,
portfolioId,
name: null,
budget: null,
goal: portfolioGoal,
status: null,
});
}
// The onClick function called to update the BUDGET in the DB
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="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>
<UsersPermissionsCard portfolioId={portfolioId} />
{isDomnaUser && <OrganisationLinkCard portfolioId={portfolioId} />}
<div className="rounded-md border border-red-500 mt-2">
<Table>
<TableHeader>
<TableRow>
<TableHead colSpan={2} className="text-lg text-brandblue">
Danger Zone:
</TableHead>
</TableRow>
</TableHeader>
<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="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>
);
}

View file

@ -1,32 +1,8 @@
import { getServerSession } from "next-auth";
import { AuthOptions } from "@/app/api/auth/[...nextauth]/authOptions";
import { getPortfolioSettings } from "../../utils";
import PortfolioSettings from "./PortfolioSettings";
import { redirect } from "next/navigation";
export default async function PortfolioSettingsPage(
props: {
params: Promise<{ slug: string }>;
}
) {
const params = await props.params;
const portfolioId = params.slug;
const [portfolioSettingsData, session] = await Promise.all([
getPortfolioSettings(portfolioId),
getServerSession(AuthOptions),
]);
const isDomnaUser = !!session?.user?.email?.endsWith("@domna.homes");
return (
<>
<div className="flex justify-center">
<PortfolioSettings
portfolioId={portfolioId}
portfolioSettingsData={portfolioSettingsData}
isDomnaUser={isDomnaUser}
/>
</div>
</>
);
export default async function SettingsRootPage(props: {
params: Promise<{ slug: string }>;
}) {
const { slug } = await props.params;
redirect(`/portfolio/${slug}/settings/general`);
}