Merge pull request #71 from Hestia-Homes/feature/show_condition_json

Feature/show condition json
This commit is contained in:
KhalimCK 2025-08-21 16:20:35 +01:00 committed by GitHub
commit 37476e8512
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
15 changed files with 182 additions and 54 deletions

View file

@ -19,7 +19,7 @@ import './commands'
// Alternatively you can use CommonJS syntax:
// require('./commands')
import { mount } from 'cypress/react18'
import { mount } from 'cypress/react'
// Augment the Cypress namespace to include type definitions for
// your custom command.

View file

@ -3,4 +3,5 @@ import type { Config } from "drizzle-kit";
export default {
schema: "./src/app/db/schema/*",
out: "./src/app/db/migrations",
dialect: "postgresql",
} satisfies Config;

1
run_build.sh Normal file
View file

@ -0,0 +1 @@
npm run build

View file

@ -108,7 +108,7 @@ function Nav({ userImage }: { userImage: string }) {
>
{(ref) => (
<div className="md:hidden" id="mobile-menu">
<div ref={ref} className="px-2 pt-2 pb-3 space-y-1 sm:px-3">
<div ref={ref as React.MutableRefObject<HTMLDivElement | null>} className="px-2 pt-2 pb-3 space-y-1 sm:px-3">
<a
href="/home"
className="hover:bg-hoverblue text-white block px-3 py-2 rounded-md text-base font-medium"

View file

@ -1,4 +1,4 @@
import { Dialog, Transition } from "@headlessui/react";
import { Dialog, DialogBackdrop, DialogPanel, DialogTitle, Transition, TransitionChild } from "@headlessui/react";
import { Fragment, useState } from "react";
const SelectComparisonModal = ({
@ -38,7 +38,7 @@ const SelectComparisonModal = ({
leaveFrom="opacity-100"
leaveTo="opacity-0"
>
<Dialog.Overlay className="fixed inset-0 bg-black opacity-30" />
<DialogBackdrop className="fixed inset-0 bg-black/30" />
</Transition.Child>
{/* This element is to trick the browser into centering the modal contents. */}

View file

@ -10,7 +10,7 @@ import {
boolean,
} from "drizzle-orm/pg-core";
import { InferModel } from "drizzle-orm";
import { C } from "drizzle-orm/db.d-cf0abe10";
// import { C } from "drizzle-orm/db.d-cf0abe10";
export const solar = pgTable("solar", {
id: bigserial("id", { mode: "bigint" }).primaryKey(),

View file

@ -1,5 +1,6 @@
import { pgTable, uuid, text, timestamp } from "drizzle-orm/pg-core";
import { pgEnum } from "drizzle-orm/pg-core";
export const DB_REPORT_TYPES = [
"ECO_CONDITION_REPORT",
@ -23,4 +24,8 @@ export const uploaded_files = pgTable("uploaded_files", {
s3JsonUploadTimestamp: timestamp("s3_json_upload_timestamp", { withTimezone: true }),
uprn: text("uprn").notNull(),
});
});
export type getUploadedFile = typeof uploaded_files.$inferSelect
export type getUploadedFiles = getUploadedFile[];

View file

@ -1,8 +1,9 @@
// insertUploadedFile.ts
import { uploaded_files } from "@/app/db/surveyDB/schema/surveyDB";
import { uploaded_files, docTypeEnum } from "@/app/db/surveyDB/schema/surveyDB";
import { surveyDB } from "../connection";
import type { ReportType, ReportTypeSchema} from "../schema/documents";
import { reportTypeToDbLabel } from "../schema/documents";
type DbDocType = (typeof docTypeEnum.enumValues)[number];
export interface UploadedFileInput {
s3JsonUri?: string; // optional
@ -19,7 +20,7 @@ export async function insertUploadedFile(data: UploadedFileInput) {
.values({
s3JsonUri: data.s3JsonUri ?? null, // Pass null if missing
s3FileUri: data.s3FileUri,
docType: reportTypeToDbLabel[data.docType], // map UI value -> DB enum NAME
docType: reportTypeToDbLabel[data.docType] as DbDocType, // map UI value -> DB enum NAME
s3FileUploadTimestamp: data.s3FileUploadTimestamp,
s3JsonUploadTimestamp: data.s3JsonUploadTimestamp ?? null, // Pass null if missing
uprn: data.uprn,

View file

@ -1,20 +1,42 @@
"use client";
import React, { useState } from "react";
import { useRouter } from "next/navigation";
import React from "react";
import { TableCell, TableRow } from "@/app/shadcn_components/ui/table";
import { BrandButton } from "@/app/components/Buttons";
import { UploadModal } from "./UploadModal";
import { documentTypeTitles, type ReportType } from "@/app/db/surveyDB/schema/documents";
import type { getUploadedFiles, getUploadedFile } from "@/app/db/surveyDB/schema/surveyDB";
type Props = {
reportType: ReportType; // <- the only type selector needed
reportType: ReportType;
uprn: string;
files: getUploadedFiles;
};
export const DocumentSection: React.FC<Props> = ({ reportType, uprn }) => {
const [showUploadModal, setShowUploadModal] = useState(false);
export const DocumentSection: React.FC<Props> = ({ reportType, uprn, files }) => {
const [showUploadModal, setShowUploadModal] = React.useState(false);
const router = useRouter();
const latestFile = React.useMemo<getUploadedFile | null>(() => {
if (!files?.length) return null;
return files.reduce((acc, cur) => {
const accTime = new Date(acc.s3FileUploadTimestamp as any).getTime();
const curTime = new Date(cur.s3FileUploadTimestamp as any).getTime();
return curTime > accTime ? cur : acc;
}, files[0]);
}, [files]);
const formatWhen = (d: string | Date) =>
new Intl.DateTimeFormat(undefined, {
year: "numeric",
month: "short",
day: "2-digit",
hour: "2-digit",
minute: "2-digit",
}).format(new Date(d));
const title = documentTypeTitles[reportType];
const count = files.length;
return (
<>
@ -23,7 +45,30 @@ export const DocumentSection: React.FC<Props> = ({ reportType, uprn }) => {
{title}
</TableCell>
<TableCell className="px-6 py-4 text-sm text-gray-500" />
<TableCell className="px-6 py-4 text-sm text-gray-500">
{latestFile ? (
<div className="flex flex-col gap-1">
<div className="flex items-center gap-3">
<a
href={latestFile.s3FileUri}
target="_blank"
rel="noopener noreferrer"
className="underline underline-offset-2 hover:no-underline"
>
View latest file
</a>
<span className="text-xs text-gray-400">
uploaded {formatWhen(latestFile.s3FileUploadTimestamp)}
</span>
</div>
<span className="text-xs text-gray-500">
{count} file{count !== 1 && "s"} on record
</span>
</div>
) : (
<span className="text-gray-400">No files uploaded yet</span>
)}
</TableCell>
<TableCell className="px-6 py-4 text-sm text-right w-1/6">
<BrandButton
@ -31,11 +76,13 @@ export const DocumentSection: React.FC<Props> = ({ reportType, uprn }) => {
onClick={() => setShowUploadModal(true)}
backgroundColor="brandblue"
/>
<UploadModal
open={showUploadModal}
onClose={() => setShowUploadModal(false)}
documentType={reportType} // <- strong ReportType
onClose={() => {
setShowUploadModal(false);
router.refresh();
}}
documentType={reportType}
uprn={uprn}
/>
</TableCell>

View file

@ -2,26 +2,60 @@
import React from "react";
import { Table, TableBody, TableRow, TableCell } from "@/app/shadcn_components/ui/table";
import { DocumentSection } from "./DocumentSection";
import { type ReportType, REPORT_TYPES, documentTypeFileTypes, documentTypeTitles } from "@/app/db/surveyDB/schema/documents";
import {
type ReportType,
REPORT_TYPES,
dbLabelToReportType, // <-- import the map
} from "@/app/db/surveyDB/schema/documents";
import type { getUploadedFile } from "@/app/db/surveyDB/schema/surveyDB";
type Props = { uprn: string };
type Props = {
uprn: string;
uploadedFilesData: getUploadedFile[];
};
export const DocumentsTable: React.FC<Props> = ({ uprn, uploadedFilesData }) => {
const filesByType = React.useMemo(() => {
const map: Partial<Record<ReportType, getUploadedFile[]>> = {};
for (const file of uploadedFilesData ?? []) {
const uiKey = dbLabelToReportType[file.docType]; // map DB → UI
if (!uiKey) continue; // unknown/legacy type? skip safely
(map[uiKey] ??= []).push(file);
}
// newest first within each group
Object.values(map).forEach(arr =>
arr!.sort(
(a, b) =>
new Date(b.s3FileUploadTimestamp as any).getTime() -
new Date(a.s3FileUploadTimestamp as any).getTime()
)
);
return map;
}, [uploadedFilesData]);
export const DocumentsTable: React.FC<Props> = ({ uprn }) => {
return (
<Table className="min-w-full table-fixed divide-y divide-gray-200 shadow overflow-hidden border-b border-gray-200 sm:rounded-lg">
<TableBody className="bg-white divide-y divide-gray-200">
{REPORT_TYPES.map((rt) => (
<React.Fragment key={rt}>
<DocumentSection
reportType={rt as ReportType}
uprn={uprn}
/>
<TableRow className="hover:bg-transparent">
<TableCell colSpan={3} className="h-3 p-0" />
</TableRow>
</React.Fragment>
))}
{REPORT_TYPES.map((reportType) => {
const filesForType = filesByType[reportType] ?? [];
return (
<React.Fragment key={reportType}>
<DocumentSection
reportType={reportType}
uprn={uprn}
files={filesForType} // array of rows
/>
<TableRow className="hover:bg-transparent">
<TableCell colSpan={3} className="h-3 p-0" />
</TableRow>
</React.Fragment>
);
})}
</TableBody>
</Table>
);
};
};

View file

@ -1,8 +1,22 @@
import { getPropertyMeta } from "@/app/portfolio/[slug]/building-passport/[propertyId]/utils";
import { eq } from "drizzle-orm";
import { DocumentsTable } from "./DocumentsTable";
import { surveyDB } from "@/app/db/surveyDB/connection";
import { uploaded_files } from "@/app/db/surveyDB/schema/surveyDB";
import { type getUploadedFiles } from "@/app/db/surveyDB/schema/surveyDB";
import { EmptyObject } from "react-hook-form";
async function getDocuments(
uprn: number
): Promise< getUploadedFiles> {
const result = surveyDB.query.uploaded_files.findMany({
where: eq(uploaded_files.uprn, String(uprn)),
});
return result;
}
export default async function DocumentsPage(
props: {
params: Promise<{ slug: string; propertyId: string }>;
@ -16,6 +30,7 @@ export default async function DocumentsPage(
}
const propertyMeta = await getPropertyMeta(propertyId);
const uploadedFiles = await getDocuments(propertyMeta.uprn);
return (
<>
@ -26,6 +41,7 @@ export default async function DocumentsPage(
<div className="py-4">
<DocumentsTable
uprn={propertyMeta.uprn.toString()}
uploadedFilesData={uploadedFiles}
/>
</div>

View file

@ -2,7 +2,7 @@ import { Menu, Transition } from "@headlessui/react";
import { Fragment } from "react";
import { Button } from "@/app/shadcn_components/ui/button";
import { PlusIcon, ChevronDownIcon } from "@heroicons/react/20/solid";
import { Float } from "@headlessui-float/react";
// import { Float } from "@headlessui-float/react";
export type Option = { label: string; value: string; disabled?: boolean };

View file

@ -1,6 +1,14 @@
"use client";
import { Dialog, Transition } from "@headlessui/react";
import {
Dialog,
DialogBackdrop,
DialogPanel,
DialogTitle,
Transition,
TransitionChild,
} from "@headlessui/react";
import { Fragment, useMemo } from "react";
import { Input } from "@/app/shadcn_components/ui/input";
import { Button } from "@/app/shadcn_components/ui/button";
@ -542,7 +550,7 @@ export default function RemoteAssessmentModal({
onClose={() => setIsOpen(false)}
>
<div className="min-h-screen px-4 text-center">
<Transition.Child
<TransitionChild
as={Fragment}
enter="ease-out duration-300"
enterFrom="opacity-0"
@ -551,8 +559,8 @@ export default function RemoteAssessmentModal({
leaveFrom="opacity-100"
leaveTo="opacity-0"
>
<Dialog.Overlay className="fixed inset-0 bg-black bg-opacity-25" />
</Transition.Child>
<DialogBackdrop className="fixed inset-0 bg-black/25" />
</TransitionChild>
{/* Spacer for centering */}
<span
@ -562,7 +570,7 @@ export default function RemoteAssessmentModal({
&#8203;
</span>
<Transition.Child
<TransitionChild
as={Fragment}
enter="ease-out duration-300"
enterFrom="opacity-0 scale-95"
@ -571,10 +579,10 @@ export default function RemoteAssessmentModal({
leaveFrom="opacity-100 scale-100"
leaveTo="opacity-0 scale-95"
>
<div className="inline-block w-full max-w-2xl p-6 my-8 overflow-visible text-left align-middle transition-all transform bg-white shadow-xl rounded-2xl">
<Dialog.Title className="text-lg font-medium">
<DialogPanel className="inline-block w-full max-w-2xl p-6 my-8 overflow-visible text-left align-middle transition-all transform bg-white shadow-xl rounded-2xl">
<DialogTitle className="text-lg font-medium">
Remote Assessment Details
</Dialog.Title>
</DialogTitle>
<FormProvider {...form}>
<form onSubmit={onSubmit} className="space-y-6 mt-4">
@ -929,8 +937,8 @@ export default function RemoteAssessmentModal({
)}
</form>
</FormProvider>
</div>
</Transition.Child>
</DialogPanel>
</TransitionChild>
</div>
</Dialog>
</Transition>

View file

@ -1,6 +1,14 @@
"use client";
import { Dialog, Transition } from "@headlessui/react";
import {
Dialog,
DialogBackdrop,
DialogPanel,
DialogTitle,
Transition,
TransitionChild,
} from "@headlessui/react";
import { Fragment, useMemo, useState } from "react";
import { useMutation } from "@tanstack/react-query";
import { InputFile } from "@/app/portfolio/[slug]/components/InputFile";
@ -427,7 +435,7 @@ export default function UploadCsvModal({
onClose={() => setIsOpen(false)}
>
<div className="min-h-screen px-4 text-center">
<Transition.Child
<TransitionChild
as={Fragment}
enter="ease-out duration-300"
enterFrom="opacity-0"
@ -436,15 +444,15 @@ export default function UploadCsvModal({
leaveFrom="opacity-100"
leaveTo="opacity-0"
>
<Dialog.Overlay className="fixed inset-0 bg-black bg-opacity-25" />
</Transition.Child>
<DialogBackdrop className="fixed inset-0 bg-black bg-opacity-25" />
</TransitionChild>
<span
className="inline-block h-screen align-middle"
aria-hidden="true"
>
&#8203;
</span>
<Transition.Child
<TransitionChild
as={Fragment}
enter="ease-out duration-300"
enterFrom="opacity-0 scale-95"
@ -454,9 +462,9 @@ export default function UploadCsvModal({
leaveTo="opacity-0 scale-95"
>
<div className="inline-block w-full max-w-2xl p-6 my-8 overflow-visible text-left align-middle transition-all transform bg-white shadow-xl rounded-2xl">
<Dialog.Title className="text-lg font-medium">
<DialogTitle className="text-lg font-medium">
Upload Property Data
</Dialog.Title>
</DialogTitle>
<FormProvider {...form}>
<form
onSubmit={onSubmit}
@ -745,7 +753,7 @@ export default function UploadCsvModal({
</form>
</FormProvider>
</div>
</Transition.Child>
</TransitionChild>
</div>
</Dialog>
</Transition>

View file

@ -14,9 +14,16 @@ const DialogPortal = ({
className,
children,
...props
}: DialogPrimitive.DialogPortalProps) => (
<DialogPrimitive.Portal className={cn(className)} {...props}>
<div className="fixed inset-0 z-50 flex items-start justify-center sm:items-center">
}: React.ComponentPropsWithoutRef<typeof DialogPrimitive.Portal> & {
className?: string
}) => (
<DialogPrimitive.Portal {...props}>
<div
className={cn(
"fixed inset-0 z-50 flex items-start justify-center sm:items-center",
className
)}
>
{children}
</div>
</DialogPrimitive.Portal>