Fix admin panel edit forms and expand API responses

- Fix dropdown population on all edit pages (residence, task, contractor, document)
- Add formInitialized state pattern to prevent empty dropdowns
- Increase pagination max limit from 100 to 10000 for admin queries
- Expand handler responses to include all editable fields
- Add settings page with seed data and limitations toggle
- Fix user form and API client types

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
Trey t
2025-11-28 21:00:42 -06:00
parent 5e95dcd015
commit 99465a590d
18 changed files with 2028 additions and 241 deletions
@@ -6,13 +6,20 @@ import { useMutation, useQuery } from '@tanstack/react-query';
import { ArrowLeft } from 'lucide-react';
import Link from 'next/link';
import { contractorsApi } from '@/lib/api';
import { contractorsApi, usersApi, residencesApi, lookupsApi } from '@/lib/api';
import type { UpdateContractorRequest } from '@/types/models';
import { Button } from '@/components/ui/button';
import { Input } from '@/components/ui/input';
import { Label } from '@/components/ui/label';
import { Textarea } from '@/components/ui/textarea';
import { Switch } from '@/components/ui/switch';
import {
Select,
SelectContent,
SelectItem,
SelectTrigger,
SelectValue,
} from '@/components/ui/select';
import {
Card,
CardContent,
@@ -27,36 +34,56 @@ export default function EditContractorPage() {
const params = useParams();
const contractorId = Number(params.id);
const [formData, setFormData] = useState<UpdateContractorRequest>({
name: '',
company: '',
phone: '',
email: '',
website: '',
notes: '',
is_favorite: false,
is_active: true,
});
const [formData, setFormData] = useState<UpdateContractorRequest>({});
const { data: contractor, isLoading } = useQuery({
const { data: contractor, isLoading: contractorLoading } = useQuery({
queryKey: ['contractor', contractorId],
queryFn: () => contractorsApi.get(contractorId),
enabled: !!contractorId,
});
const { data: usersData, isLoading: usersLoading } = useQuery({
queryKey: ['users', { per_page: 1000 }],
queryFn: () => usersApi.list({ per_page: 1000 }),
});
const { data: residencesData, isLoading: residencesLoading } = useQuery({
queryKey: ['residences', { per_page: 1000 }],
queryFn: () => residencesApi.list({ per_page: 1000 }),
});
const { data: specialties } = useQuery({
queryKey: ['lookups', 'specialties'],
queryFn: () => lookupsApi.specialties.list(),
});
const [formInitialized, setFormInitialized] = useState(false);
useEffect(() => {
if (contractor) {
if (contractor && !formInitialized) {
setFormData({
residence_id: contractor.residence_id,
created_by_id: contractor.created_by_id,
name: contractor.name,
company: contractor.company,
phone: contractor.phone,
email: contractor.email,
website: contractor.website,
notes: contractor.notes,
street_address: contractor.street_address,
city: contractor.city,
state_province: contractor.state_province,
postal_code: contractor.postal_code,
rating: contractor.rating,
is_favorite: contractor.is_favorite,
is_active: contractor.is_active,
specialty_ids: contractor.specialty_ids,
});
setFormInitialized(true);
}
}, [contractor]);
}, [contractor, formInitialized]);
const isDataLoading = contractorLoading || usersLoading || residencesLoading || !formInitialized;
const updateMutation = useMutation({
mutationFn: (data: UpdateContractorRequest) => contractorsApi.update(contractorId, data),
@@ -82,7 +109,7 @@ export default function EditContractorPage() {
setFormData((prev) => ({ ...prev, [field]: value }));
};
if (isLoading) {
if (isDataLoading) {
return (
<div className="flex items-center justify-center h-64">
<div className="text-muted-foreground">Loading...</div>
@@ -105,10 +132,57 @@ export default function EditContractorPage() {
</div>
<form onSubmit={handleSubmit} className="space-y-6">
<Card>
<CardHeader>
<CardTitle>Assignment</CardTitle>
<CardDescription>Contractor ownership</CardDescription>
</CardHeader>
<CardContent className="space-y-4">
<div className="grid grid-cols-2 gap-4">
<div className="space-y-2">
<Label htmlFor="residence_id">Residence *</Label>
<Select
value={formData.residence_id?.toString() || ''}
onValueChange={(value) => updateField('residence_id', Number(value))}
>
<SelectTrigger>
<SelectValue placeholder="Select residence" />
</SelectTrigger>
<SelectContent>
{residencesData?.data?.map((res) => (
<SelectItem key={res.id} value={res.id.toString()}>
{res.name}
</SelectItem>
))}
</SelectContent>
</Select>
</div>
<div className="space-y-2">
<Label htmlFor="created_by_id">Created By *</Label>
<Select
value={formData.created_by_id?.toString() || ''}
onValueChange={(value) => updateField('created_by_id', Number(value))}
>
<SelectTrigger>
<SelectValue placeholder="Select creator" />
</SelectTrigger>
<SelectContent>
{usersData?.data?.map((user) => (
<SelectItem key={user.id} value={user.id.toString()}>
{user.username} ({user.email})
</SelectItem>
))}
</SelectContent>
</Select>
</div>
</div>
</CardContent>
</Card>
<Card>
<CardHeader>
<CardTitle>Basic Information</CardTitle>
<CardDescription>Update the contractor details</CardDescription>
<CardDescription>Contractor contact details</CardDescription>
</CardHeader>
<CardContent className="space-y-4">
<div className="grid grid-cols-2 gap-4">
@@ -118,7 +192,7 @@ export default function EditContractorPage() {
id="name"
value={formData.name || ''}
onChange={(e) => updateField('name', e.target.value)}
placeholder="John Doe"
placeholder="Contractor name"
/>
</div>
<div className="space-y-2">
@@ -127,37 +201,10 @@ export default function EditContractorPage() {
id="company"
value={formData.company || ''}
onChange={(e) => updateField('company', e.target.value)}
placeholder="ABC Plumbing"
placeholder="Company name"
/>
</div>
</div>
<div className="flex items-center gap-6">
<div className="flex items-center space-x-2">
<Switch
id="is_favorite"
checked={formData.is_favorite}
onCheckedChange={(checked) => updateField('is_favorite', checked)}
/>
<Label htmlFor="is_favorite">Favorite</Label>
</div>
<div className="flex items-center space-x-2">
<Switch
id="is_active"
checked={formData.is_active}
onCheckedChange={(checked) => updateField('is_active', checked)}
/>
<Label htmlFor="is_active">Active</Label>
</div>
</div>
</CardContent>
</Card>
<Card>
<CardHeader>
<CardTitle>Contact Information</CardTitle>
<CardDescription>Update contact details</CardDescription>
</CardHeader>
<CardContent className="space-y-4">
<div className="grid grid-cols-2 gap-4">
<div className="space-y-2">
<Label htmlFor="phone">Phone</Label>
@@ -165,7 +212,7 @@ export default function EditContractorPage() {
id="phone"
value={formData.phone || ''}
onChange={(e) => updateField('phone', e.target.value)}
placeholder="(555) 123-4567"
placeholder="555-123-4567"
/>
</div>
<div className="space-y-2">
@@ -188,21 +235,136 @@ export default function EditContractorPage() {
placeholder="https://example.com"
/>
</div>
<div className="space-y-2">
<Label htmlFor="notes">Notes</Label>
<Textarea
id="notes"
value={formData.notes || ''}
onChange={(e) => updateField('notes', e.target.value)}
placeholder="Additional notes..."
rows={3}
/>
</div>
</CardContent>
</Card>
<Card>
<CardHeader>
<CardTitle>Notes</CardTitle>
<CardDescription>Additional information</CardDescription>
<CardTitle>Address</CardTitle>
<CardDescription>Contractor location</CardDescription>
</CardHeader>
<CardContent>
<Textarea
id="notes"
value={formData.notes || ''}
onChange={(e) => updateField('notes', e.target.value)}
placeholder="Any additional notes about this contractor..."
/>
<CardContent className="space-y-4">
<div className="space-y-2">
<Label htmlFor="street_address">Street Address</Label>
<Input
id="street_address"
value={formData.street_address || ''}
onChange={(e) => updateField('street_address', e.target.value)}
placeholder="123 Main St"
/>
</div>
<div className="grid grid-cols-3 gap-4">
<div className="space-y-2">
<Label htmlFor="city">City</Label>
<Input
id="city"
value={formData.city || ''}
onChange={(e) => updateField('city', e.target.value)}
placeholder="City"
/>
</div>
<div className="space-y-2">
<Label htmlFor="state_province">State/Province</Label>
<Input
id="state_province"
value={formData.state_province || ''}
onChange={(e) => updateField('state_province', e.target.value)}
placeholder="State"
/>
</div>
<div className="space-y-2">
<Label htmlFor="postal_code">Postal Code</Label>
<Input
id="postal_code"
value={formData.postal_code || ''}
onChange={(e) => updateField('postal_code', e.target.value)}
placeholder="12345"
/>
</div>
</div>
</CardContent>
</Card>
<Card>
<CardHeader>
<CardTitle>Rating & Specialties</CardTitle>
<CardDescription>Contractor expertise</CardDescription>
</CardHeader>
<CardContent className="space-y-4">
<div className="space-y-2">
<Label htmlFor="rating">Rating (0-5)</Label>
<Input
id="rating"
type="number"
min="0"
max="5"
step="0.1"
value={formData.rating ?? ''}
onChange={(e) => updateField('rating', e.target.value ? Number(e.target.value) : undefined)}
placeholder="4.5"
className="w-32"
/>
</div>
<div className="space-y-2">
<Label>Specialties</Label>
<div className="grid grid-cols-3 gap-2">
{specialties?.map((spec: { id: number; name: string }) => (
<label key={spec.id} className="flex items-center space-x-2 cursor-pointer">
<input
type="checkbox"
checked={formData.specialty_ids?.includes(spec.id) || false}
onChange={(e) => {
const current = formData.specialty_ids || [];
if (e.target.checked) {
updateField('specialty_ids', [...current, spec.id]);
} else {
updateField('specialty_ids', current.filter(id => id !== spec.id));
}
}}
className="rounded border-gray-300"
/>
<span className="text-sm">{spec.name}</span>
</label>
))}
</div>
</div>
</CardContent>
</Card>
<Card>
<CardHeader>
<CardTitle>Status</CardTitle>
<CardDescription>Contractor status flags</CardDescription>
</CardHeader>
<CardContent className="space-y-4">
<div className="flex items-center gap-6">
<div className="flex items-center space-x-2">
<Switch
id="is_favorite"
checked={formData.is_favorite}
onCheckedChange={(checked) => updateField('is_favorite', checked)}
/>
<Label htmlFor="is_favorite">Favorite</Label>
</div>
<div className="flex items-center space-x-2">
<Switch
id="is_active"
checked={formData.is_active}
onCheckedChange={(checked) => updateField('is_active', checked)}
/>
<Label htmlFor="is_active">Active</Label>
</div>
</div>
</CardContent>
</Card>
@@ -6,13 +6,20 @@ import { useMutation, useQuery } from '@tanstack/react-query';
import { ArrowLeft } from 'lucide-react';
import Link from 'next/link';
import { documentsApi } from '@/lib/api';
import { documentsApi, usersApi, residencesApi, tasksApi } from '@/lib/api';
import type { UpdateDocumentRequest } from '@/types/models';
import { Button } from '@/components/ui/button';
import { Input } from '@/components/ui/input';
import { Label } from '@/components/ui/label';
import { Textarea } from '@/components/ui/textarea';
import { Switch } from '@/components/ui/switch';
import {
Select,
SelectContent,
SelectItem,
SelectTrigger,
SelectValue,
} from '@/components/ui/select';
import {
Card,
CardContent,
@@ -22,36 +29,77 @@ import {
} from '@/components/ui/card';
import { toast } from 'sonner';
const DOCUMENT_TYPES = [
{ value: 'general', label: 'General' },
{ value: 'warranty', label: 'Warranty' },
{ value: 'receipt', label: 'Receipt' },
{ value: 'contract', label: 'Contract' },
{ value: 'insurance', label: 'Insurance' },
{ value: 'manual', label: 'Manual' },
];
export default function EditDocumentPage() {
const router = useRouter();
const params = useParams();
const documentId = Number(params.id);
const [formData, setFormData] = useState<UpdateDocumentRequest>({
title: '',
description: '',
vendor: '',
serial_number: '',
model_number: '',
is_active: true,
});
const [formData, setFormData] = useState<UpdateDocumentRequest>({});
const { data: document, isLoading } = useQuery({
const { data: document, isLoading: documentLoading } = useQuery({
queryKey: ['document', documentId],
queryFn: () => documentsApi.get(documentId),
enabled: !!documentId,
});
const { data: usersData, isLoading: usersLoading } = useQuery({
queryKey: ['users', { per_page: 1000 }],
queryFn: () => usersApi.list({ per_page: 1000 }),
});
const { data: residencesData, isLoading: residencesLoading } = useQuery({
queryKey: ['residences', { per_page: 1000 }],
queryFn: () => residencesApi.list({ per_page: 1000 }),
});
const { data: tasksData } = useQuery({
queryKey: ['tasks', { per_page: 1000 }],
queryFn: () => tasksApi.list({ per_page: 1000 }),
});
const [formInitialized, setFormInitialized] = useState(false);
useEffect(() => {
if (document) {
if (document && !formInitialized) {
setFormData({
residence_id: document.residence_id,
created_by_id: document.created_by_id,
title: document.title,
description: document.description,
document_type: document.document_type,
file_url: document.file_url,
file_name: document.file_name,
file_size: document.file_size,
mime_type: document.mime_type,
purchase_date: document.purchase_date,
expiry_date: document.expiry_date,
purchase_price: document.purchase_price,
vendor: document.vendor,
serial_number: document.serial_number,
model_number: document.model_number,
provider: document.provider,
provider_contact: document.provider_contact,
claim_phone: document.claim_phone,
claim_email: document.claim_email,
claim_website: document.claim_website,
notes: document.notes,
task_id: document.task_id,
is_active: document.is_active,
});
setFormInitialized(true);
}
}, [document]);
}, [document, formInitialized]);
const isDataLoading = documentLoading || usersLoading || residencesLoading || !formInitialized;
const updateMutation = useMutation({
mutationFn: (data: UpdateDocumentRequest) => documentsApi.update(documentId, data),
@@ -77,7 +125,7 @@ export default function EditDocumentPage() {
setFormData((prev) => ({ ...prev, [field]: value }));
};
if (isLoading) {
if (isDataLoading) {
return (
<div className="flex items-center justify-center h-64">
<div className="text-muted-foreground">Loading...</div>
@@ -102,18 +150,104 @@ export default function EditDocumentPage() {
<form onSubmit={handleSubmit} className="space-y-6">
<Card>
<CardHeader>
<CardTitle>Basic Information</CardTitle>
<CardDescription>Update the document details</CardDescription>
<CardTitle>Assignment</CardTitle>
<CardDescription>Document ownership</CardDescription>
</CardHeader>
<CardContent className="space-y-4">
<div className="grid grid-cols-2 gap-4">
<div className="space-y-2">
<Label htmlFor="residence_id">Residence *</Label>
<Select
value={formData.residence_id?.toString() || ''}
onValueChange={(value) => updateField('residence_id', Number(value))}
>
<SelectTrigger>
<SelectValue placeholder="Select residence" />
</SelectTrigger>
<SelectContent>
{residencesData?.data?.map((res) => (
<SelectItem key={res.id} value={res.id.toString()}>
{res.name}
</SelectItem>
))}
</SelectContent>
</Select>
</div>
<div className="space-y-2">
<Label htmlFor="created_by_id">Created By *</Label>
<Select
value={formData.created_by_id?.toString() || ''}
onValueChange={(value) => updateField('created_by_id', Number(value))}
>
<SelectTrigger>
<SelectValue placeholder="Select creator" />
</SelectTrigger>
<SelectContent>
{usersData?.data?.map((user) => (
<SelectItem key={user.id} value={user.id.toString()}>
{user.username} ({user.email})
</SelectItem>
))}
</SelectContent>
</Select>
</div>
</div>
<div className="space-y-2">
<Label htmlFor="title">Title *</Label>
<Input
id="title"
value={formData.title || ''}
onChange={(e) => updateField('title', e.target.value)}
placeholder="Document title"
/>
<Label htmlFor="task_id">Related Task</Label>
<Select
value={formData.task_id?.toString() || 'none'}
onValueChange={(value) => updateField('task_id', value === 'none' ? undefined : Number(value))}
>
<SelectTrigger>
<SelectValue placeholder="Select task" />
</SelectTrigger>
<SelectContent>
<SelectItem value="none">None</SelectItem>
{tasksData?.data?.map((task) => (
<SelectItem key={task.id} value={task.id.toString()}>
{task.title} (#{task.id})
</SelectItem>
))}
</SelectContent>
</Select>
</div>
</CardContent>
</Card>
<Card>
<CardHeader>
<CardTitle>Basic Information</CardTitle>
<CardDescription>Document details</CardDescription>
</CardHeader>
<CardContent className="space-y-4">
<div className="grid grid-cols-2 gap-4">
<div className="space-y-2">
<Label htmlFor="title">Title *</Label>
<Input
id="title"
value={formData.title || ''}
onChange={(e) => updateField('title', e.target.value)}
placeholder="Document title"
/>
</div>
<div className="space-y-2">
<Label htmlFor="document_type">Document Type</Label>
<Select
value={formData.document_type || ''}
onValueChange={(value) => updateField('document_type', value)}
>
<SelectTrigger>
<SelectValue placeholder="Select type" />
</SelectTrigger>
<SelectContent>
{DOCUMENT_TYPES.map((type) => (
<SelectItem key={type.value} value={type.value}>
{type.label}
</SelectItem>
))}
</SelectContent>
</Select>
</div>
</div>
<div className="space-y-2">
<Label htmlFor="description">Description</Label>
@@ -122,6 +256,17 @@ export default function EditDocumentPage() {
value={formData.description || ''}
onChange={(e) => updateField('description', e.target.value)}
placeholder="Document description..."
rows={3}
/>
</div>
<div className="space-y-2">
<Label htmlFor="notes">Notes</Label>
<Textarea
id="notes"
value={formData.notes || ''}
onChange={(e) => updateField('notes', e.target.value)}
placeholder="Additional notes..."
rows={2}
/>
</div>
<div className="flex items-center space-x-2">
@@ -135,6 +280,96 @@ export default function EditDocumentPage() {
</CardContent>
</Card>
<Card>
<CardHeader>
<CardTitle>File Information</CardTitle>
<CardDescription>File details</CardDescription>
</CardHeader>
<CardContent className="space-y-4">
<div className="grid grid-cols-2 gap-4">
<div className="space-y-2">
<Label htmlFor="file_url">File URL</Label>
<Input
id="file_url"
value={formData.file_url || ''}
onChange={(e) => updateField('file_url', e.target.value)}
placeholder="https://..."
/>
</div>
<div className="space-y-2">
<Label htmlFor="file_name">File Name</Label>
<Input
id="file_name"
value={formData.file_name || ''}
onChange={(e) => updateField('file_name', e.target.value)}
placeholder="document.pdf"
/>
</div>
</div>
<div className="grid grid-cols-2 gap-4">
<div className="space-y-2">
<Label htmlFor="file_size">File Size (bytes)</Label>
<Input
id="file_size"
type="number"
value={formData.file_size ?? ''}
onChange={(e) => updateField('file_size', e.target.value ? Number(e.target.value) : undefined)}
placeholder="0"
/>
</div>
<div className="space-y-2">
<Label htmlFor="mime_type">MIME Type</Label>
<Input
id="mime_type"
value={formData.mime_type || ''}
onChange={(e) => updateField('mime_type', e.target.value)}
placeholder="application/pdf"
/>
</div>
</div>
</CardContent>
</Card>
<Card>
<CardHeader>
<CardTitle>Purchase Information</CardTitle>
<CardDescription>Purchase and warranty details</CardDescription>
</CardHeader>
<CardContent className="space-y-4">
<div className="grid grid-cols-3 gap-4">
<div className="space-y-2">
<Label htmlFor="purchase_date">Purchase Date</Label>
<Input
id="purchase_date"
type="date"
value={formData.purchase_date || ''}
onChange={(e) => updateField('purchase_date', e.target.value || undefined)}
/>
</div>
<div className="space-y-2">
<Label htmlFor="expiry_date">Expiry Date</Label>
<Input
id="expiry_date"
type="date"
value={formData.expiry_date || ''}
onChange={(e) => updateField('expiry_date', e.target.value || undefined)}
/>
</div>
<div className="space-y-2">
<Label htmlFor="purchase_price">Purchase Price ($)</Label>
<Input
id="purchase_price"
type="number"
step="0.01"
value={formData.purchase_price ?? ''}
onChange={(e) => updateField('purchase_price', e.target.value ? Number(e.target.value) : undefined)}
placeholder="0.00"
/>
</div>
</div>
</CardContent>
</Card>
<Card>
<CardHeader>
<CardTitle>Product Details</CardTitle>
@@ -173,6 +408,65 @@ export default function EditDocumentPage() {
</CardContent>
</Card>
<Card>
<CardHeader>
<CardTitle>Provider/Warranty Contact</CardTitle>
<CardDescription>Contact information for claims or support</CardDescription>
</CardHeader>
<CardContent className="space-y-4">
<div className="grid grid-cols-2 gap-4">
<div className="space-y-2">
<Label htmlFor="provider">Provider</Label>
<Input
id="provider"
value={formData.provider || ''}
onChange={(e) => updateField('provider', e.target.value)}
placeholder="Provider name"
/>
</div>
<div className="space-y-2">
<Label htmlFor="provider_contact">Provider Contact</Label>
<Input
id="provider_contact"
value={formData.provider_contact || ''}
onChange={(e) => updateField('provider_contact', e.target.value)}
placeholder="Contact name"
/>
</div>
</div>
<div className="grid grid-cols-3 gap-4">
<div className="space-y-2">
<Label htmlFor="claim_phone">Claim Phone</Label>
<Input
id="claim_phone"
value={formData.claim_phone || ''}
onChange={(e) => updateField('claim_phone', e.target.value)}
placeholder="1-800-555-0123"
/>
</div>
<div className="space-y-2">
<Label htmlFor="claim_email">Claim Email</Label>
<Input
id="claim_email"
type="email"
value={formData.claim_email || ''}
onChange={(e) => updateField('claim_email', e.target.value)}
placeholder="claims@example.com"
/>
</div>
<div className="space-y-2">
<Label htmlFor="claim_website">Claim Website</Label>
<Input
id="claim_website"
value={formData.claim_website || ''}
onChange={(e) => updateField('claim_website', e.target.value)}
placeholder="https://claims.example.com"
/>
</div>
</div>
</CardContent>
</Card>
<div className="flex justify-end gap-4">
<Button type="button" variant="outline" asChild>
<Link href={`/documents/${documentId}`}>Cancel</Link>
@@ -6,12 +6,20 @@ import { useMutation, useQuery } from '@tanstack/react-query';
import { ArrowLeft } from 'lucide-react';
import Link from 'next/link';
import { residencesApi } from '@/lib/api';
import { residencesApi, usersApi, lookupsApi } from '@/lib/api';
import type { UpdateResidenceRequest } from '@/types/models';
import { Button } from '@/components/ui/button';
import { Input } from '@/components/ui/input';
import { Label } from '@/components/ui/label';
import { Switch } from '@/components/ui/switch';
import { Textarea } from '@/components/ui/textarea';
import {
Select,
SelectContent,
SelectItem,
SelectTrigger,
SelectValue,
} from '@/components/ui/select';
import {
Card,
CardContent,
@@ -26,37 +34,54 @@ export default function EditResidencePage() {
const params = useParams();
const residenceId = Number(params.id);
const [formData, setFormData] = useState<UpdateResidenceRequest>({
name: '',
street_address: '',
city: '',
state_province: '',
postal_code: '',
country: '',
is_primary: false,
is_active: true,
});
const [formData, setFormData] = useState<UpdateResidenceRequest>({});
const { data: residence, isLoading } = useQuery({
const { data: residence, isLoading: residenceLoading } = useQuery({
queryKey: ['residence', residenceId],
queryFn: () => residencesApi.get(residenceId),
enabled: !!residenceId,
});
const { data: usersData, isLoading: usersLoading, error: usersError } = useQuery({
queryKey: ['users', { per_page: 1000 }],
queryFn: () => usersApi.list({ per_page: 1000 }),
});
const { data: residenceTypes } = useQuery({
queryKey: ['lookups', 'residence-types'],
queryFn: () => lookupsApi.residenceTypes.list(),
});
const [formInitialized, setFormInitialized] = useState(false);
useEffect(() => {
if (residence) {
if (residence && !formInitialized) {
setFormData({
owner_id: residence.owner_id,
name: residence.name,
property_type_id: residence.property_type_id,
street_address: residence.street_address,
apartment_unit: residence.apartment_unit,
city: residence.city,
state_province: residence.state_province,
postal_code: residence.postal_code,
country: residence.country,
bedrooms: residence.bedrooms,
bathrooms: residence.bathrooms,
square_footage: residence.square_footage,
lot_size: residence.lot_size,
year_built: residence.year_built,
description: residence.description,
purchase_date: residence.purchase_date,
purchase_price: residence.purchase_price,
is_primary: residence.is_primary,
is_active: residence.is_active,
});
setFormInitialized(true);
}
}, [residence]);
}, [residence, formInitialized]);
const isDataLoading = residenceLoading || usersLoading || !formInitialized;
const updateMutation = useMutation({
mutationFn: (data: UpdateResidenceRequest) => residencesApi.update(residenceId, data),
@@ -82,7 +107,7 @@ export default function EditResidencePage() {
setFormData((prev) => ({ ...prev, [field]: value }));
};
if (isLoading) {
if (isDataLoading) {
return (
<div className="flex items-center justify-center h-64">
<div className="text-muted-foreground">Loading...</div>
@@ -105,19 +130,76 @@ export default function EditResidencePage() {
</div>
<form onSubmit={handleSubmit} className="space-y-6">
<Card>
<CardHeader>
<CardTitle>Ownership</CardTitle>
<CardDescription>Property ownership details</CardDescription>
</CardHeader>
<CardContent className="space-y-4">
<div className="space-y-2">
<Label htmlFor="owner_id">Owner *</Label>
<Select
value={formData.owner_id?.toString() || ''}
onValueChange={(value) => updateField('owner_id', Number(value))}
>
<SelectTrigger>
<SelectValue placeholder="Select owner" />
</SelectTrigger>
<SelectContent>
{usersData?.data?.map((user) => (
<SelectItem key={user.id} value={user.id.toString()}>
{user.username} ({user.email})
</SelectItem>
))}
</SelectContent>
</Select>
</div>
</CardContent>
</Card>
<Card>
<CardHeader>
<CardTitle>Basic Information</CardTitle>
<CardDescription>Update the property details</CardDescription>
</CardHeader>
<CardContent className="space-y-4">
<div className="grid grid-cols-2 gap-4">
<div className="space-y-2">
<Label htmlFor="name">Name *</Label>
<Input
id="name"
value={formData.name || ''}
onChange={(e) => updateField('name', e.target.value)}
placeholder="My Home"
/>
</div>
<div className="space-y-2">
<Label htmlFor="property_type_id">Property Type</Label>
<Select
value={formData.property_type_id?.toString() || ''}
onValueChange={(value) => updateField('property_type_id', value ? Number(value) : undefined)}
>
<SelectTrigger>
<SelectValue placeholder="Select type" />
</SelectTrigger>
<SelectContent>
{residenceTypes?.map((type: { id: number; name: string }) => (
<SelectItem key={type.id} value={type.id.toString()}>
{type.name}
</SelectItem>
))}
</SelectContent>
</Select>
</div>
</div>
<div className="space-y-2">
<Label htmlFor="name">Name *</Label>
<Input
id="name"
value={formData.name || ''}
onChange={(e) => updateField('name', e.target.value)}
placeholder="My Home"
<Label htmlFor="description">Description</Label>
<Textarea
id="description"
value={formData.description || ''}
onChange={(e) => updateField('description', e.target.value)}
placeholder="Property description..."
rows={3}
/>
</div>
<div className="flex items-center gap-6">
@@ -147,14 +229,25 @@ export default function EditResidencePage() {
<CardDescription>Property location details</CardDescription>
</CardHeader>
<CardContent className="space-y-4">
<div className="space-y-2">
<Label htmlFor="street_address">Street Address</Label>
<Input
id="street_address"
value={formData.street_address || ''}
onChange={(e) => updateField('street_address', e.target.value)}
placeholder="123 Main St"
/>
<div className="grid grid-cols-2 gap-4">
<div className="space-y-2">
<Label htmlFor="street_address">Street Address</Label>
<Input
id="street_address"
value={formData.street_address || ''}
onChange={(e) => updateField('street_address', e.target.value)}
placeholder="123 Main St"
/>
</div>
<div className="space-y-2">
<Label htmlFor="apartment_unit">Apartment/Unit</Label>
<Input
id="apartment_unit"
value={formData.apartment_unit || ''}
onChange={(e) => updateField('apartment_unit', e.target.value)}
placeholder="Apt 4B"
/>
</div>
</div>
<div className="grid grid-cols-2 gap-4">
<div className="space-y-2">
@@ -199,6 +292,101 @@ export default function EditResidencePage() {
</CardContent>
</Card>
<Card>
<CardHeader>
<CardTitle>Property Details</CardTitle>
<CardDescription>Physical property characteristics</CardDescription>
</CardHeader>
<CardContent className="space-y-4">
<div className="grid grid-cols-4 gap-4">
<div className="space-y-2">
<Label htmlFor="bedrooms">Bedrooms</Label>
<Input
id="bedrooms"
type="number"
value={formData.bedrooms ?? ''}
onChange={(e) => updateField('bedrooms', e.target.value ? Number(e.target.value) : undefined)}
placeholder="3"
/>
</div>
<div className="space-y-2">
<Label htmlFor="bathrooms">Bathrooms</Label>
<Input
id="bathrooms"
type="number"
step="0.5"
value={formData.bathrooms ?? ''}
onChange={(e) => updateField('bathrooms', e.target.value ? Number(e.target.value) : undefined)}
placeholder="2.5"
/>
</div>
<div className="space-y-2">
<Label htmlFor="square_footage">Square Footage</Label>
<Input
id="square_footage"
type="number"
value={formData.square_footage ?? ''}
onChange={(e) => updateField('square_footage', e.target.value ? Number(e.target.value) : undefined)}
placeholder="2000"
/>
</div>
<div className="space-y-2">
<Label htmlFor="lot_size">Lot Size (acres)</Label>
<Input
id="lot_size"
type="number"
step="0.01"
value={formData.lot_size ?? ''}
onChange={(e) => updateField('lot_size', e.target.value ? Number(e.target.value) : undefined)}
placeholder="0.25"
/>
</div>
</div>
<div className="space-y-2">
<Label htmlFor="year_built">Year Built</Label>
<Input
id="year_built"
type="number"
value={formData.year_built ?? ''}
onChange={(e) => updateField('year_built', e.target.value ? Number(e.target.value) : undefined)}
placeholder="1990"
className="w-32"
/>
</div>
</CardContent>
</Card>
<Card>
<CardHeader>
<CardTitle>Purchase Information</CardTitle>
<CardDescription>Property purchase details</CardDescription>
</CardHeader>
<CardContent className="space-y-4">
<div className="grid grid-cols-2 gap-4">
<div className="space-y-2">
<Label htmlFor="purchase_date">Purchase Date</Label>
<Input
id="purchase_date"
type="date"
value={formData.purchase_date || ''}
onChange={(e) => updateField('purchase_date', e.target.value)}
/>
</div>
<div className="space-y-2">
<Label htmlFor="purchase_price">Purchase Price ($)</Label>
<Input
id="purchase_price"
type="number"
step="0.01"
value={formData.purchase_price ?? ''}
onChange={(e) => updateField('purchase_price', e.target.value ? Number(e.target.value) : undefined)}
placeholder="250000"
/>
</div>
</div>
</CardContent>
</Card>
<div className="flex justify-end gap-4">
<Button type="button" variant="outline" asChild>
<Link href={`/residences/${residenceId}`}>Cancel</Link>
+66 -1
View File
@@ -1,7 +1,7 @@
'use client';
import { useMutation } from '@tanstack/react-query';
import { Database, TestTube } from 'lucide-react';
import { Database, TestTube, Trash2 } from 'lucide-react';
import { settingsApi } from '@/lib/api';
import { Button } from '@/components/ui/button';
@@ -46,6 +46,16 @@ export default function SettingsPage() {
},
});
const clearAllDataMutation = useMutation({
mutationFn: settingsApi.clearAllData,
onSuccess: (data) => {
toast.success(`${data.message} (${data.users_deleted} users deleted, ${data.preserved_users} superadmin accounts preserved)`);
},
onError: () => {
toast.error('Failed to clear data');
},
});
return (
<div className="p-6 space-y-6">
<div>
@@ -151,6 +161,61 @@ export default function SettingsPage() {
</AlertDialog>
</CardContent>
</Card>
{/* Clear All Data */}
<Card className="border-destructive">
<CardHeader>
<CardTitle className="flex items-center gap-2 text-destructive">
<Trash2 className="h-5 w-5" />
Clear All Data
</CardTitle>
<CardDescription>
Delete all data from the database except superadmin accounts and lookup tables
</CardDescription>
</CardHeader>
<CardContent>
<AlertDialog>
<AlertDialogTrigger asChild>
<Button
variant="destructive"
disabled={clearAllDataMutation.isPending}
>
{clearAllDataMutation.isPending ? 'Clearing...' : 'Clear All Data'}
</Button>
</AlertDialogTrigger>
<AlertDialogContent>
<AlertDialogHeader>
<AlertDialogTitle>Clear All Data?</AlertDialogTitle>
<AlertDialogDescription>
This will permanently delete:
<ul className="list-disc list-inside mt-2 space-y-1">
<li>All regular users (non-superadmin)</li>
<li>All residences</li>
<li>All tasks and completions</li>
<li>All contractors and documents</li>
<li>All notifications and devices</li>
</ul>
<strong className="text-destructive block mt-2">
Superadmin accounts and lookup tables will be preserved.
</strong>
<strong className="text-destructive block mt-2">
THIS ACTION CANNOT BE UNDONE!
</strong>
</AlertDialogDescription>
</AlertDialogHeader>
<AlertDialogFooter>
<AlertDialogCancel>Cancel</AlertDialogCancel>
<AlertDialogAction
onClick={() => clearAllDataMutation.mutate()}
className="bg-destructive text-destructive-foreground hover:bg-destructive/90"
>
Yes, Clear All Data
</AlertDialogAction>
</AlertDialogFooter>
</AlertDialogContent>
</AlertDialog>
</CardContent>
</Card>
</div>
</div>
);
@@ -6,13 +6,20 @@ import { useMutation, useQuery } from '@tanstack/react-query';
import { ArrowLeft } from 'lucide-react';
import Link from 'next/link';
import { tasksApi } from '@/lib/api';
import { tasksApi, usersApi, residencesApi, lookupsApi, contractorsApi } from '@/lib/api';
import type { UpdateTaskRequest } from '@/types/models';
import { Button } from '@/components/ui/button';
import { Input } from '@/components/ui/input';
import { Label } from '@/components/ui/label';
import { Textarea } from '@/components/ui/textarea';
import { Switch } from '@/components/ui/switch';
import {
Select,
SelectContent,
SelectItem,
SelectTrigger,
SelectValue,
} from '@/components/ui/select';
import {
Card,
CardContent,
@@ -27,29 +34,81 @@ export default function EditTaskPage() {
const params = useParams();
const taskId = Number(params.id);
const [formData, setFormData] = useState<UpdateTaskRequest>({
title: '',
description: '',
is_cancelled: false,
is_archived: false,
});
const [formData, setFormData] = useState<UpdateTaskRequest>({});
const { data: task, isLoading } = useQuery({
const { data: task, isLoading: taskLoading } = useQuery({
queryKey: ['task', taskId],
queryFn: () => tasksApi.get(taskId),
enabled: !!taskId,
});
const { data: usersData, isLoading: usersLoading } = useQuery({
queryKey: ['users', { per_page: 1000 }],
queryFn: () => usersApi.list({ per_page: 1000 }),
});
const { data: residencesData, isLoading: residencesLoading } = useQuery({
queryKey: ['residences', { per_page: 1000 }],
queryFn: () => residencesApi.list({ per_page: 1000 }),
});
const { data: categories, isLoading: categoriesLoading } = useQuery({
queryKey: ['lookups', 'categories'],
queryFn: () => lookupsApi.categories.list(),
});
const { data: priorities, isLoading: prioritiesLoading } = useQuery({
queryKey: ['lookups', 'priorities'],
queryFn: () => lookupsApi.priorities.list(),
});
const { data: statuses, isLoading: statusesLoading } = useQuery({
queryKey: ['lookups', 'statuses'],
queryFn: () => lookupsApi.statuses.list(),
});
const { data: frequencies, isLoading: frequenciesLoading } = useQuery({
queryKey: ['lookups', 'frequencies'],
queryFn: () => lookupsApi.frequencies.list(),
});
const { data: contractorsData } = useQuery({
queryKey: ['contractors', { per_page: 1000 }],
queryFn: () => contractorsApi.list({ per_page: 1000 }),
});
const { data: allTasks } = useQuery({
queryKey: ['tasks', { per_page: 1000 }],
queryFn: () => tasksApi.list({ per_page: 1000 }),
});
const [formInitialized, setFormInitialized] = useState(false);
useEffect(() => {
if (task) {
if (task && !formInitialized) {
setFormData({
residence_id: task.residence_id,
created_by_id: task.created_by_id,
assigned_to_id: task.assigned_to_id,
title: task.title,
description: task.description,
category_id: task.category_id,
priority_id: task.priority_id,
status_id: task.status_id,
frequency_id: task.frequency_id,
due_date: task.due_date,
estimated_cost: task.estimated_cost,
actual_cost: task.actual_cost,
contractor_id: task.contractor_id,
parent_task_id: task.parent_task_id,
is_cancelled: task.is_cancelled,
is_archived: task.is_archived,
});
setFormInitialized(true);
}
}, [task]);
}, [task, formInitialized]);
const isDataLoading = taskLoading || usersLoading || residencesLoading || categoriesLoading || prioritiesLoading || statusesLoading || frequenciesLoading || !formInitialized;
const updateMutation = useMutation({
mutationFn: (data: UpdateTaskRequest) => tasksApi.update(taskId, data),
@@ -75,7 +134,7 @@ export default function EditTaskPage() {
setFormData((prev) => ({ ...prev, [field]: value }));
};
if (isLoading) {
if (isDataLoading) {
return (
<div className="flex items-center justify-center h-64">
<div className="text-muted-foreground">Loading...</div>
@@ -98,6 +157,93 @@ export default function EditTaskPage() {
</div>
<form onSubmit={handleSubmit} className="space-y-6">
<Card>
<CardHeader>
<CardTitle>Assignment</CardTitle>
<CardDescription>Task ownership and assignment</CardDescription>
</CardHeader>
<CardContent className="space-y-4">
<div className="grid grid-cols-2 gap-4">
<div className="space-y-2">
<Label htmlFor="residence_id">Residence *</Label>
<Select
value={formData.residence_id?.toString() || ''}
onValueChange={(value) => updateField('residence_id', Number(value))}
>
<SelectTrigger>
<SelectValue placeholder="Select residence" />
</SelectTrigger>
<SelectContent>
{residencesData?.data?.map((res) => (
<SelectItem key={res.id} value={res.id.toString()}>
{res.name}
</SelectItem>
))}
</SelectContent>
</Select>
</div>
<div className="space-y-2">
<Label htmlFor="created_by_id">Created By *</Label>
<Select
value={formData.created_by_id?.toString() || ''}
onValueChange={(value) => updateField('created_by_id', Number(value))}
>
<SelectTrigger>
<SelectValue placeholder="Select creator" />
</SelectTrigger>
<SelectContent>
{usersData?.data?.map((user) => (
<SelectItem key={user.id} value={user.id.toString()}>
{user.username} ({user.email})
</SelectItem>
))}
</SelectContent>
</Select>
</div>
</div>
<div className="grid grid-cols-2 gap-4">
<div className="space-y-2">
<Label htmlFor="assigned_to_id">Assigned To</Label>
<Select
value={formData.assigned_to_id?.toString() || 'none'}
onValueChange={(value) => updateField('assigned_to_id', value === 'none' ? undefined : Number(value))}
>
<SelectTrigger>
<SelectValue placeholder="Select assignee" />
</SelectTrigger>
<SelectContent>
<SelectItem value="none">None</SelectItem>
{usersData?.data?.map((user) => (
<SelectItem key={user.id} value={user.id.toString()}>
{user.username} ({user.email})
</SelectItem>
))}
</SelectContent>
</Select>
</div>
<div className="space-y-2">
<Label htmlFor="contractor_id">Contractor</Label>
<Select
value={formData.contractor_id?.toString() || 'none'}
onValueChange={(value) => updateField('contractor_id', value === 'none' ? undefined : Number(value))}
>
<SelectTrigger>
<SelectValue placeholder="Select contractor" />
</SelectTrigger>
<SelectContent>
<SelectItem value="none">None</SelectItem>
{contractorsData?.data?.map((contractor) => (
<SelectItem key={contractor.id} value={contractor.id.toString()}>
{contractor.name} {contractor.company && `(${contractor.company})`}
</SelectItem>
))}
</SelectContent>
</Select>
</div>
</div>
</CardContent>
</Card>
<Card>
<CardHeader>
<CardTitle>Basic Information</CardTitle>
@@ -120,6 +266,7 @@ export default function EditTaskPage() {
value={formData.description || ''}
onChange={(e) => updateField('description', e.target.value)}
placeholder="Task description..."
rows={3}
/>
</div>
</CardContent>
@@ -127,7 +274,166 @@ export default function EditTaskPage() {
<Card>
<CardHeader>
<CardTitle>Status</CardTitle>
<CardTitle>Classification</CardTitle>
<CardDescription>Task category, priority, and status</CardDescription>
</CardHeader>
<CardContent className="space-y-4">
<div className="grid grid-cols-2 gap-4">
<div className="space-y-2">
<Label htmlFor="category_id">Category</Label>
<Select
value={formData.category_id?.toString() || 'none'}
onValueChange={(value) => updateField('category_id', value === 'none' ? undefined : Number(value))}
>
<SelectTrigger>
<SelectValue placeholder="Select category" />
</SelectTrigger>
<SelectContent>
<SelectItem value="none">None</SelectItem>
{categories?.map((cat: { id: number; name: string }) => (
<SelectItem key={cat.id} value={cat.id.toString()}>
{cat.name}
</SelectItem>
))}
</SelectContent>
</Select>
</div>
<div className="space-y-2">
<Label htmlFor="priority_id">Priority</Label>
<Select
value={formData.priority_id?.toString() || 'none'}
onValueChange={(value) => updateField('priority_id', value === 'none' ? undefined : Number(value))}
>
<SelectTrigger>
<SelectValue placeholder="Select priority" />
</SelectTrigger>
<SelectContent>
<SelectItem value="none">None</SelectItem>
{priorities?.map((pri: { id: number; name: string }) => (
<SelectItem key={pri.id} value={pri.id.toString()}>
{pri.name}
</SelectItem>
))}
</SelectContent>
</Select>
</div>
</div>
<div className="grid grid-cols-2 gap-4">
<div className="space-y-2">
<Label htmlFor="status_id">Status</Label>
<Select
value={formData.status_id?.toString() || 'none'}
onValueChange={(value) => updateField('status_id', value === 'none' ? undefined : Number(value))}
>
<SelectTrigger>
<SelectValue placeholder="Select status" />
</SelectTrigger>
<SelectContent>
<SelectItem value="none">None</SelectItem>
{statuses?.map((status: { id: number; name: string }) => (
<SelectItem key={status.id} value={status.id.toString()}>
{status.name}
</SelectItem>
))}
</SelectContent>
</Select>
</div>
<div className="space-y-2">
<Label htmlFor="frequency_id">Frequency</Label>
<Select
value={formData.frequency_id?.toString() || 'none'}
onValueChange={(value) => updateField('frequency_id', value === 'none' ? undefined : Number(value))}
>
<SelectTrigger>
<SelectValue placeholder="Select frequency" />
</SelectTrigger>
<SelectContent>
<SelectItem value="none">None (One-time)</SelectItem>
{frequencies?.map((freq: { id: number; name: string }) => (
<SelectItem key={freq.id} value={freq.id.toString()}>
{freq.name}
</SelectItem>
))}
</SelectContent>
</Select>
</div>
</div>
</CardContent>
</Card>
<Card>
<CardHeader>
<CardTitle>Scheduling & Cost</CardTitle>
<CardDescription>Due date and cost information</CardDescription>
</CardHeader>
<CardContent className="space-y-4">
<div className="grid grid-cols-3 gap-4">
<div className="space-y-2">
<Label htmlFor="due_date">Due Date</Label>
<Input
id="due_date"
type="date"
value={formData.due_date || ''}
onChange={(e) => updateField('due_date', e.target.value || undefined)}
/>
</div>
<div className="space-y-2">
<Label htmlFor="estimated_cost">Estimated Cost ($)</Label>
<Input
id="estimated_cost"
type="number"
step="0.01"
value={formData.estimated_cost ?? ''}
onChange={(e) => updateField('estimated_cost', e.target.value ? Number(e.target.value) : undefined)}
placeholder="0.00"
/>
</div>
<div className="space-y-2">
<Label htmlFor="actual_cost">Actual Cost ($)</Label>
<Input
id="actual_cost"
type="number"
step="0.01"
value={formData.actual_cost ?? ''}
onChange={(e) => updateField('actual_cost', e.target.value ? Number(e.target.value) : undefined)}
placeholder="0.00"
/>
</div>
</div>
</CardContent>
</Card>
<Card>
<CardHeader>
<CardTitle>Relationships</CardTitle>
<CardDescription>Link to parent task (for recurring tasks)</CardDescription>
</CardHeader>
<CardContent className="space-y-4">
<div className="space-y-2">
<Label htmlFor="parent_task_id">Parent Task</Label>
<Select
value={formData.parent_task_id?.toString() || 'none'}
onValueChange={(value) => updateField('parent_task_id', value === 'none' ? undefined : Number(value))}
>
<SelectTrigger>
<SelectValue placeholder="Select parent task" />
</SelectTrigger>
<SelectContent>
<SelectItem value="none">None</SelectItem>
{allTasks?.data?.filter(t => t.id !== taskId).map((t) => (
<SelectItem key={t.id} value={t.id.toString()}>
{t.title} (#{t.id})
</SelectItem>
))}
</SelectContent>
</Select>
</div>
</CardContent>
</Card>
<Card>
<CardHeader>
<CardTitle>Status Flags</CardTitle>
<CardDescription>Task state flags</CardDescription>
</CardHeader>
<CardContent className="space-y-4">
+3 -1
View File
@@ -7,7 +7,9 @@ function Textarea({ className, ...props }: React.ComponentProps<"textarea">) {
<textarea
data-slot="textarea"
className={cn(
"border-input placeholder:text-muted-foreground focus-visible:border-ring focus-visible:ring-ring/50 aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 aria-invalid:border-destructive dark:bg-input/30 flex field-sizing-content min-h-16 w-full rounded-md border bg-transparent px-3 py-2 text-base shadow-xs transition-[color,box-shadow] outline-none focus-visible:ring-[3px] disabled:cursor-not-allowed disabled:opacity-50 md:text-sm",
"placeholder:text-muted-foreground selection:bg-primary selection:text-primary-foreground dark:bg-input/30 border-input min-h-[80px] w-full min-w-0 rounded-md border bg-transparent px-3 py-2 text-base shadow-xs transition-[color,box-shadow] outline-none disabled:pointer-events-none disabled:cursor-not-allowed disabled:opacity-50 md:text-sm",
"focus-visible:border-ring focus-visible:ring-ring/50 focus-visible:ring-[3px]",
"aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 aria-invalid:border-destructive",
className
)}
{...props}
+25 -2
View File
@@ -38,6 +38,7 @@ const userFormSchema = z.object({
is_active: z.boolean(),
is_staff: z.boolean(),
is_superuser: z.boolean(),
verified: z.boolean(),
});
type FormValues = z.infer<typeof userFormSchema>;
@@ -64,6 +65,7 @@ export function UserForm({ user, onSubmit, isSubmitting }: UserFormProps) {
is_active: user?.is_active ?? true,
is_staff: user?.is_staff ?? false,
is_superuser: user?.is_superuser ?? false,
verified: user?.verified ?? false,
},
});
@@ -230,13 +232,13 @@ export function UserForm({ user, onSubmit, isSubmitting }: UserFormProps) {
{/* Permissions */}
<Card className="md:col-span-2">
<CardHeader>
<CardTitle>Permissions</CardTitle>
<CardTitle>Permissions & Status</CardTitle>
<CardDescription>
User status and access levels
</CardDescription>
</CardHeader>
<CardContent>
<div className="grid gap-4 md:grid-cols-3">
<div className="grid gap-4 md:grid-cols-4">
<FormField
control={form.control}
name="is_active"
@@ -258,6 +260,27 @@ export function UserForm({ user, onSubmit, isSubmitting }: UserFormProps) {
)}
/>
<FormField
control={form.control}
name="verified"
render={({ field }) => (
<FormItem className="flex flex-row items-start space-x-3 space-y-0 rounded-md border p-4">
<FormControl>
<Checkbox
checked={field.value}
onCheckedChange={field.onChange}
/>
</FormControl>
<div className="space-y-1 leading-none">
<FormLabel>Verified</FormLabel>
<FormDescription>
Email has been verified
</FormDescription>
</div>
</FormItem>
)}
/>
<FormField
control={form.control}
name="is_staff"
+5
View File
@@ -670,6 +670,11 @@ export const settingsApi = {
const response = await api.post<{ message: string }>('/settings/seed-test-data');
return response.data;
},
clearAllData: async (): Promise<{ message: string; users_deleted: number; preserved_users: number }> => {
const response = await api.post<{ message: string; users_deleted: number; preserved_users: number }>('/settings/clear-all-data');
return response.data;
},
};
// Limitations types
+93 -4
View File
@@ -66,6 +66,7 @@ export interface UpdateUserRequest {
is_active?: boolean;
is_staff?: boolean;
is_superuser?: boolean;
verified?: boolean;
}
// Query params for listing
@@ -90,8 +91,10 @@ export interface Residence {
name: string;
owner_id: number;
owner_name: string;
property_type_id?: number;
property_type?: string;
street_address: string;
apartment_unit: string;
city: string;
state_province: string;
postal_code: string;
@@ -99,9 +102,15 @@ export interface Residence {
bedrooms?: number;
bathrooms?: number;
square_footage?: number;
lot_size?: number;
year_built?: number;
description: string;
purchase_date?: string;
purchase_price?: number;
is_primary: boolean;
is_active: boolean;
created_at: string;
updated_at: string;
}
export interface ResidenceDetail extends Residence {
@@ -141,12 +150,23 @@ export interface CreateResidenceRequest {
}
export interface UpdateResidenceRequest {
owner_id?: number;
name?: string;
property_type_id?: number;
street_address?: string;
apartment_unit?: string;
city?: string;
state_province?: string;
postal_code?: string;
country?: string;
bedrooms?: number;
bathrooms?: number;
square_footage?: number;
lot_size?: number;
year_built?: number;
description?: string;
purchase_date?: string;
purchase_price?: number;
is_active?: boolean;
is_primary?: boolean;
}
@@ -156,17 +176,29 @@ export interface Task {
id: number;
residence_id: number;
residence_name: string;
created_by_id: number;
created_by_name: string;
assigned_to_id?: number;
assigned_to_name?: string;
title: string;
description: string;
created_by_name: string;
category_id?: number;
category_name?: string;
priority_id?: number;
priority_name?: string;
status_id?: number;
status_name?: string;
frequency_id?: number;
frequency_name?: string;
due_date?: string;
estimated_cost?: number;
actual_cost?: number;
contractor_id?: number;
parent_task_id?: number;
is_cancelled: boolean;
is_archived: boolean;
created_at: string;
updated_at: string;
}
export interface TaskDetail extends Task {
@@ -199,11 +231,20 @@ export interface CreateTaskRequest {
}
export interface UpdateTaskRequest {
residence_id?: number;
created_by_id?: number;
assigned_to_id?: number;
title?: string;
description?: string;
category_id?: number;
priority_id?: number;
status_id?: number;
frequency_id?: number;
due_date?: string;
estimated_cost?: number;
actual_cost?: number;
contractor_id?: number;
parent_task_id?: number;
is_cancelled?: boolean;
is_archived?: boolean;
}
@@ -213,17 +254,25 @@ export interface Contractor {
id: number;
residence_id: number;
residence_name: string;
created_by_id: number;
created_by_name: string;
name: string;
company: string;
phone: string;
email: string;
website: string;
notes: string;
street_address: string;
city: string;
state_province: string;
postal_code: string;
rating?: number;
specialties?: string[];
specialty_ids?: number[];
is_favorite: boolean;
is_active: boolean;
created_at: string;
updated_at: string;
}
export interface ContractorDetail extends Contractor {
@@ -254,14 +303,22 @@ export interface CreateContractorRequest {
}
export interface UpdateContractorRequest {
residence_id?: number;
created_by_id?: number;
name?: string;
company?: string;
phone?: string;
email?: string;
website?: string;
notes?: string;
street_address?: string;
city?: string;
state_province?: string;
postal_code?: string;
rating?: number;
is_favorite?: boolean;
is_active?: boolean;
specialty_ids?: number[];
}
// Document types
@@ -275,17 +332,32 @@ export interface Document {
id: number;
residence_id: number;
residence_name: string;
created_by_id: number;
created_by_name: string;
title: string;
description: string;
document_type: string;
file_name: string;
file_url: string;
vendor: string;
expiry_date?: string;
file_name: string;
file_size?: number;
mime_type: string;
purchase_date?: string;
expiry_date?: string;
purchase_price?: number;
vendor: string;
serial_number: string;
model_number: string;
provider: string;
provider_contact: string;
claim_phone: string;
claim_email: string;
claim_website: string;
notes: string;
task_id?: number;
is_active: boolean;
images: DocumentImage[];
created_at: string;
updated_at: string;
}
export interface DocumentDetail extends Document {
@@ -319,11 +391,28 @@ export interface CreateDocumentRequest {
}
export interface UpdateDocumentRequest {
residence_id?: number;
created_by_id?: number;
title?: string;
description?: string;
document_type?: string;
file_url?: string;
file_name?: string;
file_size?: number;
mime_type?: string;
purchase_date?: string;
expiry_date?: string;
purchase_price?: number;
vendor?: string;
serial_number?: string;
model_number?: string;
provider?: string;
provider_contact?: string;
claim_phone?: string;
claim_email?: string;
claim_website?: string;
notes?: string;
task_id?: number;
is_active?: boolean;
}