Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 8 additions & 0 deletions client/src/App.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,10 @@ import Sidebar from "@/components/layout/Sidebar";
import Header from "@/components/layout/Header";
import Connections from "@/pages/Connections";
import Dashboard from "@/pages/Dashboard";
import Transactions from "@/pages/Transactions";
import Accounts from "@/pages/Accounts";
import Reports from "@/pages/Reports";
import Integrations from "@/pages/Integrations";
import { User } from "@shared/schema";
import { ThemeProvider } from "@/contexts/ThemeContext";
import { TenantProvider } from "@/contexts/TenantContext";
Expand All @@ -33,6 +37,10 @@ function Router() {
<Switch>
<Route path="/" component={Properties} />
<Route path="/dashboard" component={Dashboard} />
<Route path="/transactions" component={Transactions} />
<Route path="/accounts" component={Accounts} />
<Route path="/reports" component={Reports} />
<Route path="/integrations" component={Integrations} />
<Route path="/properties/:id" component={PropertyDetail} />
<Route path="/connections" component={Connections} />
<Route path="/admin" component={Admin} />
Expand Down
7 changes: 6 additions & 1 deletion client/src/components/layout/Sidebar.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,8 @@ import { cn } from "@/lib/utils";
import {
Settings, Plug, Building2, Shield,
ChevronDown, ChevronRight, ChevronsUpDown,
Menu, X, Activity, LayoutDashboard
Menu, X, Activity, LayoutDashboard,
ArrowLeftRight, Wallet, BarChart3, Cable
} from "lucide-react";
import { useState, useMemo } from "react";
import { useRole, type UserRole } from "@/contexts/RoleContext";
Expand All @@ -21,6 +22,10 @@ interface NavItem {
const NAV_ITEMS: NavItem[] = [
{ href: "/", label: "Portfolio", icon: Building2, roles: ["cfo", "accountant", "bookkeeper", "user"] },
{ href: "/dashboard", label: "Dashboard", icon: LayoutDashboard, roles: ["cfo", "accountant", "bookkeeper", "user"] },
{ href: "/transactions", label: "Transactions", icon: ArrowLeftRight, roles: ["cfo", "accountant", "bookkeeper"] },
{ href: "/accounts", label: "Accounts", icon: Wallet, roles: ["cfo", "accountant", "bookkeeper"] },
{ href: "/reports", label: "Reports", icon: BarChart3, roles: ["cfo", "accountant"] },
{ href: "/integrations", label: "Integrations", icon: Cable, roles: ["cfo", "accountant"] },
{ href: "/connections", label: "Connections", icon: Plug, roles: ["cfo", "accountant"] },
{ href: "/admin", label: "Admin", icon: Shield, roles: ["cfo"] },
{ href: "/settings", label: "Settings", icon: Settings, roles: ["cfo", "accountant", "bookkeeper", "user"] },
Expand Down
170 changes: 170 additions & 0 deletions client/src/components/property/AddLeaseDialog.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,170 @@
import { useState } from 'react';
import { FileText } from 'lucide-react';
import {
Dialog, DialogContent, DialogHeader, DialogTitle, DialogFooter, DialogDescription,
} from '@/components/ui/dialog';
import { Button } from '@/components/ui/button';
import { Input } from '@/components/ui/input';
import { Label } from '@/components/ui/label';
import {
Select, SelectTrigger, SelectValue, SelectContent, SelectItem,
} from '@/components/ui/select';
import { useToast } from '@/hooks/use-toast';
import { useCreateLease, usePropertyUnits } from '@/hooks/use-property';

interface AddLeaseDialogProps {
propertyId: string;
open: boolean;
onOpenChange: (open: boolean) => void;
}

const initialFormState = {
unitId: '',
tenantName: '',
tenantEmail: '',
tenantPhone: '',
startDate: '',
endDate: '',
monthlyRent: '',
securityDeposit: '',
};

export default function AddLeaseDialog({ propertyId, open, onOpenChange }: AddLeaseDialogProps) {
const [form, setForm] = useState(initialFormState);
const { toast } = useToast();
const createLease = useCreateLease(propertyId);
const { data: units = [] } = usePropertyUnits(propertyId);

function resetForm() {
setForm(initialFormState);
}

function handleOpenChange(nextOpen: boolean) {
if (!nextOpen) resetForm();
onOpenChange(nextOpen);
}

function handleChange(field: keyof typeof form, value: string) {
setForm(prev => ({ ...prev, [field]: value }));
}

function handleSubmit(e: React.FormEvent) {
e.preventDefault();

const payload: Record<string, string | undefined> = {
unitId: form.unitId,
tenantName: form.tenantName.trim(),
startDate: form.startDate,
endDate: form.endDate,
monthlyRent: form.monthlyRent,
};
if (form.tenantEmail.trim()) payload.tenantEmail = form.tenantEmail.trim();
if (form.tenantPhone.trim()) payload.tenantPhone = form.tenantPhone.trim();
if (form.securityDeposit) payload.securityDeposit = form.securityDeposit;

createLease.mutate(payload, {
onSuccess: () => {
toast({ title: 'Lease created', description: `Lease for ${payload.tenantName} has been created.` });
handleOpenChange(false);
},
onError: (error: Error) => {
toast({ title: 'Failed to create lease', description: error.message, variant: 'destructive' });
},
});
}

const isValid =
form.unitId !== '' &&
form.tenantName.trim() !== '' &&
form.startDate !== '' &&
form.endDate !== '' &&
form.monthlyRent !== '';

return (
<Dialog open={open} onOpenChange={handleOpenChange}>
<DialogContent className="sm:max-w-[520px]">
<DialogHeader>
<DialogTitle className="flex items-center gap-2">
<FileText className="h-5 w-5" />
Add Lease
</DialogTitle>
<DialogDescription>Create a new lease for a unit in this property.</DialogDescription>
</DialogHeader>

<form onSubmit={handleSubmit} className="space-y-4">
<div className="space-y-2">
<Label htmlFor="lease-unit">Unit *</Label>
<Select value={form.unitId} onValueChange={v => handleChange('unitId', v)}>
<SelectTrigger id="lease-unit">
<SelectValue placeholder="Select unit" />
</SelectTrigger>
<SelectContent>
{units.map(u => (
<SelectItem key={u.id} value={u.id}>
Unit {u.unitNumber} — {u.bedrooms}br/{u.bathrooms}ba
</SelectItem>
))}
</SelectContent>
</Select>
{units.length === 0 && (
<p className="text-xs text-muted-foreground">No units available. Add a unit first.</p>
)}
</div>

<div className="space-y-2">
<Label htmlFor="lease-tenant-name">Tenant Name *</Label>
<Input id="lease-tenant-name" placeholder="e.g. John Smith" value={form.tenantName}
onChange={e => handleChange('tenantName', e.target.value)} required />
</div>

<div className="grid grid-cols-2 gap-3">
<div className="space-y-2">
<Label htmlFor="lease-tenant-email">Email</Label>
<Input id="lease-tenant-email" type="email" placeholder="tenant@example.com"
value={form.tenantEmail} onChange={e => handleChange('tenantEmail', e.target.value)} />
</div>
<div className="space-y-2">
<Label htmlFor="lease-tenant-phone">Phone</Label>
<Input id="lease-tenant-phone" type="tel" placeholder="(312) 555-0100"
value={form.tenantPhone} onChange={e => handleChange('tenantPhone', e.target.value)} />
</div>
</div>

<div className="grid grid-cols-2 gap-3">
<div className="space-y-2">
<Label htmlFor="lease-start">Start Date *</Label>
<Input id="lease-start" type="date" value={form.startDate}
onChange={e => handleChange('startDate', e.target.value)} required />
</div>
<div className="space-y-2">
<Label htmlFor="lease-end">End Date *</Label>
<Input id="lease-end" type="date" value={form.endDate}
onChange={e => handleChange('endDate', e.target.value)} required />
</div>
</div>

<div className="grid grid-cols-2 gap-3">
<div className="space-y-2">
<Label htmlFor="lease-rent">Monthly Rent *</Label>
<Input id="lease-rent" type="number" min="0" step="0.01" placeholder="0.00"
value={form.monthlyRent} onChange={e => handleChange('monthlyRent', e.target.value)} required />
</div>
<div className="space-y-2">
<Label htmlFor="lease-deposit">Security Deposit</Label>
<Input id="lease-deposit" type="number" min="0" step="0.01" placeholder="0.00"
value={form.securityDeposit} onChange={e => handleChange('securityDeposit', e.target.value)} />
</div>
</div>

<DialogFooter>
<Button type="button" variant="outline" onClick={() => handleOpenChange(false)}
disabled={createLease.isPending}>Cancel</Button>
<Button type="submit" disabled={!isValid || createLease.isPending}>
{createLease.isPending ? 'Creating...' : 'Add Lease'}
</Button>
</DialogFooter>
</form>
</DialogContent>
</Dialog>
);
}
123 changes: 123 additions & 0 deletions client/src/components/property/AddUnitDialog.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,123 @@
import { useState } from 'react';
import { Home } from 'lucide-react';
import {
Dialog, DialogContent, DialogHeader, DialogTitle, DialogFooter, DialogDescription,
} from '@/components/ui/dialog';
import { Button } from '@/components/ui/button';
import { Input } from '@/components/ui/input';
import { Label } from '@/components/ui/label';
import { useToast } from '@/hooks/use-toast';
import { useCreateUnit } from '@/hooks/use-property';

interface AddUnitDialogProps {
propertyId: string;
open: boolean;
onOpenChange: (open: boolean) => void;
}

const initialFormState = {
unitNumber: '',
bedrooms: '',
bathrooms: '',
squareFeet: '',
monthlyRent: '',
};

export default function AddUnitDialog({ propertyId, open, onOpenChange }: AddUnitDialogProps) {
const [form, setForm] = useState(initialFormState);
const { toast } = useToast();
const createUnit = useCreateUnit(propertyId);

function resetForm() {
setForm(initialFormState);
}

function handleOpenChange(nextOpen: boolean) {
if (!nextOpen) resetForm();
onOpenChange(nextOpen);
}

function handleChange(field: keyof typeof form, value: string) {
setForm(prev => ({ ...prev, [field]: value }));
}

function handleSubmit(e: React.FormEvent) {
e.preventDefault();

const payload: Record<string, string | number | undefined> = {
unitNumber: form.unitNumber.trim(),
};
if (form.bedrooms) payload.bedrooms = parseInt(form.bedrooms, 10);
if (form.bathrooms) payload.bathrooms = form.bathrooms;
if (form.squareFeet) payload.squareFeet = parseInt(form.squareFeet, 10);
if (form.monthlyRent) payload.monthlyRent = form.monthlyRent;

createUnit.mutate(payload, {
onSuccess: () => {
toast({ title: 'Unit added', description: `Unit ${payload.unitNumber} has been created.` });
handleOpenChange(false);
},
onError: (error: Error) => {
toast({ title: 'Failed to add unit', description: error.message, variant: 'destructive' });
},
});
}

const isValid = form.unitNumber.trim() !== '';

return (
<Dialog open={open} onOpenChange={handleOpenChange}>
<DialogContent className="sm:max-w-[440px]">
<DialogHeader>
<DialogTitle className="flex items-center gap-2">
<Home className="h-5 w-5" />
Add Unit
</DialogTitle>
<DialogDescription>Add a new rental unit to this property.</DialogDescription>
</DialogHeader>

<form onSubmit={handleSubmit} className="space-y-4">
<div className="space-y-2">
<Label htmlFor="unit-number">Unit Number *</Label>
<Input id="unit-number" placeholder="e.g. 1A, 201, Studio" value={form.unitNumber}
onChange={e => handleChange('unitNumber', e.target.value)} required />
</div>

<div className="grid grid-cols-2 gap-3">
<div className="space-y-2">
<Label htmlFor="unit-beds">Bedrooms</Label>
<Input id="unit-beds" type="number" min="0" placeholder="0" value={form.bedrooms}
onChange={e => handleChange('bedrooms', e.target.value)} />
</div>
<div className="space-y-2">
<Label htmlFor="unit-baths">Bathrooms</Label>
<Input id="unit-baths" type="number" min="0" step="0.5" placeholder="0" value={form.bathrooms}
onChange={e => handleChange('bathrooms', e.target.value)} />
</div>
</div>

<div className="grid grid-cols-2 gap-3">
<div className="space-y-2">
<Label htmlFor="unit-sqft">Square Feet</Label>
<Input id="unit-sqft" type="number" min="0" placeholder="0" value={form.squareFeet}
onChange={e => handleChange('squareFeet', e.target.value)} />
</div>
<div className="space-y-2">
<Label htmlFor="unit-rent">Monthly Rent</Label>
<Input id="unit-rent" type="number" min="0" step="0.01" placeholder="0.00" value={form.monthlyRent}
onChange={e => handleChange('monthlyRent', e.target.value)} />
</div>
</div>

<DialogFooter>
<Button type="button" variant="outline" onClick={() => handleOpenChange(false)}
disabled={createUnit.isPending}>Cancel</Button>
<Button type="submit" disabled={!isValid || createUnit.isPending}>
{createUnit.isPending ? 'Adding...' : 'Add Unit'}
</Button>
</DialogFooter>
</form>
</DialogContent>
</Dialog>
);
}
Loading
Loading