diff --git a/apps/www/src/app/(analytics)/analytics/page.tsx b/apps/www/src/app/(analytics)/analytics/page.tsx
index 12d4a7e..4705bdd 100644
--- a/apps/www/src/app/(analytics)/analytics/page.tsx
+++ b/apps/www/src/app/(analytics)/analytics/page.tsx
@@ -1,36 +1,35 @@
-import Link from "next/link";
-import { redirect } from "next/navigation";
-import { getTenants } from "@/actions/get-tenants";
+import Link from "next/link"
+import { redirect } from "next/navigation"
+import { getAnalyses } from "@/actions/get-analyst"
import {
Card,
CardContent,
CardHeader,
CardTitle,
-} from "@dingify/ui/components/card";
+} from "@dingify/ui/components/card"
-import { authOptions } from "@/lib/auth";
-import { prisma } from "@/lib/db";
-import { getCurrentUser } from "@/lib/session";
-import AddTenantDropdownButton from "@/components/buttons/AddTenantDropdownButton";
-import { AddTenantSheet } from "@/components/buttons/AddTenantSheet";
-import { AddWorkspaceButton } from "@/components/buttons/AddWorkspaceButton";
-import { DashboardHeader } from "@/components/dashboard/header";
-import { DashboardShell } from "@/components/dashboard/shell";
-import PropertyMap from "@/components/maps/PropertyMap";
-import { EmptyPlaceholder } from "@/components/shared/empty-placeholder";
+import { authOptions } from "@/lib/auth"
+import { prisma } from "@/lib/db"
+import { getCurrentUser } from "@/lib/session"
+import { AddAnalysisSheet } from "@/components/buttons/AddAnalysisSheet"
+import { AddWorkspaceButton } from "@/components/buttons/AddWorkspaceButton"
+import { DashboardHeader } from "@/components/dashboard/header"
+import { DashboardShell } from "@/components/dashboard/shell"
+import PropertyMap from "@/components/maps/PropertyMap"
+import { EmptyPlaceholder } from "@/components/shared/empty-placeholder"
export const metadata = {
title: "Propdock Analyser - Oversikt over dine analyser",
description:
"Monitor and analyze all your critical events in real-time. Access key metrics, track important journeys, and make data-driven decisions to optimize your business performance on the Dingify Dashboard.",
-};
+}
export default async function DashboardPage() {
- const user = await getCurrentUser();
+ const user = await getCurrentUser()
if (!user) {
- redirect(authOptions.pages?.signIn || "/login");
+ redirect(authOptions.pages?.signIn || "/login")
}
// Fetch workspace associated with the user
@@ -45,7 +44,7 @@ export default async function DashboardPage() {
select: {
id: true,
},
- });
+ })
if (!userWorkspace) {
return (
@@ -66,18 +65,18 @@ export default async function DashboardPage() {
- );
+ )
}
- // Fetch tenants associated with the user's workspace
- const { success, tenants = [], error } = await getTenants(userWorkspace.id);
+ // Fetch analyses associated with the user's workspace
+ const { success, analyses = [], error } = await getAnalyses(userWorkspace.id)
if (!success) {
return (
- );
+ )
}
return (
@@ -86,21 +85,21 @@ export default async function DashboardPage() {
heading="Analyser"
text="Dine analyser for dine eiendommer"
>
-
- {tenants.length === 0 ? (
+ {analyses.length === 0 ? (
- Legg til din fĂžrste leietaker
+ Legg til din fĂžrste analyse
- Du har ingen leietakere ennÄ. Legg til en leietaker for Ä komme i
+ Du har ingen analyser ennÄ. Legg til en analyse for Ä komme i
gang.
-
+
) : (
@@ -108,20 +107,19 @@ export default async function DashboardPage() {
- {tenants.map((tenant) => (
-
+ {analyses.map((analysis) => (
+
- {tenant.name}
+
+ {analysis.name}
+
- Organisasjonsnummer: {tenant.orgnr}
- Antall ansatte: {tenant.numEmployees}
- Bygning: {tenant.building?.name || "N/A"}
- Etasje: {tenant.floor ? tenant.floor.number : "N/A"}
-
- Kontorplass:{" "}
- {tenant.officeSpace ? tenant.officeSpace.name : "N/A"}
-
+ Bygning: {analysis.building?.name || "N/A"}
+ Utleibart areal: {analysis.rentableArea}
+ Leiepris per kvm per Ă„r: {analysis.rentPerArea}
+ Sum nÄverdi: {analysis.sumValueNow}
+ Exit verdi: {analysis.sumValueExit}
))}
@@ -130,5 +128,5 @@ export default async function DashboardPage() {
)}
- );
+ )
}
diff --git a/apps/www/src/app/(tenant)/tenant/[id]/invoice/page.tsx b/apps/www/src/app/(tenant)/tenant/[id]/invoice/page.tsx
new file mode 100644
index 0000000..30ad895
--- /dev/null
+++ b/apps/www/src/app/(tenant)/tenant/[id]/invoice/page.tsx
@@ -0,0 +1,83 @@
+import { getTenantDetails } from "@/actions/get-tenant-details"
+import { Settings } from "lucide-react"
+
+import { Button } from "@dingify/ui/components/button"
+import {
+ Card,
+ CardContent,
+ CardHeader,
+ CardTitle,
+} from "@dingify/ui/components/card"
+
+import { AddContactPersonSheet } from "@/components/buttons/AddContactPersonSheet"
+import { EditContactPersonSheet } from "@/components/buttons/EditContactPersonSheet"
+import { DashboardHeader } from "@/components/dashboard/header"
+import { DashboardShell } from "@/components/dashboard/shell"
+import { EmptyPlaceholder } from "@/components/shared/empty-placeholder"
+import TenantSendInvoice from "@/components/tenant/TenantSendInvoice"
+
+const mockCustomers = [
+ { id: "genesis", name: "Proaktiv Eiendomsmegling", orgnr: "123456789" },
+ { id: "explorer", name: "Neural Explorer", orgnr: "987654321" },
+ { id: "quantum", name: "Neural Quantum", orgnr: "192837465" },
+]
+
+const mockProducts = [
+ { id: "product1", name: "Produkt 1", price: 100 },
+ { id: "product2", name: "Produkt 2", price: 200 },
+ { id: "product3", name: "Produkt 3", price: 300 },
+]
+
+export default async function ContactPerson({
+ params,
+}: {
+ params: { id: string }
+}) {
+ const tenantId = params.id
+
+ if (!tenantId) {
+ return (
+
+
+
+ )
+ }
+
+ try {
+ const tenantDetails = await getTenantDetails(tenantId)
+
+ if (!tenantDetails || tenantDetails.contacts.length === 0) {
+ return (
+
+
+
+
+ )
+ }
+
+ return (
+
+
+ asdasda
+
+ )
+ } catch (error) {
+ return (
+
+
+
+ )
+ }
+}
diff --git a/apps/www/src/components/AnalysisCards.tsx b/apps/www/src/components/AnalysisCards.tsx
new file mode 100644
index 0000000..12b0da3
--- /dev/null
+++ b/apps/www/src/components/AnalysisCards.tsx
@@ -0,0 +1,105 @@
+import Link from "next/link"
+import { CircleUser, Menu, Package2, Search } from "lucide-react"
+
+import { Button } from "@/components/ui/button"
+import {
+ Card,
+ CardContent,
+ CardDescription,
+ CardFooter,
+ CardHeader,
+ CardTitle,
+} from "@/components/ui/card"
+import { Checkbox } from "@/components/ui/checkbox"
+import {
+ DropdownMenu,
+ DropdownMenuContent,
+ DropdownMenuItem,
+ DropdownMenuLabel,
+ DropdownMenuSeparator,
+ DropdownMenuTrigger,
+} from "@/components/ui/dropdown-menu"
+import { Input } from "@/components/ui/input"
+import { Sheet, SheetContent, SheetTrigger } from "@/components/ui/sheet"
+import { EditAnalysisSheet } from "@/components/buttons/EditAnalysisSheet" // Import the EditAnalysisSheet component
+
+export function AnalysisCards({ analysis }) {
+ return (
+
+
+
+ {/* Analysis Name Card */}
+
+
+ Analysis Name
+
+ This is the name of the financial analysis.
+
+
+
+
+
+
+
+
+
+
+ {/* Analysis Details Card */}
+
+
+ Analysis Details
+
+ Detailed information about the financial analysis.
+
+
+
+
+
+
+
+
+
+
+
+ )
+}
diff --git a/apps/www/src/components/analyse/AnalysisDetailsTable.tsx b/apps/www/src/components/analyse/AnalysisDetailsTable.tsx
new file mode 100644
index 0000000..dcb4d5b
--- /dev/null
+++ b/apps/www/src/components/analyse/AnalysisDetailsTable.tsx
@@ -0,0 +1,109 @@
+import { format } from "date-fns"
+
+import {
+ Card,
+ CardContent,
+ CardDescription,
+ CardHeader,
+ CardTitle,
+} from "@dingify/ui/components/card"
+import {
+ Table,
+ TableBody,
+ TableCell,
+ TableHead,
+ TableHeader,
+ TableRow,
+} from "@dingify/ui/components/table"
+
+interface AnalysisDetailsTableProps {
+ details: any
+}
+
+export function AnalysisDetailsTable({ details }: AnalysisDetailsTableProps) {
+ const tableHeaders = ["Field", "Value"]
+
+ const formatValue = (value: any) => {
+ if (value instanceof Date) {
+ return format(value, "yyyy-MM-dd")
+ }
+ return value
+ }
+
+ const dataRows = [
+ { label: "Building", value: details.building.name },
+ { label: "Rentable Area", value: details.rentableArea },
+ { label: "Rent Per Area", value: details.rentPerArea },
+ { label: "Sum Value Now", value: details.sumValueNow },
+ { label: "Sum Value Exit", value: details.sumValueExit },
+ {
+ label: "Appreciation Date",
+ value: formatValue(details.appreciationDate),
+ },
+ { label: "Last Day of Year", value: formatValue(details.lastDayOfYear) },
+ { label: "Last Balance Date", value: formatValue(details.lastBalanceDate) },
+ {
+ label: "Vacancy Per Year",
+ value: JSON.stringify(details.vacancyPerYear),
+ },
+ {
+ label: "Owner Costs Method",
+ value: details.ownerCostsMethod ? "True" : "False",
+ },
+ { label: "Owner Costs Manual", value: details.ownerCostsManual },
+ { label: "Cost Maintenance", value: details.costMaintenance },
+ { label: "Cost Insurance", value: details.costInsurance },
+ { label: "Cost Revision", value: details.costRevision },
+ { label: "Cost Adm", value: details.costAdm },
+ { label: "Cost Other", value: details.costOther },
+ { label: "Cost Negotiation", value: details.costNegotiation },
+ { label: "Cost Legal Fees", value: details.costLegalFees },
+ { label: "Cost Consult Fees", value: details.costConsultFees },
+ { label: "Cost Asset Mgmt", value: details.costAssetMgmt },
+ { label: "Cost Sum", value: details.costSum },
+ { label: "Use Calc ROI", value: details.useCalcROI ? "True" : "False" },
+ { label: "ROI Weighted Yield", value: details.roiWeightedYield },
+ { label: "ROI Inflation", value: details.roiInflation },
+ { label: "ROI Calculated", value: details.roiCalculated },
+ { label: "ROI Manual", value: details.roiManual },
+ { label: "Market Rent Office", value: details.marketRentOffice },
+ { label: "Market Rent Merch", value: details.marketRentMerch },
+ { label: "Market Rent Misc", value: details.marketRentMisc },
+ {
+ label: "Use Prime Yield",
+ value: details.usePrimeYield ? "True" : "False",
+ },
+ { label: "Manual Yield Office", value: details.manYieldOffice },
+ { label: "Manual Yield Merch", value: details.manYieldMerch },
+ { label: "Manual Yield Misc", value: details.manYieldMisc },
+ { label: "Manual Yield Weighted", value: details.manYieldWeighted },
+ ]
+
+ return (
+
+
+ Analysis Details
+ All details about the analysis
+
+
+
+
+
+ {tableHeaders.map((header) => (
+ {header}
+ ))}
+
+
+
+ {dataRows.map((row, index) => (
+
+ {row.label}
+ {formatValue(row.value)}
+
+ ))}
+
+
+
+
+ )
+}
diff --git a/apps/www/src/components/analyse/AnalysisTableDCF.tsx b/apps/www/src/components/analyse/AnalysisTableDCF.tsx
new file mode 100644
index 0000000..87fe752
--- /dev/null
+++ b/apps/www/src/components/analyse/AnalysisTableDCF.tsx
@@ -0,0 +1,422 @@
+import { differenceInDays } from "date-fns"
+
+import {
+ Card,
+ CardContent,
+ CardDescription,
+ CardHeader,
+ CardTitle,
+} from "@dingify/ui/components/card"
+import {
+ Table,
+ TableBody,
+ TableCell,
+ TableHead,
+ TableHeader,
+ TableRow,
+} from "@dingify/ui/components/table"
+import {
+ Tooltip,
+ TooltipContent,
+ TooltipProvider,
+ TooltipTrigger,
+} from "@dingify/ui/components/tooltip"
+
+interface AnalysisTableDCFProps {
+ details: any
+}
+
+export function AnalysisTableDCF({ details }: AnalysisTableDCFProps) {
+ const startYear = 2024
+ const endYear = 2033
+ const years = Array.from({ length: endYear - startYear + 1 }, (_, i) =>
+ (startYear + i).toString(),
+ )
+
+ const kpiAdjustments = [
+ 0, // First year, no adjustment
+ details.kpi1,
+ details.kpi2,
+ details.kpi3,
+ ...Array(endYear - startYear - 3).fill(details.kpi4),
+ ]
+
+ // Calculate rental incomes for each year
+ const rentalIncomes = [details.rentPerArea]
+ for (let i = 1; i < years.length; i++) {
+ const previousIncome = rentalIncomes[i - 1]
+ const kpi = kpiAdjustments[i] || 0
+ rentalIncomes.push(previousIncome * (1 + kpi / 100))
+ }
+
+ // Round the rental incomes for display
+ const roundedRentalIncomes = rentalIncomes.map((income) => Math.round(income))
+
+ // Calculate gross rental incomes
+ const grossRentalIncomes = roundedRentalIncomes.map(
+ (income) => income * details.rentableArea,
+ )
+
+ const formatNumber = (num: number) => {
+ return new Intl.NumberFormat("nb-NO", {
+ minimumFractionDigits: 0,
+ maximumFractionDigits: 0,
+ }).format(num)
+ }
+
+ // Parse the big expenses
+ const bigExpenses = JSON.parse(details.costBigExpenses || "{}")
+
+ // Calculate the sum of individual cost items
+ const calculateAutomaticCosts = () => {
+ const sum =
+ (details.costMaintenance || 0) +
+ (details.costInsurance || 0) +
+ (details.costRevision || 0) +
+ (details.costAdm || 0) +
+ (details.costOther || 0) +
+ (details.costNegotiation || 0) +
+ (details.costLegalFees || 0) +
+ (details.costConsultFees || 0) +
+ (details.costAssetMgmt || 0)
+
+ return sum
+ }
+
+ // Calculate the net rental incomes
+ const netRentalIncomes = grossRentalIncomes.map((grossIncome, index) => {
+ const year = years[index]
+ const totalCosts = details.ownerCostsMethod
+ ? calculateAutomaticCosts()
+ : details.ownerCostsManual || 0
+ const tenantAdjustments = bigExpenses[year] || 0
+ return grossIncome - totalCosts - tenantAdjustments
+ })
+
+ // Calculate "Andel inkludert i DCF" for each year
+ const andelInkludertIDCF = years.map((year, index) => {
+ const currentYear = new Date(`${year}-01-01`)
+ const lastDayOfYear = new Date(`${year}-12-31`)
+ const daysForDCF =
+ index === 0
+ ? differenceInDays(lastDayOfYear, new Date(details.appreciationDate))
+ : 365
+ return daysForDCF / 365
+ })
+
+ // Determine the discount rate based on useCalcROI
+ const discountRate = details.useCalcROI
+ ? details.roiWeightedYield + details.roiInflation + details.roiCalculated
+ : details.roiManual
+
+ // Calculate diskonteringsperiode, diskonteringsfaktor, and nÄverdi kontantstrÞm
+ const calculateDiscountValues = () => {
+ const discountValues = andelInkludertIDCF.map((percentage, index) => {
+ const discountPeriod = index === 0 ? percentage : index + percentage
+ const discountFactor = 1 / Math.pow(1 + discountRate, discountPeriod)
+ const discountedCashFlow = netRentalIncomes[index] * discountFactor
+ return {
+ discountPeriod: discountPeriod,
+ discountFactor: discountFactor,
+ discountedCashFlow: discountedCashFlow,
+ }
+ })
+ return discountValues
+ }
+
+ const discountValues = calculateDiscountValues()
+
+ // Calculate Sum NĂ„verdi by summing all the discounted cash flows
+ const sumNaverdi = discountValues.reduce(
+ (sum, value) => sum + value.discountedCashFlow,
+ 0,
+ )
+
+ // Calculate Exit Verdi using the last year's net rental income and yield
+ const exitYearNetIncome = netRentalIncomes[netRentalIncomes.length - 1]
+ const exitYield = details.usePrimeYield
+ ? details.manYieldWeighted
+ : (details.manYieldOffice + details.manYieldMerch + details.manYieldMisc) /
+ 3
+ const exitVerdi = exitYearNetIncome / exitYield
+
+ // Calculate Total Eiendomsverdi by summing Sum NĂ„verdi and Exit Verdi and multiplying by the last year's discount factor
+ const totalEiendomsverdi =
+ (sumNaverdi + exitVerdi) *
+ discountValues[discountValues.length - 1].discountFactor
+
+ return (
+ <>
+
+
+ KontantstrĂžm
+ KontantstrĂžm for eiendommen
+
+
+
+
+
+
+ NOK
+ {years.map((year) => (
+ {year}
+ ))}
+
+
+
+
+
+
+
+ Utleibart areal
+
+
+ Dette representerer utleibart areal
+
+
+
+ {years.map((year) => (
+
+ {formatNumber(details.rentableArea)}
+
+ ))}
+
+
+
+
+
+ LĂžpende leie
+
+
+ Dette representerer lĂžpende leie
+
+
+
+ {roundedRentalIncomes.map((income, index) => (
+
+ {formatNumber(income)}
+
+ ))}
+
+
+
+
+
+ Brutto leieinntekter
+
+
+ Utleibart areal x lĂžpende leie
+
+
+
+ {grossRentalIncomes.map((income, index) => (
+
+ {formatNumber(income)}
+
+ ))}
+
+
+ Kostnader
+
+ {details.ownerCostsMethod ? (
+
+
+
+
+ Automatisk regnet kostnader
+
+
+
+ Sum av alle individuelle kostnader: vedlikehold,
+ forsikring, revisjon, administrasjon, andre
+ driftskostnader, megling/utleie, juridiske
+ honorarer, honorar konsulenter, asset management.
+
+
+
+
+ {years.map((year) => (
+
+ {formatNumber(calculateAutomaticCosts())}
+
+ ))}
+
+ ) : (
+
+
+
+
+ Manuelle eierkostnader
+
+
+ Totale manuelle eierkostnader
+
+
+
+ {years.map((year) => (
+
+ {formatNumber(details.ownerCostsManual || 0)}
+
+ ))}
+
+ )}
+
+
+
+
+ Leietakertilpassninger
+
+
+ Kostnader for leietakertilpassninger
+
+
+
+ {years.map((year) => (
+
+ {formatNumber(bigExpenses[year] || 0)}
+
+ ))}
+
+
+
+
+
+ Netto leieinntekter
+
+
+
+ Brutto leieinntekter minus kostnader og
+ leietakertilpassninger
+
+
+
+
+ {netRentalIncomes.map((income, index) => (
+
+ {formatNumber(income)}
+
+ ))}
+
+
+
+ Diskonteringsdetaljer
+
+
+
+
+
+
+ Andel inkludert i DCF
+
+
+
+ Andel inkludert i DCF for hver periode. FĂžrste Ă„ret er
+ antall dager mellom verdsettelsesdato og slutten av
+ Äret delt pÄ 365. For pÄfÞlgende Är er det 365 dager
+ delt pÄ 365.
+
+
+
+
+ {andelInkludertIDCF.map((percentage, index) => (
+
+ {(percentage * 100).toFixed(2)}%
+
+ ))}
+
+
+
+
+
+ Diskonteringsperiode
+
+
+
+ Periode for diskontering. For fĂžrste Ă„ret er det andel
+ inkludert i DCF. For pÄfÞlgende Är er det Äret + andel
+ inkludert i DCF.
+
+
+
+
+ {discountValues.map((value, index) => (
+
+ {value.discountPeriod.toFixed(2)}
+
+ ))}
+
+
+
+
+
+ Diskonteringsfaktor
+
+
+
+ Faktor for diskontering, beregnet som 1 / (1 +
+ diskonteringsrate) ^ diskonteringsperiode.
+
+
+
+
+ {discountValues.map((value, index) => (
+
+ {value.discountFactor.toFixed(4)}
+
+ ))}
+
+
+
+
+
+ NĂ„verdi kontantstrĂžm
+
+
+
+ NĂ„verdi av kontantstrĂžm, beregnet som netto
+ leieinntekter multiplisert med diskonteringsfaktor.
+
+
+
+
+ {discountValues.map((value, index) => (
+
+ {formatNumber(value.discountedCashFlow)}
+
+ ))}
+
+
+
+
+
+
+
+
+
+
+ Sum NĂ„verdi
+
+
+ {formatNumber(sumNaverdi)}
+
+
+
+
+ Exit Verdi
+
+
+ {formatNumber(exitVerdi)}
+
+
+
+
+ Total Eiendomsverdi
+
+
+ {formatNumber(totalEiendomsverdi)}
+
+
+
+ >
+ )
+}
diff --git a/apps/www/src/components/analyse/EditAnalysisNameCard.tsx b/apps/www/src/components/analyse/EditAnalysisNameCard.tsx
new file mode 100644
index 0000000..c6ed926
--- /dev/null
+++ b/apps/www/src/components/analyse/EditAnalysisNameCard.tsx
@@ -0,0 +1,159 @@
+"use client"
+
+import { useState } from "react"
+import { updateAnalysis } from "@/actions/update-analysis"
+import { zodResolver } from "@hookform/resolvers/zod"
+import { CalendarIcon } from "@radix-ui/react-icons"
+import { format } from "date-fns"
+import { useForm } from "react-hook-form"
+import { toast } from "sonner"
+import { z } from "zod"
+
+import { Button } from "@dingify/ui/components/button"
+import { Calendar } from "@dingify/ui/components/calendar"
+import {
+ Card,
+ CardContent,
+ CardDescription,
+ CardHeader,
+ CardTitle,
+} from "@dingify/ui/components/card"
+import {
+ Form,
+ FormControl,
+ FormField,
+ FormItem,
+ FormLabel,
+ FormMessage,
+} from "@dingify/ui/components/form"
+import { Input } from "@dingify/ui/components/input"
+import {
+ Popover,
+ PopoverContent,
+ PopoverTrigger,
+} from "@dingify/ui/components/popover"
+
+import { cn } from "@/lib/utils"
+
+const FormSchema = z.object({
+ name: z.string().min(2, {
+ message: "Name must be at least 2 characters.",
+ }),
+ appreciationDate: z.date({
+ required_error: "Appreciation date is required.",
+ }),
+})
+
+interface EditAnalysisNameCardProps {
+ analysisId: number
+ initialName: string
+ initialDate: Date
+}
+
+export function EditAnalysisNameCard({
+ analysisId,
+ initialName,
+ initialDate,
+}: EditAnalysisNameCardProps) {
+ const [isLoading, setIsLoading] = useState(false)
+
+ const form = useForm
>({
+ resolver: zodResolver(FormSchema),
+ defaultValues: {
+ name: initialName,
+ appreciationDate: initialDate,
+ },
+ })
+
+ async function onSubmit(data: z.infer) {
+ setIsLoading(true)
+ try {
+ const result = await updateAnalysis(analysisId, {
+ name: data.name,
+ appreciationDate: data.appreciationDate,
+ })
+ if (result.success) {
+ toast.success("Analysis updated successfully.")
+ } else {
+ throw new Error(result.error || "Failed to update analysis.")
+ }
+ } catch (error) {
+ toast.error(error.message)
+ console.error("Error updating analysis:", error)
+ } finally {
+ setIsLoading(false)
+ }
+ }
+
+ return (
+
+
+ Navn
+ Endre navn og dato pÄ analysen.
+
+
+
+
+
+
+ )
+}
diff --git a/apps/www/src/components/analyse/EditKpiCard.tsx.tsx b/apps/www/src/components/analyse/EditKpiCard.tsx.tsx
new file mode 100644
index 0000000..b244e30
--- /dev/null
+++ b/apps/www/src/components/analyse/EditKpiCard.tsx.tsx
@@ -0,0 +1,175 @@
+"use client"
+
+import { useState } from "react"
+import { updateAnalysis } from "@/actions/update-analysis"
+import { zodResolver } from "@hookform/resolvers/zod"
+import { useForm } from "react-hook-form"
+import { toast } from "sonner"
+import { z } from "zod"
+
+import { Button } from "@dingify/ui/components/button"
+import {
+ Card,
+ CardContent,
+ CardDescription,
+ CardHeader,
+ CardTitle,
+} from "@dingify/ui/components/card"
+import {
+ Form,
+ FormControl,
+ FormField,
+ FormItem,
+ FormLabel,
+ FormMessage,
+} from "@dingify/ui/components/form"
+import { Input } from "@dingify/ui/components/input"
+
+const FormSchema = z.object({
+ kpi1: z
+ .string()
+ .nonempty("KPI 1 is required.")
+ .refine((value) => !isNaN(Number(value)), {
+ message: "KPI 1 must be a number.",
+ }),
+ kpi2: z
+ .string()
+ .nonempty("KPI 2 is required.")
+ .refine((value) => !isNaN(Number(value)), {
+ message: "KPI 2 must be a number.",
+ }),
+ kpi3: z
+ .string()
+ .nonempty("KPI 3 is required.")
+ .refine((value) => !isNaN(Number(value)), {
+ message: "KPI 3 must be a number.",
+ }),
+ kpi4: z
+ .string()
+ .nonempty("KPI 4 is required.")
+ .refine((value) => !isNaN(Number(value)), {
+ message: "KPI 4 must be a number.",
+ }),
+})
+
+interface EditKpiCardProps {
+ analysisId: number
+ initialKpi1: number
+ initialKpi2: number
+ initialKpi3: number
+ initialKpi4: number
+}
+
+export function EditKpiCard({
+ analysisId,
+ initialKpi1,
+ initialKpi2,
+ initialKpi3,
+ initialKpi4,
+}: EditKpiCardProps) {
+ const [isLoading, setIsLoading] = useState(false)
+
+ const form = useForm>({
+ resolver: zodResolver(FormSchema),
+ defaultValues: {
+ kpi1: initialKpi1.toString(),
+ kpi2: initialKpi2.toString(),
+ kpi3: initialKpi3.toString(),
+ kpi4: initialKpi4.toString(),
+ },
+ })
+
+ async function onSubmit(data: z.infer) {
+ setIsLoading(true)
+ try {
+ const result = await updateAnalysis(analysisId, {
+ kpi1: Number(data.kpi1),
+ kpi2: Number(data.kpi2),
+ kpi3: Number(data.kpi3),
+ kpi4: Number(data.kpi4),
+ })
+ if (result.success) {
+ toast.success("Analysis updated successfully.")
+ } else {
+ throw new Error(result.error || "Failed to update analysis.")
+ }
+ } catch (error) {
+ toast.error(error.message)
+ console.error("Error updating analysis:", error)
+ } finally {
+ setIsLoading(false)
+ }
+ }
+
+ return (
+
+
+ KPI Values
+ Edit the KPI values.
+
+
+
+
+
+
+ )
+}
diff --git a/apps/www/src/components/analyse/EditMarketDataCard.tsx b/apps/www/src/components/analyse/EditMarketDataCard.tsx
new file mode 100644
index 0000000..69201a0
--- /dev/null
+++ b/apps/www/src/components/analyse/EditMarketDataCard.tsx
@@ -0,0 +1,293 @@
+"use client"
+
+import { useState } from "react"
+import { updateAnalysis } from "@/actions/update-analysis"
+import { zodResolver } from "@hookform/resolvers/zod"
+import { useForm } from "react-hook-form"
+import { toast } from "sonner"
+import { z } from "zod"
+
+import { Button } from "@dingify/ui/components/button"
+import {
+ Card,
+ CardContent,
+ CardDescription,
+ CardHeader,
+ CardTitle,
+} from "@dingify/ui/components/card"
+import {
+ Form,
+ FormControl,
+ FormField,
+ FormItem,
+ FormLabel,
+ FormMessage,
+} from "@dingify/ui/components/form"
+import { Input } from "@dingify/ui/components/input"
+import { Switch } from "@dingify/ui/components/switch"
+
+const FormSchema = z.object({
+ marketRentOffice: z
+ .string()
+ .nonempty("Market Rent Office is required.")
+ .refine((value) => !isNaN(Number(value)), {
+ message: "Market Rent Office must be a number.",
+ }),
+ marketRentMerch: z
+ .string()
+ .nonempty("Market Rent Merch is required.")
+ .refine((value) => !isNaN(Number(value)), {
+ message: "Market Rent Merch must be a number.",
+ }),
+ marketRentMisc: z
+ .string()
+ .nonempty("Market Rent Misc is required.")
+ .refine((value) => !isNaN(Number(value)), {
+ message: "Market Rent Misc must be a number.",
+ }),
+ usePrimeYield: z.boolean().default(false).optional(),
+ manYieldOffice: z
+ .string()
+ .nonempty("Manual Yield Office is required.")
+ .optional(),
+ manYieldMerch: z
+ .string()
+ .nonempty("Manual Yield Merch is required.")
+ .optional(),
+ manYieldMisc: z
+ .string()
+ .nonempty("Manual Yield Misc is required.")
+ .optional(),
+ manYieldWeighted: z
+ .string()
+ .nonempty("Manual Yield Weighted is required.")
+ .optional(),
+})
+
+interface EditMarketDataCardProps {
+ analysisId: number
+ initialMarketRentOffice: number
+ initialMarketRentMerch: number
+ initialMarketRentMisc: number
+ initialUsePrimeYield: boolean
+ initialManYieldOffice?: number
+ initialManYieldMerch?: number
+ initialManYieldMisc?: number
+ initialManYieldWeighted?: number
+}
+
+export function EditMarketDataCard({
+ analysisId,
+ initialMarketRentOffice,
+ initialMarketRentMerch,
+ initialMarketRentMisc,
+ initialUsePrimeYield,
+ initialManYieldOffice,
+ initialManYieldMerch,
+ initialManYieldMisc,
+ initialManYieldWeighted,
+}: EditMarketDataCardProps) {
+ const [isLoading, setIsLoading] = useState(false)
+
+ const form = useForm>({
+ resolver: zodResolver(FormSchema),
+ defaultValues: {
+ marketRentOffice: initialMarketRentOffice.toString(),
+ marketRentMerch: initialMarketRentMerch.toString(),
+ marketRentMisc: initialMarketRentMisc.toString(),
+ usePrimeYield: initialUsePrimeYield,
+ manYieldOffice: initialManYieldOffice?.toString() || "",
+ manYieldMerch: initialManYieldMerch?.toString() || "",
+ manYieldMisc: initialManYieldMisc?.toString() || "",
+ manYieldWeighted: initialManYieldWeighted?.toString() || "",
+ },
+ })
+
+ async function onSubmit(data: z.infer) {
+ setIsLoading(true)
+ try {
+ const result = await updateAnalysis(analysisId, {
+ marketRentOffice: Number(data.marketRentOffice),
+ marketRentMerch: Number(data.marketRentMerch),
+ marketRentMisc: Number(data.marketRentMisc),
+ usePrimeYield: data.usePrimeYield,
+ manYieldOffice: data.manYieldOffice
+ ? Number(data.manYieldOffice)
+ : undefined,
+ manYieldMerch: data.manYieldMerch
+ ? Number(data.manYieldMerch)
+ : undefined,
+ manYieldMisc: data.manYieldMisc ? Number(data.manYieldMisc) : undefined,
+ manYieldWeighted: data.manYieldWeighted
+ ? Number(data.manYieldWeighted)
+ : undefined,
+ })
+ if (result.success) {
+ toast.success("Analysis updated successfully.")
+ } else {
+ throw new Error(result.error || "Failed to update analysis.")
+ }
+ } catch (error) {
+ toast.error(error.message)
+ console.error("Error updating analysis:", error)
+ } finally {
+ setIsLoading(false)
+ }
+ }
+
+ return (
+
+
+ Market Data
+ Edit market rent and yield data.
+
+
+
+
+
+
+ )
+}
diff --git a/apps/www/src/components/analyse/EditOwnerCostsCard.tsx b/apps/www/src/components/analyse/EditOwnerCostsCard.tsx
new file mode 100644
index 0000000..3c7808c
--- /dev/null
+++ b/apps/www/src/components/analyse/EditOwnerCostsCard.tsx
@@ -0,0 +1,367 @@
+"use client"
+
+import { useState } from "react"
+import { updateAnalysis } from "@/actions/update-analysis"
+import { zodResolver } from "@hookform/resolvers/zod"
+import { useForm } from "react-hook-form"
+import { toast } from "sonner"
+import { z } from "zod"
+
+import { Button } from "@dingify/ui/components/button"
+import {
+ Card,
+ CardContent,
+ CardDescription,
+ CardHeader,
+ CardTitle,
+} from "@dingify/ui/components/card"
+import {
+ Form,
+ FormControl,
+ FormField,
+ FormItem,
+ FormLabel,
+ FormMessage,
+} from "@dingify/ui/components/form"
+import { Input } from "@dingify/ui/components/input"
+import { Switch } from "@dingify/ui/components/switch"
+
+const FormSchema = z.object({
+ ownerCostsMethod: z.boolean().default(false).optional(),
+ ownerCostsManual: z
+ .string()
+ .nonempty("Owner Costs Manual is required.")
+ .optional(),
+ costMaintenance: z
+ .string()
+ .nonempty("Cost Maintenance is required.")
+ .optional(),
+ costInsurance: z.string().nonempty("Cost Insurance is required.").optional(),
+ costRevision: z.string().nonempty("Cost Revision is required.").optional(),
+ costAdm: z.string().nonempty("Cost Adm is required.").optional(),
+ costOther: z.string().nonempty("Cost Other is required.").optional(),
+ costNegotiation: z
+ .string()
+ .nonempty("Cost Negotiation is required.")
+ .optional(),
+ costLegalFees: z.string().nonempty("Cost Legal Fees is required.").optional(),
+ costConsultFees: z
+ .string()
+ .nonempty("Cost Consult Fees is required.")
+ .optional(),
+ costAssetMgmt: z.string().nonempty("Cost Asset Mgmt is required.").optional(),
+ costSum: z.string().nonempty("Cost Sum is required.").optional(),
+})
+
+interface EditOwnerCostsCardProps {
+ analysisId: number
+ initialOwnerCostsMethod: boolean
+ initialOwnerCostsManual?: number
+ initialCostMaintenance?: number
+ initialCostInsurance?: number
+ initialCostRevision?: number
+ initialCostAdm?: number
+ initialCostOther?: number
+ initialCostNegotiation?: number
+ initialCostLegalFees?: number
+ initialCostConsultFees?: number
+ initialCostAssetMgmt?: number
+ initialCostSum?: number
+}
+
+export function EditOwnerCostsCard({
+ analysisId,
+ initialOwnerCostsMethod,
+ initialOwnerCostsManual,
+ initialCostMaintenance,
+ initialCostInsurance,
+ initialCostRevision,
+ initialCostAdm,
+ initialCostOther,
+ initialCostNegotiation,
+ initialCostLegalFees,
+ initialCostConsultFees,
+ initialCostAssetMgmt,
+ initialCostSum,
+}: EditOwnerCostsCardProps) {
+ const [isLoading, setIsLoading] = useState(false)
+
+ const form = useForm>({
+ resolver: zodResolver(FormSchema),
+ defaultValues: {
+ ownerCostsMethod: initialOwnerCostsMethod,
+ ownerCostsManual: initialOwnerCostsManual?.toString() || "",
+ costMaintenance: initialCostMaintenance?.toString() || "",
+ costInsurance: initialCostInsurance?.toString() || "",
+ costRevision: initialCostRevision?.toString() || "",
+ costAdm: initialCostAdm?.toString() || "",
+ costOther: initialCostOther?.toString() || "",
+ costNegotiation: initialCostNegotiation?.toString() || "",
+ costLegalFees: initialCostLegalFees?.toString() || "",
+ costConsultFees: initialCostConsultFees?.toString() || "",
+ costAssetMgmt: initialCostAssetMgmt?.toString() || "",
+ costSum: initialCostSum?.toString() || "",
+ },
+ })
+
+ async function onSubmit(data: z.infer) {
+ setIsLoading(true)
+ try {
+ const result = await updateAnalysis(analysisId, {
+ ownerCostsMethod: data.ownerCostsMethod,
+ ownerCostsManual: data.ownerCostsManual
+ ? Number(data.ownerCostsManual)
+ : undefined,
+ costMaintenance: data.costMaintenance
+ ? Number(data.costMaintenance)
+ : undefined,
+ costInsurance: data.costInsurance
+ ? Number(data.costInsurance)
+ : undefined,
+ costRevision: data.costRevision ? Number(data.costRevision) : undefined,
+ costAdm: data.costAdm ? Number(data.costAdm) : undefined,
+ costOther: data.costOther ? Number(data.costOther) : undefined,
+ costNegotiation: data.costNegotiation
+ ? Number(data.costNegotiation)
+ : undefined,
+ costLegalFees: data.costLegalFees
+ ? Number(data.costLegalFees)
+ : undefined,
+ costConsultFees: data.costConsultFees
+ ? Number(data.costConsultFees)
+ : undefined,
+ costAssetMgmt: data.costAssetMgmt
+ ? Number(data.costAssetMgmt)
+ : undefined,
+ costSum: data.costSum ? Number(data.costSum) : undefined,
+ })
+ if (result.success) {
+ toast.success("Analysis updated successfully.")
+ } else {
+ throw new Error(result.error || "Failed to update analysis.")
+ }
+ } catch (error) {
+ toast.error(error.message)
+ console.error("Error updating analysis:", error)
+ } finally {
+ setIsLoading(false)
+ }
+ }
+
+ return (
+
+
+ Owner Costs
+ Edit owner costs and related values.
+
+
+
+
+
+
+ )
+}
diff --git a/apps/www/src/components/analyse/EditROICard.tsx b/apps/www/src/components/analyse/EditROICard.tsx
new file mode 100644
index 0000000..a728fea
--- /dev/null
+++ b/apps/www/src/components/analyse/EditROICard.tsx
@@ -0,0 +1,194 @@
+"use client"
+
+import { useState } from "react"
+import { updateAnalysis } from "@/actions/update-analysis"
+import { zodResolver } from "@hookform/resolvers/zod"
+import { useForm } from "react-hook-form"
+import { toast } from "sonner"
+import { z } from "zod"
+
+import { Button } from "@dingify/ui/components/button"
+import {
+ Card,
+ CardContent,
+ CardDescription,
+ CardHeader,
+ CardTitle,
+} from "@dingify/ui/components/card"
+import {
+ Form,
+ FormControl,
+ FormField,
+ FormItem,
+ FormLabel,
+ FormMessage,
+} from "@dingify/ui/components/form"
+import { Input } from "@dingify/ui/components/input"
+import { Switch } from "@dingify/ui/components/switch"
+
+const FormSchema = z.object({
+ useCalcROI: z.boolean().default(false).optional(),
+ roiWeightedYield: z
+ .string()
+ .nonempty("Weighted Yield is required.")
+ .optional(),
+ roiInflation: z.string().nonempty("Inflation is required.").optional(),
+ roiCalculated: z.string().nonempty("Calculated ROI is required.").optional(),
+ roiManual: z.string().nonempty("Manual ROI is required.").optional(),
+})
+
+interface EditROICardProps {
+ analysisId: number
+ initialUseCalcROI: boolean
+ initialROIWeightedYield?: number
+ initialROIInflation?: number
+ initialROICalculated?: number
+ initialROIManual?: number
+}
+
+export function EditROICard({
+ analysisId,
+ initialUseCalcROI,
+ initialROIWeightedYield,
+ initialROIInflation,
+ initialROICalculated,
+ initialROIManual,
+}: EditROICardProps) {
+ const [isLoading, setIsLoading] = useState(false)
+
+ const form = useForm>({
+ resolver: zodResolver(FormSchema),
+ defaultValues: {
+ useCalcROI: initialUseCalcROI,
+ roiWeightedYield: initialROIWeightedYield?.toString() || "",
+ roiInflation: initialROIInflation?.toString() || "",
+ roiCalculated: initialROICalculated?.toString() || "",
+ roiManual: initialROIManual?.toString() || "",
+ },
+ })
+
+ async function onSubmit(data: z.infer) {
+ setIsLoading(true)
+ try {
+ const result = await updateAnalysis(analysisId, {
+ useCalcROI: data.useCalcROI,
+ roiWeightedYield: data.roiWeightedYield
+ ? Number(data.roiWeightedYield)
+ : undefined,
+ roiInflation: data.roiInflation ? Number(data.roiInflation) : undefined,
+ roiCalculated: data.roiCalculated
+ ? Number(data.roiCalculated)
+ : undefined,
+ roiManual: data.roiManual ? Number(data.roiManual) : undefined,
+ })
+ if (result.success) {
+ toast.success("Analysis updated successfully.")
+ } else {
+ throw new Error(result.error || "Failed to update analysis.")
+ }
+ } catch (error) {
+ toast.error(error.message)
+ console.error("Error updating analysis:", error)
+ } finally {
+ setIsLoading(false)
+ }
+ }
+
+ return (
+
+
+ ROI Inputs
+ Edit ROI inputs and related values.
+
+
+
+
+
+
+ )
+}
diff --git a/apps/www/src/components/analyse/EditRentableAreaCard.tsx b/apps/www/src/components/analyse/EditRentableAreaCard.tsx
new file mode 100644
index 0000000..59ad195
--- /dev/null
+++ b/apps/www/src/components/analyse/EditRentableAreaCard.tsx
@@ -0,0 +1,197 @@
+"use client"
+
+import { useState } from "react"
+import { updateAnalysis } from "@/actions/update-analysis"
+import { zodResolver } from "@hookform/resolvers/zod"
+import { useForm } from "react-hook-form"
+import { toast } from "sonner"
+import { z } from "zod"
+
+import { Button } from "@dingify/ui/components/button"
+import {
+ Card,
+ CardContent,
+ CardDescription,
+ CardHeader,
+ CardTitle,
+} from "@dingify/ui/components/card"
+import {
+ Form,
+ FormControl,
+ FormField,
+ FormItem,
+ FormLabel,
+ FormMessage,
+} from "@dingify/ui/components/form"
+import { Input } from "@dingify/ui/components/input"
+
+const FormSchema = z.object({
+ rentableArea: z
+ .string()
+ .nonempty("Rentable area is required.")
+ .refine((value) => !isNaN(Number(value)), {
+ message: "Rentable area must be a number.",
+ }),
+ ratioAreaOffice: z
+ .string()
+ .nonempty("Ratio Area Office is required.")
+ .refine(
+ (value) =>
+ !isNaN(Number(value)) && Number(value) >= 0 && Number(value) <= 1,
+ {
+ message: "Ratio Area Office must be a number between 0 and 1.",
+ },
+ ),
+ ratioAreaMerch: z
+ .string()
+ .nonempty("Ratio Area Merch is required.")
+ .refine(
+ (value) =>
+ !isNaN(Number(value)) && Number(value) >= 0 && Number(value) <= 1,
+ {
+ message: "Ratio Area Merch must be a number between 0 and 1.",
+ },
+ ),
+ ratioAreaMisc: z
+ .string()
+ .nonempty("Ratio Area Misc is required.")
+ .refine(
+ (value) =>
+ !isNaN(Number(value)) && Number(value) >= 0 && Number(value) <= 1,
+ {
+ message: "Ratio Area Misc must be a number between 0 and 1.",
+ },
+ ),
+})
+
+interface EditRentableAreaCardProps {
+ analysisId: number
+ initialRentableArea: number
+ initialRatioAreaOffice: number
+ initialRatioAreaMerch: number
+ initialRatioAreaMisc: number
+}
+
+export function EditRentableAreaCard({
+ analysisId,
+ initialRentableArea,
+ initialRatioAreaOffice,
+ initialRatioAreaMerch,
+ initialRatioAreaMisc,
+}: EditRentableAreaCardProps) {
+ const [isLoading, setIsLoading] = useState(false)
+
+ const form = useForm>({
+ resolver: zodResolver(FormSchema),
+ defaultValues: {
+ rentableArea: initialRentableArea.toString(),
+ ratioAreaOffice: initialRatioAreaOffice.toString(),
+ ratioAreaMerch: initialRatioAreaMerch.toString(),
+ ratioAreaMisc: initialRatioAreaMisc.toString(),
+ },
+ })
+
+ async function onSubmit(data: z.infer) {
+ setIsLoading(true)
+ try {
+ const result = await updateAnalysis(analysisId, {
+ rentableArea: Number(data.rentableArea),
+ ratioAreaOffice: Number(data.ratioAreaOffice),
+ ratioAreaMerch: Number(data.ratioAreaMerch),
+ ratioAreaMisc: Number(data.ratioAreaMisc),
+ })
+ if (result.success) {
+ toast.success("Analysis updated successfully.")
+ } else {
+ throw new Error(result.error || "Failed to update analysis.")
+ }
+ } catch (error) {
+ toast.error(error.message)
+ console.error("Error updating analysis:", error)
+ } finally {
+ setIsLoading(false)
+ }
+ }
+
+ return (
+
+
+ Rentable Area
+ Edit rentable area and ratios.
+
+
+
+
+
+
+ )
+}
diff --git a/apps/www/src/components/analyse/EditVacancyCard.tsx b/apps/www/src/components/analyse/EditVacancyCard.tsx
new file mode 100644
index 0000000..9675d9c
--- /dev/null
+++ b/apps/www/src/components/analyse/EditVacancyCard.tsx
@@ -0,0 +1,152 @@
+"use client"
+
+import { useState } from "react"
+import { updateAnalysis } from "@/actions/update-analysis"
+import { zodResolver } from "@hookform/resolvers/zod"
+import { useForm } from "react-hook-form"
+import { toast } from "sonner"
+import { z } from "zod"
+
+import { Button } from "@dingify/ui/components/button"
+import {
+ Card,
+ CardContent,
+ CardDescription,
+ CardHeader,
+ CardTitle,
+} from "@dingify/ui/components/card"
+import {
+ Form,
+ FormControl,
+ FormField,
+ FormItem,
+ FormLabel,
+ FormMessage,
+} from "@dingify/ui/components/form"
+import { Input } from "@dingify/ui/components/input"
+import {
+ Select,
+ SelectContent,
+ SelectItem,
+ SelectTrigger,
+ SelectValue,
+} from "@dingify/ui/components/select"
+
+const FormSchema = z.object({
+ year: z.string().min(1, "Year is required"),
+ value: z.string().min(1, "Value is required"),
+})
+
+interface EditVacancyCardProps {
+ analysisId: number
+ initialVacancyPerYear: string // JSON string of vacancy per year
+}
+
+export function EditVacancyCard({
+ analysisId,
+ initialVacancyPerYear,
+}: EditVacancyCardProps) {
+ const [isLoading, setIsLoading] = useState(false)
+ const [vacancyData, setVacancyData] = useState(
+ JSON.parse(initialVacancyPerYear),
+ )
+
+ const form = useForm>({
+ resolver: zodResolver(FormSchema),
+ defaultValues: {
+ year: "",
+ value: "",
+ },
+ })
+
+ const years = Array.from({ length: 10 }, (_, i) =>
+ (new Date().getFullYear() + i).toString(),
+ )
+
+ async function onSubmit(data: z.infer) {
+ setIsLoading(true)
+ try {
+ const updatedVacancyData = { ...vacancyData, [data.year]: data.value }
+ const result = await updateAnalysis(analysisId, {
+ vacancyPerYear: JSON.stringify(updatedVacancyData),
+ })
+ if (result.success) {
+ toast.success("Analysis updated successfully.")
+ setVacancyData(updatedVacancyData)
+ } else {
+ throw new Error(result.error || "Failed to update analysis.")
+ }
+ } catch (error) {
+ toast.error(error.message)
+ console.error("Error updating analysis:", error)
+ } finally {
+ setIsLoading(false)
+ }
+ }
+
+ return (
+
+
+ Vacancy Per Year
+ Edit vacancy per year values.
+
+
+
+
+
+ {Object.entries(vacancyData).map(([year, value]) => (
+
+ {year}
+ {value}
+
+ ))}
+
+
+
+ )
+}
diff --git a/apps/www/src/components/buttons/AddAnalysisSheet.tsx b/apps/www/src/components/buttons/AddAnalysisSheet.tsx
new file mode 100644
index 0000000..a2b8991
--- /dev/null
+++ b/apps/www/src/components/buttons/AddAnalysisSheet.tsx
@@ -0,0 +1,215 @@
+"use client"
+
+import { useEffect, useState } from "react"
+import { createAnalysis } from "@/actions/create-analyse"
+import { getBuildings } from "@/actions/get-buildings" // Import this function
+import { getProperties } from "@/actions/get-properties"
+import { zodResolver } from "@hookform/resolvers/zod"
+import { useForm } from "react-hook-form"
+import { toast } from "sonner"
+import { z } from "zod"
+
+import { Button } from "@dingify/ui/components/button"
+import {
+ Form,
+ FormControl,
+ FormField,
+ FormItem,
+ FormLabel,
+ FormMessage,
+} from "@dingify/ui/components/form"
+import { Input } from "@dingify/ui/components/input"
+import {
+ Select,
+ SelectContent,
+ SelectItem,
+ SelectTrigger,
+ SelectValue,
+} from "@dingify/ui/components/select"
+import {
+ Sheet,
+ SheetContent,
+ SheetDescription,
+ SheetFooter,
+ SheetHeader,
+ SheetTitle,
+ SheetTrigger,
+} from "@dingify/ui/components/sheet"
+
+const AnalysisSchema = z.object({
+ name: z.string().min(1, "Name is required"),
+ propertyId: z.string().min(1, "Property is required"),
+ buildingId: z.string().min(1, "Building is required"),
+})
+
+interface Property {
+ id: string
+ name: string
+}
+
+interface Building {
+ id: string
+ name: string
+}
+
+export function AddAnalysisSheet() {
+ const [isLoading, setIsLoading] = useState(false)
+ const [properties, setProperties] = useState([])
+ const [buildings, setBuildings] = useState([])
+
+ const form = useForm({
+ resolver: zodResolver(AnalysisSchema),
+ defaultValues: {
+ name: "",
+ propertyId: "",
+ buildingId: "",
+ },
+ })
+
+ useEffect(() => {
+ async function fetchProperties() {
+ try {
+ const properties = await getProperties()
+ setProperties(properties)
+ } catch (error) {
+ console.error("Failed to fetch properties:", error)
+ }
+ }
+ fetchProperties()
+ }, [])
+
+ const onPropertyChange = async (propertyId: string) => {
+ form.setValue("propertyId", propertyId)
+ try {
+ const buildings = await getBuildings(propertyId)
+ setBuildings(buildings)
+ form.setValue("buildingId", "") // Reset building selection when property changes
+ } catch (error) {
+ console.error("Failed to fetch buildings:", error)
+ }
+ }
+
+ const onSubmit = async (data: z.infer) => {
+ setIsLoading(true)
+
+ try {
+ const analysisData = {
+ ...data,
+ }
+
+ const result = await createAnalysis(analysisData)
+
+ if (!result.success) {
+ throw new Error(result.error || "Failed to save analysis.")
+ }
+
+ toast.success(`Analysis for property was saved.`)
+ form.reset()
+ // Optionally, refresh the page or update the state to show the new analysis
+ } catch (error) {
+ toast.error(error.message)
+ console.error(error)
+ } finally {
+ setIsLoading(false)
+ }
+ }
+
+ return (
+
+
+
+
+
+
+ Lag en ny analyse
+ Skriv basis informasjon
+
+
+
+
+
+
+ )
+}
diff --git a/apps/www/src/components/shared/icons.tsx b/apps/www/src/components/shared/icons.tsx
index 49934ed..0844497 100644
--- a/apps/www/src/components/shared/icons.tsx
+++ b/apps/www/src/components/shared/icons.tsx
@@ -143,3 +143,47 @@ export const Icons = {
),
}
+
+export function PlusIcon(props: LucideProps) {
+ return (
+
+ )
+}
+
+export function BirdIcon(props: LucideProps) {
+ return (
+
+ )
+}
diff --git a/apps/www/src/components/tenant/TenantSendInvoice.tsx b/apps/www/src/components/tenant/TenantSendInvoice.tsx
new file mode 100644
index 0000000..4fd456a
--- /dev/null
+++ b/apps/www/src/components/tenant/TenantSendInvoice.tsx
@@ -0,0 +1,538 @@
+"use client"
+
+import { useEffect, useState } from "react"
+import { zodResolver } from "@hookform/resolvers/zod"
+import { addDays, differenceInCalendarDays, format } from "date-fns"
+import { nb } from "date-fns/locale"
+import { CalendarIcon, PlusIcon, SendIcon } from "lucide-react"
+import { useForm, useWatch } from "react-hook-form"
+import { toast } from "sonner"
+import { z } from "zod"
+
+import { Button } from "@dingify/ui/components/button"
+import { Calendar } from "@dingify/ui/components/calendar"
+import {
+ Form,
+ FormControl,
+ FormDescription,
+ FormField,
+ FormItem,
+ FormLabel,
+ FormMessage,
+} from "@dingify/ui/components/form"
+import { Input } from "@dingify/ui/components/input"
+import {
+ Popover,
+ PopoverContent,
+ PopoverTrigger,
+} from "@dingify/ui/components/popover"
+import {
+ Select,
+ SelectContent,
+ SelectItem,
+ SelectTrigger,
+ SelectValue,
+} from "@dingify/ui/components/select"
+import { Separator } from "@dingify/ui/components/separator"
+import { Textarea } from "@dingify/ui/components/textarea"
+
+import { cn } from "@/lib/utils"
+
+const InvoiceSchema = z.object({
+ customer: z.string().min(1, "Kunde er pÄkrevd"),
+ email: z.string().email("Ugyldig epostadresse").min(1, "Epost er pÄkrevd"),
+ ourReference: z.string().optional(),
+ theirReference: z.string().optional(),
+ orderReference: z.string().optional(),
+ product: z.string().min(1, "Produkt er pÄkrevd"),
+ quantity: z.number().min(1, "Antall mÄ vÊre minst 1"),
+ price: z.number().min(0, "Pris mÄ vÊre minst 0"),
+ invoiceEmail: z
+ .string()
+ .email("Ugyldig epostadresse")
+ .min(1, "Epost er pÄkrevd"),
+ date: z.date({ required_error: "Dato er pÄkrevd" }),
+ dueDate: z.date({ required_error: "Forfallsdato er pÄkrevd" }),
+ accountNumber: z.string().min(1, "Kontonummer er pÄkrevd"),
+ comment: z.string().optional(),
+})
+
+export default function TenantSendInvoice({ customers, products }) {
+ const today = new Date()
+ const fourteenDaysFromToday = addDays(today, 14)
+
+ const form = useForm({
+ resolver: zodResolver(InvoiceSchema),
+ defaultValues: {
+ customer: "",
+ email: "",
+ ourReference: "",
+ theirReference: "",
+ orderReference: "",
+ product: "",
+ quantity: 1,
+ price: 0,
+ invoiceEmail: "",
+ date: today,
+ dueDate: fourteenDaysFromToday,
+ accountNumber: "",
+ comment: "",
+ },
+ })
+
+ const quantity = useWatch({ control: form.control, name: "quantity" })
+ const price = useWatch({ control: form.control, name: "price" })
+ const date = useWatch({ control: form.control, name: "date" })
+ const dueDate = useWatch({ control: form.control, name: "dueDate" })
+
+ const totalPrice = quantity * price
+ const vat = totalPrice * 0.25
+ const totalPriceWithVat = totalPrice + vat
+
+ const daysBetween =
+ date && dueDate ? differenceInCalendarDays(dueDate, date) : 0
+
+ const onSubmit = async (data) => {
+ const promise = new Promise((resolve, reject) => {
+ setTimeout(() => {
+ const isSuccess = true
+ if (isSuccess) {
+ resolve({ name: "Invoice" })
+ } else {
+ reject("Error creating invoice")
+ }
+ }, 2000)
+ })
+
+ toast.promise(promise, {
+ loading: "Vennligst vent...",
+ success: (data) => {
+ return `Faktura er blitt sendt`
+ },
+ error: "Error",
+ })
+
+ promise
+ .then(() => {
+ console.log(data)
+ form.reset()
+ })
+ .catch((error) => {
+ console.error(error)
+ })
+ }
+
+ return (
+
+
+
+ Lag faktura
+
+
+
+
+
+
+
+
+
+
+
+ )
+}