From a849731848b1481203dfa14074ba35a851e30467 Mon Sep 17 00:00:00 2001 From: Khalim Conn-Kowlessar Date: Sat, 4 Apr 2026 20:44:44 +0000 Subject: [PATCH] rebuilding the settings page and including logs --- src/app/iq/IQDashboard.tsx | 146 ----- src/app/iq/SubtaskDetails.tsx | 289 --------- src/app/iq/TaskList.tsx | 293 ---------- src/app/iq/page.tsx | 5 - .../settings/PortfolioSettings.tsx | 548 ------------------ .../[slug]/(portfolio)/settings/page.tsx | 36 +- 6 files changed, 6 insertions(+), 1311 deletions(-) delete mode 100644 src/app/iq/IQDashboard.tsx delete mode 100644 src/app/iq/SubtaskDetails.tsx delete mode 100644 src/app/iq/TaskList.tsx delete mode 100644 src/app/iq/page.tsx delete mode 100644 src/app/portfolio/[slug]/(portfolio)/settings/PortfolioSettings.tsx diff --git a/src/app/iq/IQDashboard.tsx b/src/app/iq/IQDashboard.tsx deleted file mode 100644 index 262a35f7..00000000 --- a/src/app/iq/IQDashboard.tsx +++ /dev/null @@ -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([]); - const [selectedTaskId, setSelectedTaskId] = useState(null); - const [subtasks, setSubtasks] = useState([]); - const [loading, setLoading] = useState(true); - const [error, setError] = useState(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 ( -
- {/* Left sidebar - Task list */} -
- -
- - {/* Right side - Subtask details */} -
- {selectedTaskId ? ( - t.id === selectedTaskId)} - /> - ) : ( -
- Select a task to view its subtasks -
- )} -
-
- ); -} diff --git a/src/app/iq/SubtaskDetails.tsx b/src/app/iq/SubtaskDetails.tsx deleted file mode 100644 index 7139a1ad..00000000 --- a/src/app/iq/SubtaskDetails.tsx +++ /dev/null @@ -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 ( -
-
-

{label}

- -
-
-        {content}
-      
-
- ); -} - -interface ExpandedSubtask { - [key: string]: boolean; -} - -function ExpandableSubtaskTile({ - subtask, - index, - isExpanded, - onToggle, -}: { - subtask: SubTask; - index: number; - isExpanded: boolean; - onToggle: () => void; -}) { - return ( - - {/* Tile Header */} - - - {/* Expanded Content */} - {isExpanded && ( -
- {/* Timeline */} - {(subtask.jobStarted || subtask.jobCompleted) && ( -
- {subtask.jobStarted && ( -
-

Started

-

- {new Date(subtask.jobStarted).toLocaleString()} -

-
- )} - {subtask.jobCompleted && ( -
-

Completed

-

- {new Date(subtask.jobCompleted).toLocaleString()} -

-
- )} -
- )} - - {/* Inputs */} - {subtask.inputs && ( - - )} - - {/* Outputs */} - {subtask.outputs && ( - - )} - - {/* Cloud Logs */} - {subtask.cloudLogsURL && ( -
-

- Cloud Logs -

- - {subtask.cloudLogsURL} - -
- )} - - {/* Updated */} -

- Updated: {new Date(subtask.updatedAt).toLocaleString()} -

-
- )} -
- ); -} - -export default function SubtaskDetails({ - selectedTaskId, - subtasks, - task, -}: SubtaskDetailsProps) { - const [expandedSubtasks, setExpandedSubtasks] = useState({}); - - const toggleSubtask = (subtaskId: string) => { - setExpandedSubtasks((prev) => ({ - ...prev, - [subtaskId]: !prev[subtaskId], - })); - }; - - return ( -
- {/* Task Header */} - {task && ( -
-
-
-

- {task.taskSource} -

- - {task.status} - -
-
-
-

Task ID

- - {task.id} - -
- {task.service && ( -
-

Service

-

{task.service}

-
- )} - {task.jobStarted && ( -
-

Job Started

-

- {new Date(task.jobStarted).toLocaleString()} -

-
- )} - {task.jobCompleted && ( -
-

Job Completed

-

- {new Date(task.jobCompleted).toLocaleString()} -

-
- )} -
-

Updated

-

- {new Date(task.updatedAt).toLocaleString()} -

-
-
-
-
- )} - - {/* Subtasks List */} - -
-
-

- Subtasks ({subtasks.length}) -

- - {subtasks.length === 0 && ( -
- No subtasks found -
- )} - -
- {subtasks.map((subtask, index) => ( - toggleSubtask(subtask.id)} - /> - ))} -
-
-
-
-
- ); -} diff --git a/src/app/iq/TaskList.tsx b/src/app/iq/TaskList.tsx deleted file mode 100644 index 8be56ccb..00000000 --- a/src/app/iq/TaskList.tsx +++ /dev/null @@ -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("all"); - const [serviceFilter, setServiceFilter] = useState("all"); - const [sortBy, setSortBy] = useState("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 ( -
- {/* Header */} -
-
-
-

Tasks

-

- {filteredTasks.length} of {tasks.length} (Total: {total}) -

-
- -
-
- - {/* Filters */} -
- {/* Search */} - setSearchQuery(e.target.value)} - className="text-sm" - /> - - {/* Sort */} - - - {/* Status Filter */} - {uniqueStatuses.length > 0 && ( - - )} - - {/* Service Filter */} - {uniqueServices.length > 0 && ( - - )} - - {/* Reset Filters */} - {(searchQuery || statusFilter !== "all" || serviceFilter !== "all") && ( - - )} -
- - {/* Content */} - - {error && ( -
- {error} -
- )} - - {loading && ( -
- Loading tasks... -
- )} - - {!loading && !error && tasks.length === 0 && ( -
- No tasks found -
- )} - -
- {filteredTasks.map((task) => ( - - ))} - - {/* Load More Button */} - {tasks.length < total && ( -
- -
- )} -
-
-
- ); -} diff --git a/src/app/iq/page.tsx b/src/app/iq/page.tsx deleted file mode 100644 index 44106780..00000000 --- a/src/app/iq/page.tsx +++ /dev/null @@ -1,5 +0,0 @@ -import IQDashboard from "./IQDashboard"; - -export default function IQPage() { - return ; -} diff --git a/src/app/portfolio/[slug]/(portfolio)/settings/PortfolioSettings.tsx b/src/app/portfolio/[slug]/(portfolio)/settings/PortfolioSettings.tsx deleted file mode 100644 index 169b9657..00000000 --- a/src/app/portfolio/[slug]/(portfolio)/settings/PortfolioSettings.tsx +++ /dev/null @@ -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 ( - - ); -} - -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
Loading...
; - } - - 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) { - 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) { - 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 ( -
-
- - - - - Rename the Portfolio: -

- Permanently change the name of your portfolio -

-
- - - - - - -
- - - Change the Portfolio Budget: -

- The total budget across ALL properties. Works aim to stay - within this budget -

-
- - handleNumericKeyDown(e)} - /> - - - - -
- - - Change the Portfolio Goal: -

- Adjust the overall aim of the works conducted on this - portfolio -

-
- - - - - - -
- - - Change the Status of the Portfolio: -

- Adjust where the portfolio stands in the works pipeline -

-
- - - - - - -
-
-
-
- - {isDomnaUser && } -
- - - - - Danger Zone: - - - - - - - - Delete the Portfolio: -

- Permanently delete the portfolio and all property data - assigned to this portfolio -

-
- - - - -
-
-
- - - Are you sure? -

- To confirm, please type the name of the portfolio ( - {portfolioSettingsData.name}) -

- setDeleteConfirmationByName(e.target.value)} - placeholder="Type portfolio name" - /> - - - - -
-
-
-
- ); -} diff --git a/src/app/portfolio/[slug]/(portfolio)/settings/page.tsx b/src/app/portfolio/[slug]/(portfolio)/settings/page.tsx index 4732c675..8d30a547 100644 --- a/src/app/portfolio/[slug]/(portfolio)/settings/page.tsx +++ b/src/app/portfolio/[slug]/(portfolio)/settings/page.tsx @@ -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 ( - <> -
- -
- - ); +export default async function SettingsRootPage(props: { + params: Promise<{ slug: string }>; +}) { + const { slug } = await props.params; + redirect(`/portfolio/${slug}/settings/general`); }