mirror of
https://github.com/Hestia-Homes/assessment-model.git
synced 2026-06-30 12:55:02 +00:00
Merge pull request #71 from Hestia-Homes/feature/show_condition_json
Feature/show condition json
This commit is contained in:
commit
37476e8512
15 changed files with 182 additions and 54 deletions
|
|
@ -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.
|
||||
|
|
|
|||
|
|
@ -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
1
run_build.sh
Normal file
|
|
@ -0,0 +1 @@
|
|||
npm run build
|
||||
|
|
@ -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"
|
||||
|
|
|
|||
|
|
@ -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. */}
|
||||
|
|
|
|||
|
|
@ -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(),
|
||||
|
|
|
|||
|
|
@ -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[];
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
);
|
||||
};
|
||||
};
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
||||
|
|
|
|||
|
|
@ -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 };
|
||||
|
||||
|
|
|
|||
|
|
@ -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({
|
|||
​
|
||||
</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>
|
||||
|
|
|
|||
|
|
@ -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"
|
||||
>
|
||||
​
|
||||
</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>
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue