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:
@@ -6,13 +6,20 @@ import { useMutation, useQuery } from '@tanstack/react-query';
|
|||||||
import { ArrowLeft } from 'lucide-react';
|
import { ArrowLeft } from 'lucide-react';
|
||||||
import Link from 'next/link';
|
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 type { UpdateContractorRequest } from '@/types/models';
|
||||||
import { Button } from '@/components/ui/button';
|
import { Button } from '@/components/ui/button';
|
||||||
import { Input } from '@/components/ui/input';
|
import { Input } from '@/components/ui/input';
|
||||||
import { Label } from '@/components/ui/label';
|
import { Label } from '@/components/ui/label';
|
||||||
import { Textarea } from '@/components/ui/textarea';
|
import { Textarea } from '@/components/ui/textarea';
|
||||||
import { Switch } from '@/components/ui/switch';
|
import { Switch } from '@/components/ui/switch';
|
||||||
|
import {
|
||||||
|
Select,
|
||||||
|
SelectContent,
|
||||||
|
SelectItem,
|
||||||
|
SelectTrigger,
|
||||||
|
SelectValue,
|
||||||
|
} from '@/components/ui/select';
|
||||||
import {
|
import {
|
||||||
Card,
|
Card,
|
||||||
CardContent,
|
CardContent,
|
||||||
@@ -27,36 +34,56 @@ export default function EditContractorPage() {
|
|||||||
const params = useParams();
|
const params = useParams();
|
||||||
const contractorId = Number(params.id);
|
const contractorId = Number(params.id);
|
||||||
|
|
||||||
const [formData, setFormData] = useState<UpdateContractorRequest>({
|
const [formData, setFormData] = useState<UpdateContractorRequest>({});
|
||||||
name: '',
|
|
||||||
company: '',
|
|
||||||
phone: '',
|
|
||||||
email: '',
|
|
||||||
website: '',
|
|
||||||
notes: '',
|
|
||||||
is_favorite: false,
|
|
||||||
is_active: true,
|
|
||||||
});
|
|
||||||
|
|
||||||
const { data: contractor, isLoading } = useQuery({
|
const { data: contractor, isLoading: contractorLoading } = useQuery({
|
||||||
queryKey: ['contractor', contractorId],
|
queryKey: ['contractor', contractorId],
|
||||||
queryFn: () => contractorsApi.get(contractorId),
|
queryFn: () => contractorsApi.get(contractorId),
|
||||||
enabled: !!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(() => {
|
useEffect(() => {
|
||||||
if (contractor) {
|
if (contractor && !formInitialized) {
|
||||||
setFormData({
|
setFormData({
|
||||||
|
residence_id: contractor.residence_id,
|
||||||
|
created_by_id: contractor.created_by_id,
|
||||||
name: contractor.name,
|
name: contractor.name,
|
||||||
company: contractor.company,
|
company: contractor.company,
|
||||||
phone: contractor.phone,
|
phone: contractor.phone,
|
||||||
email: contractor.email,
|
email: contractor.email,
|
||||||
website: contractor.website,
|
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_favorite: contractor.is_favorite,
|
||||||
is_active: contractor.is_active,
|
is_active: contractor.is_active,
|
||||||
|
specialty_ids: contractor.specialty_ids,
|
||||||
});
|
});
|
||||||
|
setFormInitialized(true);
|
||||||
}
|
}
|
||||||
}, [contractor]);
|
}, [contractor, formInitialized]);
|
||||||
|
|
||||||
|
const isDataLoading = contractorLoading || usersLoading || residencesLoading || !formInitialized;
|
||||||
|
|
||||||
const updateMutation = useMutation({
|
const updateMutation = useMutation({
|
||||||
mutationFn: (data: UpdateContractorRequest) => contractorsApi.update(contractorId, data),
|
mutationFn: (data: UpdateContractorRequest) => contractorsApi.update(contractorId, data),
|
||||||
@@ -82,7 +109,7 @@ export default function EditContractorPage() {
|
|||||||
setFormData((prev) => ({ ...prev, [field]: value }));
|
setFormData((prev) => ({ ...prev, [field]: value }));
|
||||||
};
|
};
|
||||||
|
|
||||||
if (isLoading) {
|
if (isDataLoading) {
|
||||||
return (
|
return (
|
||||||
<div className="flex items-center justify-center h-64">
|
<div className="flex items-center justify-center h-64">
|
||||||
<div className="text-muted-foreground">Loading...</div>
|
<div className="text-muted-foreground">Loading...</div>
|
||||||
@@ -105,10 +132,57 @@ export default function EditContractorPage() {
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<form onSubmit={handleSubmit} className="space-y-6">
|
<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>
|
<Card>
|
||||||
<CardHeader>
|
<CardHeader>
|
||||||
<CardTitle>Basic Information</CardTitle>
|
<CardTitle>Basic Information</CardTitle>
|
||||||
<CardDescription>Update the contractor details</CardDescription>
|
<CardDescription>Contractor contact details</CardDescription>
|
||||||
</CardHeader>
|
</CardHeader>
|
||||||
<CardContent className="space-y-4">
|
<CardContent className="space-y-4">
|
||||||
<div className="grid grid-cols-2 gap-4">
|
<div className="grid grid-cols-2 gap-4">
|
||||||
@@ -118,7 +192,7 @@ export default function EditContractorPage() {
|
|||||||
id="name"
|
id="name"
|
||||||
value={formData.name || ''}
|
value={formData.name || ''}
|
||||||
onChange={(e) => updateField('name', e.target.value)}
|
onChange={(e) => updateField('name', e.target.value)}
|
||||||
placeholder="John Doe"
|
placeholder="Contractor name"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
<div className="space-y-2">
|
<div className="space-y-2">
|
||||||
@@ -127,37 +201,10 @@ export default function EditContractorPage() {
|
|||||||
id="company"
|
id="company"
|
||||||
value={formData.company || ''}
|
value={formData.company || ''}
|
||||||
onChange={(e) => updateField('company', e.target.value)}
|
onChange={(e) => updateField('company', e.target.value)}
|
||||||
placeholder="ABC Plumbing"
|
placeholder="Company name"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
</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="grid grid-cols-2 gap-4">
|
||||||
<div className="space-y-2">
|
<div className="space-y-2">
|
||||||
<Label htmlFor="phone">Phone</Label>
|
<Label htmlFor="phone">Phone</Label>
|
||||||
@@ -165,7 +212,7 @@ export default function EditContractorPage() {
|
|||||||
id="phone"
|
id="phone"
|
||||||
value={formData.phone || ''}
|
value={formData.phone || ''}
|
||||||
onChange={(e) => updateField('phone', e.target.value)}
|
onChange={(e) => updateField('phone', e.target.value)}
|
||||||
placeholder="(555) 123-4567"
|
placeholder="555-123-4567"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
<div className="space-y-2">
|
<div className="space-y-2">
|
||||||
@@ -188,21 +235,136 @@ export default function EditContractorPage() {
|
|||||||
placeholder="https://example.com"
|
placeholder="https://example.com"
|
||||||
/>
|
/>
|
||||||
</div>
|
</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>
|
</CardContent>
|
||||||
</Card>
|
</Card>
|
||||||
|
|
||||||
<Card>
|
<Card>
|
||||||
<CardHeader>
|
<CardHeader>
|
||||||
<CardTitle>Notes</CardTitle>
|
<CardTitle>Address</CardTitle>
|
||||||
<CardDescription>Additional information</CardDescription>
|
<CardDescription>Contractor location</CardDescription>
|
||||||
</CardHeader>
|
</CardHeader>
|
||||||
<CardContent>
|
<CardContent className="space-y-4">
|
||||||
<Textarea
|
<div className="space-y-2">
|
||||||
id="notes"
|
<Label htmlFor="street_address">Street Address</Label>
|
||||||
value={formData.notes || ''}
|
<Input
|
||||||
onChange={(e) => updateField('notes', e.target.value)}
|
id="street_address"
|
||||||
placeholder="Any additional notes about this contractor..."
|
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>
|
</CardContent>
|
||||||
</Card>
|
</Card>
|
||||||
|
|
||||||
|
|||||||
@@ -6,13 +6,20 @@ import { useMutation, useQuery } from '@tanstack/react-query';
|
|||||||
import { ArrowLeft } from 'lucide-react';
|
import { ArrowLeft } from 'lucide-react';
|
||||||
import Link from 'next/link';
|
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 type { UpdateDocumentRequest } from '@/types/models';
|
||||||
import { Button } from '@/components/ui/button';
|
import { Button } from '@/components/ui/button';
|
||||||
import { Input } from '@/components/ui/input';
|
import { Input } from '@/components/ui/input';
|
||||||
import { Label } from '@/components/ui/label';
|
import { Label } from '@/components/ui/label';
|
||||||
import { Textarea } from '@/components/ui/textarea';
|
import { Textarea } from '@/components/ui/textarea';
|
||||||
import { Switch } from '@/components/ui/switch';
|
import { Switch } from '@/components/ui/switch';
|
||||||
|
import {
|
||||||
|
Select,
|
||||||
|
SelectContent,
|
||||||
|
SelectItem,
|
||||||
|
SelectTrigger,
|
||||||
|
SelectValue,
|
||||||
|
} from '@/components/ui/select';
|
||||||
import {
|
import {
|
||||||
Card,
|
Card,
|
||||||
CardContent,
|
CardContent,
|
||||||
@@ -22,36 +29,77 @@ import {
|
|||||||
} from '@/components/ui/card';
|
} from '@/components/ui/card';
|
||||||
import { toast } from 'sonner';
|
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() {
|
export default function EditDocumentPage() {
|
||||||
const router = useRouter();
|
const router = useRouter();
|
||||||
const params = useParams();
|
const params = useParams();
|
||||||
const documentId = Number(params.id);
|
const documentId = Number(params.id);
|
||||||
|
|
||||||
const [formData, setFormData] = useState<UpdateDocumentRequest>({
|
const [formData, setFormData] = useState<UpdateDocumentRequest>({});
|
||||||
title: '',
|
|
||||||
description: '',
|
|
||||||
vendor: '',
|
|
||||||
serial_number: '',
|
|
||||||
model_number: '',
|
|
||||||
is_active: true,
|
|
||||||
});
|
|
||||||
|
|
||||||
const { data: document, isLoading } = useQuery({
|
const { data: document, isLoading: documentLoading } = useQuery({
|
||||||
queryKey: ['document', documentId],
|
queryKey: ['document', documentId],
|
||||||
queryFn: () => documentsApi.get(documentId),
|
queryFn: () => documentsApi.get(documentId),
|
||||||
enabled: !!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(() => {
|
useEffect(() => {
|
||||||
if (document) {
|
if (document && !formInitialized) {
|
||||||
setFormData({
|
setFormData({
|
||||||
|
residence_id: document.residence_id,
|
||||||
|
created_by_id: document.created_by_id,
|
||||||
title: document.title,
|
title: document.title,
|
||||||
description: document.description,
|
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,
|
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,
|
is_active: document.is_active,
|
||||||
});
|
});
|
||||||
|
setFormInitialized(true);
|
||||||
}
|
}
|
||||||
}, [document]);
|
}, [document, formInitialized]);
|
||||||
|
|
||||||
|
const isDataLoading = documentLoading || usersLoading || residencesLoading || !formInitialized;
|
||||||
|
|
||||||
const updateMutation = useMutation({
|
const updateMutation = useMutation({
|
||||||
mutationFn: (data: UpdateDocumentRequest) => documentsApi.update(documentId, data),
|
mutationFn: (data: UpdateDocumentRequest) => documentsApi.update(documentId, data),
|
||||||
@@ -77,7 +125,7 @@ export default function EditDocumentPage() {
|
|||||||
setFormData((prev) => ({ ...prev, [field]: value }));
|
setFormData((prev) => ({ ...prev, [field]: value }));
|
||||||
};
|
};
|
||||||
|
|
||||||
if (isLoading) {
|
if (isDataLoading) {
|
||||||
return (
|
return (
|
||||||
<div className="flex items-center justify-center h-64">
|
<div className="flex items-center justify-center h-64">
|
||||||
<div className="text-muted-foreground">Loading...</div>
|
<div className="text-muted-foreground">Loading...</div>
|
||||||
@@ -102,18 +150,104 @@ export default function EditDocumentPage() {
|
|||||||
<form onSubmit={handleSubmit} className="space-y-6">
|
<form onSubmit={handleSubmit} className="space-y-6">
|
||||||
<Card>
|
<Card>
|
||||||
<CardHeader>
|
<CardHeader>
|
||||||
<CardTitle>Basic Information</CardTitle>
|
<CardTitle>Assignment</CardTitle>
|
||||||
<CardDescription>Update the document details</CardDescription>
|
<CardDescription>Document ownership</CardDescription>
|
||||||
</CardHeader>
|
</CardHeader>
|
||||||
<CardContent className="space-y-4">
|
<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">
|
<div className="space-y-2">
|
||||||
<Label htmlFor="title">Title *</Label>
|
<Label htmlFor="task_id">Related Task</Label>
|
||||||
<Input
|
<Select
|
||||||
id="title"
|
value={formData.task_id?.toString() || 'none'}
|
||||||
value={formData.title || ''}
|
onValueChange={(value) => updateField('task_id', value === 'none' ? undefined : Number(value))}
|
||||||
onChange={(e) => updateField('title', e.target.value)}
|
>
|
||||||
placeholder="Document title"
|
<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>
|
||||||
<div className="space-y-2">
|
<div className="space-y-2">
|
||||||
<Label htmlFor="description">Description</Label>
|
<Label htmlFor="description">Description</Label>
|
||||||
@@ -122,6 +256,17 @@ export default function EditDocumentPage() {
|
|||||||
value={formData.description || ''}
|
value={formData.description || ''}
|
||||||
onChange={(e) => updateField('description', e.target.value)}
|
onChange={(e) => updateField('description', e.target.value)}
|
||||||
placeholder="Document description..."
|
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>
|
||||||
<div className="flex items-center space-x-2">
|
<div className="flex items-center space-x-2">
|
||||||
@@ -135,6 +280,96 @@ export default function EditDocumentPage() {
|
|||||||
</CardContent>
|
</CardContent>
|
||||||
</Card>
|
</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>
|
<Card>
|
||||||
<CardHeader>
|
<CardHeader>
|
||||||
<CardTitle>Product Details</CardTitle>
|
<CardTitle>Product Details</CardTitle>
|
||||||
@@ -173,6 +408,65 @@ export default function EditDocumentPage() {
|
|||||||
</CardContent>
|
</CardContent>
|
||||||
</Card>
|
</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">
|
<div className="flex justify-end gap-4">
|
||||||
<Button type="button" variant="outline" asChild>
|
<Button type="button" variant="outline" asChild>
|
||||||
<Link href={`/documents/${documentId}`}>Cancel</Link>
|
<Link href={`/documents/${documentId}`}>Cancel</Link>
|
||||||
|
|||||||
@@ -6,12 +6,20 @@ import { useMutation, useQuery } from '@tanstack/react-query';
|
|||||||
import { ArrowLeft } from 'lucide-react';
|
import { ArrowLeft } from 'lucide-react';
|
||||||
import Link from 'next/link';
|
import Link from 'next/link';
|
||||||
|
|
||||||
import { residencesApi } from '@/lib/api';
|
import { residencesApi, usersApi, lookupsApi } from '@/lib/api';
|
||||||
import type { UpdateResidenceRequest } from '@/types/models';
|
import type { UpdateResidenceRequest } from '@/types/models';
|
||||||
import { Button } from '@/components/ui/button';
|
import { Button } from '@/components/ui/button';
|
||||||
import { Input } from '@/components/ui/input';
|
import { Input } from '@/components/ui/input';
|
||||||
import { Label } from '@/components/ui/label';
|
import { Label } from '@/components/ui/label';
|
||||||
import { Switch } from '@/components/ui/switch';
|
import { Switch } from '@/components/ui/switch';
|
||||||
|
import { Textarea } from '@/components/ui/textarea';
|
||||||
|
import {
|
||||||
|
Select,
|
||||||
|
SelectContent,
|
||||||
|
SelectItem,
|
||||||
|
SelectTrigger,
|
||||||
|
SelectValue,
|
||||||
|
} from '@/components/ui/select';
|
||||||
import {
|
import {
|
||||||
Card,
|
Card,
|
||||||
CardContent,
|
CardContent,
|
||||||
@@ -26,37 +34,54 @@ export default function EditResidencePage() {
|
|||||||
const params = useParams();
|
const params = useParams();
|
||||||
const residenceId = Number(params.id);
|
const residenceId = Number(params.id);
|
||||||
|
|
||||||
const [formData, setFormData] = useState<UpdateResidenceRequest>({
|
const [formData, setFormData] = useState<UpdateResidenceRequest>({});
|
||||||
name: '',
|
|
||||||
street_address: '',
|
|
||||||
city: '',
|
|
||||||
state_province: '',
|
|
||||||
postal_code: '',
|
|
||||||
country: '',
|
|
||||||
is_primary: false,
|
|
||||||
is_active: true,
|
|
||||||
});
|
|
||||||
|
|
||||||
const { data: residence, isLoading } = useQuery({
|
const { data: residence, isLoading: residenceLoading } = useQuery({
|
||||||
queryKey: ['residence', residenceId],
|
queryKey: ['residence', residenceId],
|
||||||
queryFn: () => residencesApi.get(residenceId),
|
queryFn: () => residencesApi.get(residenceId),
|
||||||
enabled: !!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(() => {
|
useEffect(() => {
|
||||||
if (residence) {
|
if (residence && !formInitialized) {
|
||||||
setFormData({
|
setFormData({
|
||||||
|
owner_id: residence.owner_id,
|
||||||
name: residence.name,
|
name: residence.name,
|
||||||
|
property_type_id: residence.property_type_id,
|
||||||
street_address: residence.street_address,
|
street_address: residence.street_address,
|
||||||
|
apartment_unit: residence.apartment_unit,
|
||||||
city: residence.city,
|
city: residence.city,
|
||||||
state_province: residence.state_province,
|
state_province: residence.state_province,
|
||||||
postal_code: residence.postal_code,
|
postal_code: residence.postal_code,
|
||||||
country: residence.country,
|
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_primary: residence.is_primary,
|
||||||
is_active: residence.is_active,
|
is_active: residence.is_active,
|
||||||
});
|
});
|
||||||
|
setFormInitialized(true);
|
||||||
}
|
}
|
||||||
}, [residence]);
|
}, [residence, formInitialized]);
|
||||||
|
|
||||||
|
const isDataLoading = residenceLoading || usersLoading || !formInitialized;
|
||||||
|
|
||||||
const updateMutation = useMutation({
|
const updateMutation = useMutation({
|
||||||
mutationFn: (data: UpdateResidenceRequest) => residencesApi.update(residenceId, data),
|
mutationFn: (data: UpdateResidenceRequest) => residencesApi.update(residenceId, data),
|
||||||
@@ -82,7 +107,7 @@ export default function EditResidencePage() {
|
|||||||
setFormData((prev) => ({ ...prev, [field]: value }));
|
setFormData((prev) => ({ ...prev, [field]: value }));
|
||||||
};
|
};
|
||||||
|
|
||||||
if (isLoading) {
|
if (isDataLoading) {
|
||||||
return (
|
return (
|
||||||
<div className="flex items-center justify-center h-64">
|
<div className="flex items-center justify-center h-64">
|
||||||
<div className="text-muted-foreground">Loading...</div>
|
<div className="text-muted-foreground">Loading...</div>
|
||||||
@@ -105,19 +130,76 @@ export default function EditResidencePage() {
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<form onSubmit={handleSubmit} className="space-y-6">
|
<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>
|
<Card>
|
||||||
<CardHeader>
|
<CardHeader>
|
||||||
<CardTitle>Basic Information</CardTitle>
|
<CardTitle>Basic Information</CardTitle>
|
||||||
<CardDescription>Update the property details</CardDescription>
|
<CardDescription>Update the property details</CardDescription>
|
||||||
</CardHeader>
|
</CardHeader>
|
||||||
<CardContent className="space-y-4">
|
<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">
|
<div className="space-y-2">
|
||||||
<Label htmlFor="name">Name *</Label>
|
<Label htmlFor="description">Description</Label>
|
||||||
<Input
|
<Textarea
|
||||||
id="name"
|
id="description"
|
||||||
value={formData.name || ''}
|
value={formData.description || ''}
|
||||||
onChange={(e) => updateField('name', e.target.value)}
|
onChange={(e) => updateField('description', e.target.value)}
|
||||||
placeholder="My Home"
|
placeholder="Property description..."
|
||||||
|
rows={3}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
<div className="flex items-center gap-6">
|
<div className="flex items-center gap-6">
|
||||||
@@ -147,14 +229,25 @@ export default function EditResidencePage() {
|
|||||||
<CardDescription>Property location details</CardDescription>
|
<CardDescription>Property location details</CardDescription>
|
||||||
</CardHeader>
|
</CardHeader>
|
||||||
<CardContent className="space-y-4">
|
<CardContent className="space-y-4">
|
||||||
<div className="space-y-2">
|
<div className="grid grid-cols-2 gap-4">
|
||||||
<Label htmlFor="street_address">Street Address</Label>
|
<div className="space-y-2">
|
||||||
<Input
|
<Label htmlFor="street_address">Street Address</Label>
|
||||||
id="street_address"
|
<Input
|
||||||
value={formData.street_address || ''}
|
id="street_address"
|
||||||
onChange={(e) => updateField('street_address', e.target.value)}
|
value={formData.street_address || ''}
|
||||||
placeholder="123 Main St"
|
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>
|
||||||
<div className="grid grid-cols-2 gap-4">
|
<div className="grid grid-cols-2 gap-4">
|
||||||
<div className="space-y-2">
|
<div className="space-y-2">
|
||||||
@@ -199,6 +292,101 @@ export default function EditResidencePage() {
|
|||||||
</CardContent>
|
</CardContent>
|
||||||
</Card>
|
</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">
|
<div className="flex justify-end gap-4">
|
||||||
<Button type="button" variant="outline" asChild>
|
<Button type="button" variant="outline" asChild>
|
||||||
<Link href={`/residences/${residenceId}`}>Cancel</Link>
|
<Link href={`/residences/${residenceId}`}>Cancel</Link>
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
'use client';
|
'use client';
|
||||||
|
|
||||||
import { useMutation } from '@tanstack/react-query';
|
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 { settingsApi } from '@/lib/api';
|
||||||
import { Button } from '@/components/ui/button';
|
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 (
|
return (
|
||||||
<div className="p-6 space-y-6">
|
<div className="p-6 space-y-6">
|
||||||
<div>
|
<div>
|
||||||
@@ -151,6 +161,61 @@ export default function SettingsPage() {
|
|||||||
</AlertDialog>
|
</AlertDialog>
|
||||||
</CardContent>
|
</CardContent>
|
||||||
</Card>
|
</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>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -6,13 +6,20 @@ import { useMutation, useQuery } from '@tanstack/react-query';
|
|||||||
import { ArrowLeft } from 'lucide-react';
|
import { ArrowLeft } from 'lucide-react';
|
||||||
import Link from 'next/link';
|
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 type { UpdateTaskRequest } from '@/types/models';
|
||||||
import { Button } from '@/components/ui/button';
|
import { Button } from '@/components/ui/button';
|
||||||
import { Input } from '@/components/ui/input';
|
import { Input } from '@/components/ui/input';
|
||||||
import { Label } from '@/components/ui/label';
|
import { Label } from '@/components/ui/label';
|
||||||
import { Textarea } from '@/components/ui/textarea';
|
import { Textarea } from '@/components/ui/textarea';
|
||||||
import { Switch } from '@/components/ui/switch';
|
import { Switch } from '@/components/ui/switch';
|
||||||
|
import {
|
||||||
|
Select,
|
||||||
|
SelectContent,
|
||||||
|
SelectItem,
|
||||||
|
SelectTrigger,
|
||||||
|
SelectValue,
|
||||||
|
} from '@/components/ui/select';
|
||||||
import {
|
import {
|
||||||
Card,
|
Card,
|
||||||
CardContent,
|
CardContent,
|
||||||
@@ -27,29 +34,81 @@ export default function EditTaskPage() {
|
|||||||
const params = useParams();
|
const params = useParams();
|
||||||
const taskId = Number(params.id);
|
const taskId = Number(params.id);
|
||||||
|
|
||||||
const [formData, setFormData] = useState<UpdateTaskRequest>({
|
const [formData, setFormData] = useState<UpdateTaskRequest>({});
|
||||||
title: '',
|
|
||||||
description: '',
|
|
||||||
is_cancelled: false,
|
|
||||||
is_archived: false,
|
|
||||||
});
|
|
||||||
|
|
||||||
const { data: task, isLoading } = useQuery({
|
const { data: task, isLoading: taskLoading } = useQuery({
|
||||||
queryKey: ['task', taskId],
|
queryKey: ['task', taskId],
|
||||||
queryFn: () => tasksApi.get(taskId),
|
queryFn: () => tasksApi.get(taskId),
|
||||||
enabled: !!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(() => {
|
useEffect(() => {
|
||||||
if (task) {
|
if (task && !formInitialized) {
|
||||||
setFormData({
|
setFormData({
|
||||||
|
residence_id: task.residence_id,
|
||||||
|
created_by_id: task.created_by_id,
|
||||||
|
assigned_to_id: task.assigned_to_id,
|
||||||
title: task.title,
|
title: task.title,
|
||||||
description: task.description,
|
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_cancelled: task.is_cancelled,
|
||||||
is_archived: task.is_archived,
|
is_archived: task.is_archived,
|
||||||
});
|
});
|
||||||
|
setFormInitialized(true);
|
||||||
}
|
}
|
||||||
}, [task]);
|
}, [task, formInitialized]);
|
||||||
|
|
||||||
|
const isDataLoading = taskLoading || usersLoading || residencesLoading || categoriesLoading || prioritiesLoading || statusesLoading || frequenciesLoading || !formInitialized;
|
||||||
|
|
||||||
const updateMutation = useMutation({
|
const updateMutation = useMutation({
|
||||||
mutationFn: (data: UpdateTaskRequest) => tasksApi.update(taskId, data),
|
mutationFn: (data: UpdateTaskRequest) => tasksApi.update(taskId, data),
|
||||||
@@ -75,7 +134,7 @@ export default function EditTaskPage() {
|
|||||||
setFormData((prev) => ({ ...prev, [field]: value }));
|
setFormData((prev) => ({ ...prev, [field]: value }));
|
||||||
};
|
};
|
||||||
|
|
||||||
if (isLoading) {
|
if (isDataLoading) {
|
||||||
return (
|
return (
|
||||||
<div className="flex items-center justify-center h-64">
|
<div className="flex items-center justify-center h-64">
|
||||||
<div className="text-muted-foreground">Loading...</div>
|
<div className="text-muted-foreground">Loading...</div>
|
||||||
@@ -98,6 +157,93 @@ export default function EditTaskPage() {
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<form onSubmit={handleSubmit} className="space-y-6">
|
<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>
|
<Card>
|
||||||
<CardHeader>
|
<CardHeader>
|
||||||
<CardTitle>Basic Information</CardTitle>
|
<CardTitle>Basic Information</CardTitle>
|
||||||
@@ -120,6 +266,7 @@ export default function EditTaskPage() {
|
|||||||
value={formData.description || ''}
|
value={formData.description || ''}
|
||||||
onChange={(e) => updateField('description', e.target.value)}
|
onChange={(e) => updateField('description', e.target.value)}
|
||||||
placeholder="Task description..."
|
placeholder="Task description..."
|
||||||
|
rows={3}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
</CardContent>
|
</CardContent>
|
||||||
@@ -127,7 +274,166 @@ export default function EditTaskPage() {
|
|||||||
|
|
||||||
<Card>
|
<Card>
|
||||||
<CardHeader>
|
<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>
|
<CardDescription>Task state flags</CardDescription>
|
||||||
</CardHeader>
|
</CardHeader>
|
||||||
<CardContent className="space-y-4">
|
<CardContent className="space-y-4">
|
||||||
|
|||||||
@@ -7,7 +7,9 @@ function Textarea({ className, ...props }: React.ComponentProps<"textarea">) {
|
|||||||
<textarea
|
<textarea
|
||||||
data-slot="textarea"
|
data-slot="textarea"
|
||||||
className={cn(
|
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
|
className
|
||||||
)}
|
)}
|
||||||
{...props}
|
{...props}
|
||||||
|
|||||||
@@ -38,6 +38,7 @@ const userFormSchema = z.object({
|
|||||||
is_active: z.boolean(),
|
is_active: z.boolean(),
|
||||||
is_staff: z.boolean(),
|
is_staff: z.boolean(),
|
||||||
is_superuser: z.boolean(),
|
is_superuser: z.boolean(),
|
||||||
|
verified: z.boolean(),
|
||||||
});
|
});
|
||||||
|
|
||||||
type FormValues = z.infer<typeof userFormSchema>;
|
type FormValues = z.infer<typeof userFormSchema>;
|
||||||
@@ -64,6 +65,7 @@ export function UserForm({ user, onSubmit, isSubmitting }: UserFormProps) {
|
|||||||
is_active: user?.is_active ?? true,
|
is_active: user?.is_active ?? true,
|
||||||
is_staff: user?.is_staff ?? false,
|
is_staff: user?.is_staff ?? false,
|
||||||
is_superuser: user?.is_superuser ?? false,
|
is_superuser: user?.is_superuser ?? false,
|
||||||
|
verified: user?.verified ?? false,
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -230,13 +232,13 @@ export function UserForm({ user, onSubmit, isSubmitting }: UserFormProps) {
|
|||||||
{/* Permissions */}
|
{/* Permissions */}
|
||||||
<Card className="md:col-span-2">
|
<Card className="md:col-span-2">
|
||||||
<CardHeader>
|
<CardHeader>
|
||||||
<CardTitle>Permissions</CardTitle>
|
<CardTitle>Permissions & Status</CardTitle>
|
||||||
<CardDescription>
|
<CardDescription>
|
||||||
User status and access levels
|
User status and access levels
|
||||||
</CardDescription>
|
</CardDescription>
|
||||||
</CardHeader>
|
</CardHeader>
|
||||||
<CardContent>
|
<CardContent>
|
||||||
<div className="grid gap-4 md:grid-cols-3">
|
<div className="grid gap-4 md:grid-cols-4">
|
||||||
<FormField
|
<FormField
|
||||||
control={form.control}
|
control={form.control}
|
||||||
name="is_active"
|
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
|
<FormField
|
||||||
control={form.control}
|
control={form.control}
|
||||||
name="is_staff"
|
name="is_staff"
|
||||||
|
|||||||
@@ -670,6 +670,11 @@ export const settingsApi = {
|
|||||||
const response = await api.post<{ message: string }>('/settings/seed-test-data');
|
const response = await api.post<{ message: string }>('/settings/seed-test-data');
|
||||||
return response.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
|
// Limitations types
|
||||||
|
|||||||
@@ -66,6 +66,7 @@ export interface UpdateUserRequest {
|
|||||||
is_active?: boolean;
|
is_active?: boolean;
|
||||||
is_staff?: boolean;
|
is_staff?: boolean;
|
||||||
is_superuser?: boolean;
|
is_superuser?: boolean;
|
||||||
|
verified?: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Query params for listing
|
// Query params for listing
|
||||||
@@ -90,8 +91,10 @@ export interface Residence {
|
|||||||
name: string;
|
name: string;
|
||||||
owner_id: number;
|
owner_id: number;
|
||||||
owner_name: string;
|
owner_name: string;
|
||||||
|
property_type_id?: number;
|
||||||
property_type?: string;
|
property_type?: string;
|
||||||
street_address: string;
|
street_address: string;
|
||||||
|
apartment_unit: string;
|
||||||
city: string;
|
city: string;
|
||||||
state_province: string;
|
state_province: string;
|
||||||
postal_code: string;
|
postal_code: string;
|
||||||
@@ -99,9 +102,15 @@ export interface Residence {
|
|||||||
bedrooms?: number;
|
bedrooms?: number;
|
||||||
bathrooms?: number;
|
bathrooms?: number;
|
||||||
square_footage?: number;
|
square_footage?: number;
|
||||||
|
lot_size?: number;
|
||||||
|
year_built?: number;
|
||||||
|
description: string;
|
||||||
|
purchase_date?: string;
|
||||||
|
purchase_price?: number;
|
||||||
is_primary: boolean;
|
is_primary: boolean;
|
||||||
is_active: boolean;
|
is_active: boolean;
|
||||||
created_at: string;
|
created_at: string;
|
||||||
|
updated_at: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface ResidenceDetail extends Residence {
|
export interface ResidenceDetail extends Residence {
|
||||||
@@ -141,12 +150,23 @@ export interface CreateResidenceRequest {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export interface UpdateResidenceRequest {
|
export interface UpdateResidenceRequest {
|
||||||
|
owner_id?: number;
|
||||||
name?: string;
|
name?: string;
|
||||||
|
property_type_id?: number;
|
||||||
street_address?: string;
|
street_address?: string;
|
||||||
|
apartment_unit?: string;
|
||||||
city?: string;
|
city?: string;
|
||||||
state_province?: string;
|
state_province?: string;
|
||||||
postal_code?: string;
|
postal_code?: string;
|
||||||
country?: 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_active?: boolean;
|
||||||
is_primary?: boolean;
|
is_primary?: boolean;
|
||||||
}
|
}
|
||||||
@@ -156,17 +176,29 @@ export interface Task {
|
|||||||
id: number;
|
id: number;
|
||||||
residence_id: number;
|
residence_id: number;
|
||||||
residence_name: string;
|
residence_name: string;
|
||||||
|
created_by_id: number;
|
||||||
|
created_by_name: string;
|
||||||
|
assigned_to_id?: number;
|
||||||
|
assigned_to_name?: string;
|
||||||
title: string;
|
title: string;
|
||||||
description: string;
|
description: string;
|
||||||
created_by_name: string;
|
category_id?: number;
|
||||||
category_name?: string;
|
category_name?: string;
|
||||||
|
priority_id?: number;
|
||||||
priority_name?: string;
|
priority_name?: string;
|
||||||
|
status_id?: number;
|
||||||
status_name?: string;
|
status_name?: string;
|
||||||
|
frequency_id?: number;
|
||||||
|
frequency_name?: string;
|
||||||
due_date?: string;
|
due_date?: string;
|
||||||
estimated_cost?: number;
|
estimated_cost?: number;
|
||||||
|
actual_cost?: number;
|
||||||
|
contractor_id?: number;
|
||||||
|
parent_task_id?: number;
|
||||||
is_cancelled: boolean;
|
is_cancelled: boolean;
|
||||||
is_archived: boolean;
|
is_archived: boolean;
|
||||||
created_at: string;
|
created_at: string;
|
||||||
|
updated_at: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface TaskDetail extends Task {
|
export interface TaskDetail extends Task {
|
||||||
@@ -199,11 +231,20 @@ export interface CreateTaskRequest {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export interface UpdateTaskRequest {
|
export interface UpdateTaskRequest {
|
||||||
|
residence_id?: number;
|
||||||
|
created_by_id?: number;
|
||||||
|
assigned_to_id?: number;
|
||||||
title?: string;
|
title?: string;
|
||||||
description?: string;
|
description?: string;
|
||||||
category_id?: number;
|
category_id?: number;
|
||||||
priority_id?: number;
|
priority_id?: number;
|
||||||
status_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_cancelled?: boolean;
|
||||||
is_archived?: boolean;
|
is_archived?: boolean;
|
||||||
}
|
}
|
||||||
@@ -213,17 +254,25 @@ export interface Contractor {
|
|||||||
id: number;
|
id: number;
|
||||||
residence_id: number;
|
residence_id: number;
|
||||||
residence_name: string;
|
residence_name: string;
|
||||||
|
created_by_id: number;
|
||||||
|
created_by_name: string;
|
||||||
name: string;
|
name: string;
|
||||||
company: string;
|
company: string;
|
||||||
phone: string;
|
phone: string;
|
||||||
email: string;
|
email: string;
|
||||||
website: string;
|
website: string;
|
||||||
|
notes: string;
|
||||||
|
street_address: string;
|
||||||
city: string;
|
city: string;
|
||||||
|
state_province: string;
|
||||||
|
postal_code: string;
|
||||||
rating?: number;
|
rating?: number;
|
||||||
specialties?: string[];
|
specialties?: string[];
|
||||||
|
specialty_ids?: number[];
|
||||||
is_favorite: boolean;
|
is_favorite: boolean;
|
||||||
is_active: boolean;
|
is_active: boolean;
|
||||||
created_at: string;
|
created_at: string;
|
||||||
|
updated_at: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface ContractorDetail extends Contractor {
|
export interface ContractorDetail extends Contractor {
|
||||||
@@ -254,14 +303,22 @@ export interface CreateContractorRequest {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export interface UpdateContractorRequest {
|
export interface UpdateContractorRequest {
|
||||||
|
residence_id?: number;
|
||||||
|
created_by_id?: number;
|
||||||
name?: string;
|
name?: string;
|
||||||
company?: string;
|
company?: string;
|
||||||
phone?: string;
|
phone?: string;
|
||||||
email?: string;
|
email?: string;
|
||||||
website?: string;
|
website?: string;
|
||||||
notes?: string;
|
notes?: string;
|
||||||
|
street_address?: string;
|
||||||
|
city?: string;
|
||||||
|
state_province?: string;
|
||||||
|
postal_code?: string;
|
||||||
|
rating?: number;
|
||||||
is_favorite?: boolean;
|
is_favorite?: boolean;
|
||||||
is_active?: boolean;
|
is_active?: boolean;
|
||||||
|
specialty_ids?: number[];
|
||||||
}
|
}
|
||||||
|
|
||||||
// Document types
|
// Document types
|
||||||
@@ -275,17 +332,32 @@ export interface Document {
|
|||||||
id: number;
|
id: number;
|
||||||
residence_id: number;
|
residence_id: number;
|
||||||
residence_name: string;
|
residence_name: string;
|
||||||
|
created_by_id: number;
|
||||||
|
created_by_name: string;
|
||||||
title: string;
|
title: string;
|
||||||
description: string;
|
description: string;
|
||||||
document_type: string;
|
document_type: string;
|
||||||
file_name: string;
|
|
||||||
file_url: string;
|
file_url: string;
|
||||||
vendor: string;
|
file_name: string;
|
||||||
expiry_date?: string;
|
file_size?: number;
|
||||||
|
mime_type: string;
|
||||||
purchase_date?: 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;
|
is_active: boolean;
|
||||||
images: DocumentImage[];
|
images: DocumentImage[];
|
||||||
created_at: string;
|
created_at: string;
|
||||||
|
updated_at: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface DocumentDetail extends Document {
|
export interface DocumentDetail extends Document {
|
||||||
@@ -319,11 +391,28 @@ export interface CreateDocumentRequest {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export interface UpdateDocumentRequest {
|
export interface UpdateDocumentRequest {
|
||||||
|
residence_id?: number;
|
||||||
|
created_by_id?: number;
|
||||||
title?: string;
|
title?: string;
|
||||||
description?: 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;
|
vendor?: string;
|
||||||
serial_number?: string;
|
serial_number?: string;
|
||||||
model_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;
|
is_active?: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -3,7 +3,7 @@ package dto
|
|||||||
// PaginationParams holds pagination query parameters
|
// PaginationParams holds pagination query parameters
|
||||||
type PaginationParams struct {
|
type PaginationParams struct {
|
||||||
Page int `form:"page" binding:"omitempty,min=1"`
|
Page int `form:"page" binding:"omitempty,min=1"`
|
||||||
PerPage int `form:"per_page" binding:"omitempty,min=1,max=100"`
|
PerPage int `form:"per_page" binding:"omitempty,min=1,max=10000"`
|
||||||
Search string `form:"search"`
|
Search string `form:"search"`
|
||||||
SortBy string `form:"sort_by"`
|
SortBy string `form:"sort_by"`
|
||||||
SortDir string `form:"sort_dir" binding:"omitempty,oneof=asc desc"`
|
SortDir string `form:"sort_dir" binding:"omitempty,oneof=asc desc"`
|
||||||
@@ -22,8 +22,8 @@ func (p *PaginationParams) GetPerPage() int {
|
|||||||
if p.PerPage < 1 {
|
if p.PerPage < 1 {
|
||||||
return 20
|
return 20
|
||||||
}
|
}
|
||||||
if p.PerPage > 100 {
|
if p.PerPage > 10000 {
|
||||||
return 100
|
return 10000
|
||||||
}
|
}
|
||||||
return p.PerPage
|
return p.PerPage
|
||||||
}
|
}
|
||||||
@@ -74,6 +74,7 @@ type UpdateUserRequest struct {
|
|||||||
IsActive *bool `json:"is_active"`
|
IsActive *bool `json:"is_active"`
|
||||||
IsStaff *bool `json:"is_staff"`
|
IsStaff *bool `json:"is_staff"`
|
||||||
IsSuperuser *bool `json:"is_superuser"`
|
IsSuperuser *bool `json:"is_superuser"`
|
||||||
|
Verified *bool `json:"verified"`
|
||||||
}
|
}
|
||||||
|
|
||||||
// BulkDeleteRequest for bulk delete operations
|
// BulkDeleteRequest for bulk delete operations
|
||||||
@@ -90,14 +91,25 @@ type ResidenceFilters struct {
|
|||||||
|
|
||||||
// UpdateResidenceRequest for updating a residence
|
// UpdateResidenceRequest for updating a residence
|
||||||
type UpdateResidenceRequest struct {
|
type UpdateResidenceRequest struct {
|
||||||
Name *string `json:"name" binding:"omitempty,max=200"`
|
OwnerID *uint `json:"owner_id"`
|
||||||
StreetAddress *string `json:"street_address" binding:"omitempty,max=255"`
|
Name *string `json:"name" binding:"omitempty,max=200"`
|
||||||
City *string `json:"city" binding:"omitempty,max=100"`
|
PropertyTypeID *uint `json:"property_type_id"`
|
||||||
StateProvince *string `json:"state_province" binding:"omitempty,max=100"`
|
StreetAddress *string `json:"street_address" binding:"omitempty,max=255"`
|
||||||
PostalCode *string `json:"postal_code" binding:"omitempty,max=20"`
|
ApartmentUnit *string `json:"apartment_unit" binding:"omitempty,max=50"`
|
||||||
Country *string `json:"country" binding:"omitempty,max=100"`
|
City *string `json:"city" binding:"omitempty,max=100"`
|
||||||
IsActive *bool `json:"is_active"`
|
StateProvince *string `json:"state_province" binding:"omitempty,max=100"`
|
||||||
IsPrimary *bool `json:"is_primary"`
|
PostalCode *string `json:"postal_code" binding:"omitempty,max=20"`
|
||||||
|
Country *string `json:"country" binding:"omitempty,max=100"`
|
||||||
|
Bedrooms *int `json:"bedrooms"`
|
||||||
|
Bathrooms *float64 `json:"bathrooms"`
|
||||||
|
SquareFootage *int `json:"square_footage"`
|
||||||
|
LotSize *float64 `json:"lot_size"`
|
||||||
|
YearBuilt *int `json:"year_built"`
|
||||||
|
Description *string `json:"description"`
|
||||||
|
PurchaseDate *string `json:"purchase_date"`
|
||||||
|
PurchasePrice *float64 `json:"purchase_price"`
|
||||||
|
IsActive *bool `json:"is_active"`
|
||||||
|
IsPrimary *bool `json:"is_primary"`
|
||||||
}
|
}
|
||||||
|
|
||||||
// TaskFilters holds task-specific filter parameters
|
// TaskFilters holds task-specific filter parameters
|
||||||
@@ -113,13 +125,22 @@ type TaskFilters struct {
|
|||||||
|
|
||||||
// UpdateTaskRequest for updating a task
|
// UpdateTaskRequest for updating a task
|
||||||
type UpdateTaskRequest struct {
|
type UpdateTaskRequest struct {
|
||||||
Title *string `json:"title" binding:"omitempty,max=200"`
|
ResidenceID *uint `json:"residence_id"`
|
||||||
Description *string `json:"description"`
|
CreatedByID *uint `json:"created_by_id"`
|
||||||
CategoryID *uint `json:"category_id"`
|
AssignedToID *uint `json:"assigned_to_id"`
|
||||||
PriorityID *uint `json:"priority_id"`
|
Title *string `json:"title" binding:"omitempty,max=200"`
|
||||||
StatusID *uint `json:"status_id"`
|
Description *string `json:"description"`
|
||||||
IsCancelled *bool `json:"is_cancelled"`
|
CategoryID *uint `json:"category_id"`
|
||||||
IsArchived *bool `json:"is_archived"`
|
PriorityID *uint `json:"priority_id"`
|
||||||
|
StatusID *uint `json:"status_id"`
|
||||||
|
FrequencyID *uint `json:"frequency_id"`
|
||||||
|
DueDate *string `json:"due_date"`
|
||||||
|
EstimatedCost *float64 `json:"estimated_cost"`
|
||||||
|
ActualCost *float64 `json:"actual_cost"`
|
||||||
|
ContractorID *uint `json:"contractor_id"`
|
||||||
|
ParentTaskID *uint `json:"parent_task_id"`
|
||||||
|
IsCancelled *bool `json:"is_cancelled"`
|
||||||
|
IsArchived *bool `json:"is_archived"`
|
||||||
}
|
}
|
||||||
|
|
||||||
// ContractorFilters holds contractor-specific filter parameters
|
// ContractorFilters holds contractor-specific filter parameters
|
||||||
@@ -132,14 +153,22 @@ type ContractorFilters struct {
|
|||||||
|
|
||||||
// UpdateContractorRequest for updating a contractor
|
// UpdateContractorRequest for updating a contractor
|
||||||
type UpdateContractorRequest struct {
|
type UpdateContractorRequest struct {
|
||||||
Name *string `json:"name" binding:"omitempty,max=200"`
|
ResidenceID *uint `json:"residence_id"`
|
||||||
Company *string `json:"company" binding:"omitempty,max=200"`
|
CreatedByID *uint `json:"created_by_id"`
|
||||||
Phone *string `json:"phone" binding:"omitempty,max=20"`
|
Name *string `json:"name" binding:"omitempty,max=200"`
|
||||||
Email *string `json:"email" binding:"omitempty,email"`
|
Company *string `json:"company" binding:"omitempty,max=200"`
|
||||||
Website *string `json:"website" binding:"omitempty,max=200"`
|
Phone *string `json:"phone" binding:"omitempty,max=20"`
|
||||||
Notes *string `json:"notes"`
|
Email *string `json:"email" binding:"omitempty,email"`
|
||||||
IsFavorite *bool `json:"is_favorite"`
|
Website *string `json:"website" binding:"omitempty,max=200"`
|
||||||
IsActive *bool `json:"is_active"`
|
Notes *string `json:"notes"`
|
||||||
|
StreetAddress *string `json:"street_address" binding:"omitempty,max=255"`
|
||||||
|
City *string `json:"city" binding:"omitempty,max=100"`
|
||||||
|
StateProvince *string `json:"state_province" binding:"omitempty,max=100"`
|
||||||
|
PostalCode *string `json:"postal_code" binding:"omitempty,max=20"`
|
||||||
|
Rating *float64 `json:"rating"`
|
||||||
|
IsFavorite *bool `json:"is_favorite"`
|
||||||
|
IsActive *bool `json:"is_active"`
|
||||||
|
SpecialtyIDs []uint `json:"specialty_ids"`
|
||||||
}
|
}
|
||||||
|
|
||||||
// DocumentFilters holds document-specific filter parameters
|
// DocumentFilters holds document-specific filter parameters
|
||||||
@@ -152,12 +181,29 @@ type DocumentFilters struct {
|
|||||||
|
|
||||||
// UpdateDocumentRequest for updating a document
|
// UpdateDocumentRequest for updating a document
|
||||||
type UpdateDocumentRequest struct {
|
type UpdateDocumentRequest struct {
|
||||||
Title *string `json:"title" binding:"omitempty,max=200"`
|
ResidenceID *uint `json:"residence_id"`
|
||||||
Description *string `json:"description"`
|
CreatedByID *uint `json:"created_by_id"`
|
||||||
Vendor *string `json:"vendor" binding:"omitempty,max=200"`
|
Title *string `json:"title" binding:"omitempty,max=200"`
|
||||||
SerialNumber *string `json:"serial_number" binding:"omitempty,max=100"`
|
Description *string `json:"description"`
|
||||||
ModelNumber *string `json:"model_number" binding:"omitempty,max=100"`
|
DocumentType *string `json:"document_type"`
|
||||||
IsActive *bool `json:"is_active"`
|
FileURL *string `json:"file_url" binding:"omitempty,max=500"`
|
||||||
|
FileName *string `json:"file_name" binding:"omitempty,max=255"`
|
||||||
|
FileSize *int64 `json:"file_size"`
|
||||||
|
MimeType *string `json:"mime_type" binding:"omitempty,max=100"`
|
||||||
|
PurchaseDate *string `json:"purchase_date"`
|
||||||
|
ExpiryDate *string `json:"expiry_date"`
|
||||||
|
PurchasePrice *float64 `json:"purchase_price"`
|
||||||
|
Vendor *string `json:"vendor" binding:"omitempty,max=200"`
|
||||||
|
SerialNumber *string `json:"serial_number" binding:"omitempty,max=100"`
|
||||||
|
ModelNumber *string `json:"model_number" binding:"omitempty,max=100"`
|
||||||
|
Provider *string `json:"provider" binding:"omitempty,max=200"`
|
||||||
|
ProviderContact *string `json:"provider_contact" binding:"omitempty,max=200"`
|
||||||
|
ClaimPhone *string `json:"claim_phone" binding:"omitempty,max=50"`
|
||||||
|
ClaimEmail *string `json:"claim_email" binding:"omitempty,email"`
|
||||||
|
ClaimWebsite *string `json:"claim_website" binding:"omitempty,max=500"`
|
||||||
|
Notes *string `json:"notes"`
|
||||||
|
TaskID *uint `json:"task_id"`
|
||||||
|
IsActive *bool `json:"is_active"`
|
||||||
}
|
}
|
||||||
|
|
||||||
// NotificationFilters holds notification-specific filter parameters
|
// NotificationFilters holds notification-specific filter parameters
|
||||||
@@ -188,8 +234,12 @@ type SubscriptionFilters struct {
|
|||||||
|
|
||||||
// UpdateSubscriptionRequest for updating a subscription
|
// UpdateSubscriptionRequest for updating a subscription
|
||||||
type UpdateSubscriptionRequest struct {
|
type UpdateSubscriptionRequest struct {
|
||||||
Tier *string `json:"tier" binding:"omitempty,oneof=free premium pro"`
|
Tier *string `json:"tier" binding:"omitempty,oneof=free premium pro"`
|
||||||
AutoRenew *bool `json:"auto_renew"`
|
AutoRenew *bool `json:"auto_renew"`
|
||||||
|
Platform *string `json:"platform" binding:"omitempty,max=20"`
|
||||||
|
SubscribedAt *string `json:"subscribed_at"`
|
||||||
|
ExpiresAt *string `json:"expires_at"`
|
||||||
|
CancelledAt *string `json:"cancelled_at"`
|
||||||
}
|
}
|
||||||
|
|
||||||
// CreateResidenceRequest for creating a new residence
|
// CreateResidenceRequest for creating a new residence
|
||||||
|
|||||||
@@ -76,22 +76,30 @@ type UserSummary struct {
|
|||||||
|
|
||||||
// ResidenceResponse represents a residence in admin responses
|
// ResidenceResponse represents a residence in admin responses
|
||||||
type ResidenceResponse struct {
|
type ResidenceResponse struct {
|
||||||
ID uint `json:"id"`
|
ID uint `json:"id"`
|
||||||
Name string `json:"name"`
|
Name string `json:"name"`
|
||||||
OwnerID uint `json:"owner_id"`
|
OwnerID uint `json:"owner_id"`
|
||||||
OwnerName string `json:"owner_name"`
|
OwnerName string `json:"owner_name"`
|
||||||
PropertyType *string `json:"property_type,omitempty"`
|
PropertyTypeID *uint `json:"property_type_id,omitempty"`
|
||||||
StreetAddress string `json:"street_address"`
|
PropertyType *string `json:"property_type,omitempty"`
|
||||||
City string `json:"city"`
|
StreetAddress string `json:"street_address"`
|
||||||
StateProvince string `json:"state_province"`
|
ApartmentUnit string `json:"apartment_unit"`
|
||||||
PostalCode string `json:"postal_code"`
|
City string `json:"city"`
|
||||||
Country string `json:"country"`
|
StateProvince string `json:"state_province"`
|
||||||
Bedrooms *int `json:"bedrooms,omitempty"`
|
PostalCode string `json:"postal_code"`
|
||||||
Bathrooms *float64 `json:"bathrooms,omitempty"`
|
Country string `json:"country"`
|
||||||
SquareFootage *int `json:"square_footage,omitempty"`
|
Bedrooms *int `json:"bedrooms,omitempty"`
|
||||||
IsPrimary bool `json:"is_primary"`
|
Bathrooms *float64 `json:"bathrooms,omitempty"`
|
||||||
IsActive bool `json:"is_active"`
|
SquareFootage *int `json:"square_footage,omitempty"`
|
||||||
CreatedAt string `json:"created_at"`
|
LotSize *float64 `json:"lot_size,omitempty"`
|
||||||
|
YearBuilt *int `json:"year_built,omitempty"`
|
||||||
|
Description string `json:"description"`
|
||||||
|
PurchaseDate *string `json:"purchase_date,omitempty"`
|
||||||
|
PurchasePrice *float64 `json:"purchase_price,omitempty"`
|
||||||
|
IsPrimary bool `json:"is_primary"`
|
||||||
|
IsActive bool `json:"is_active"`
|
||||||
|
CreatedAt string `json:"created_at"`
|
||||||
|
UpdatedAt string `json:"updated_at"`
|
||||||
}
|
}
|
||||||
|
|
||||||
// ResidenceDetailResponse includes more details for single residence view
|
// ResidenceDetailResponse includes more details for single residence view
|
||||||
@@ -105,20 +113,32 @@ type ResidenceDetailResponse struct {
|
|||||||
|
|
||||||
// TaskResponse represents a task in admin responses
|
// TaskResponse represents a task in admin responses
|
||||||
type TaskResponse struct {
|
type TaskResponse struct {
|
||||||
ID uint `json:"id"`
|
ID uint `json:"id"`
|
||||||
ResidenceID uint `json:"residence_id"`
|
ResidenceID uint `json:"residence_id"`
|
||||||
ResidenceName string `json:"residence_name"`
|
ResidenceName string `json:"residence_name"`
|
||||||
Title string `json:"title"`
|
CreatedByID uint `json:"created_by_id"`
|
||||||
Description string `json:"description"`
|
CreatedByName string `json:"created_by_name"`
|
||||||
CreatedByName string `json:"created_by_name"`
|
AssignedToID *uint `json:"assigned_to_id,omitempty"`
|
||||||
CategoryName *string `json:"category_name,omitempty"`
|
AssignedToName *string `json:"assigned_to_name,omitempty"`
|
||||||
PriorityName *string `json:"priority_name,omitempty"`
|
Title string `json:"title"`
|
||||||
StatusName *string `json:"status_name,omitempty"`
|
Description string `json:"description"`
|
||||||
DueDate *string `json:"due_date,omitempty"`
|
CategoryID *uint `json:"category_id,omitempty"`
|
||||||
EstimatedCost *float64 `json:"estimated_cost,omitempty"`
|
CategoryName *string `json:"category_name,omitempty"`
|
||||||
IsCancelled bool `json:"is_cancelled"`
|
PriorityID *uint `json:"priority_id,omitempty"`
|
||||||
IsArchived bool `json:"is_archived"`
|
PriorityName *string `json:"priority_name,omitempty"`
|
||||||
CreatedAt string `json:"created_at"`
|
StatusID *uint `json:"status_id,omitempty"`
|
||||||
|
StatusName *string `json:"status_name,omitempty"`
|
||||||
|
FrequencyID *uint `json:"frequency_id,omitempty"`
|
||||||
|
FrequencyName *string `json:"frequency_name,omitempty"`
|
||||||
|
DueDate *string `json:"due_date,omitempty"`
|
||||||
|
EstimatedCost *float64 `json:"estimated_cost,omitempty"`
|
||||||
|
ActualCost *float64 `json:"actual_cost,omitempty"`
|
||||||
|
ContractorID *uint `json:"contractor_id,omitempty"`
|
||||||
|
ParentTaskID *uint `json:"parent_task_id,omitempty"`
|
||||||
|
IsCancelled bool `json:"is_cancelled"`
|
||||||
|
IsArchived bool `json:"is_archived"`
|
||||||
|
CreatedAt string `json:"created_at"`
|
||||||
|
UpdatedAt string `json:"updated_at"`
|
||||||
}
|
}
|
||||||
|
|
||||||
// TaskDetailResponse includes more details for single task view
|
// TaskDetailResponse includes more details for single task view
|
||||||
@@ -133,17 +153,25 @@ type ContractorResponse struct {
|
|||||||
ID uint `json:"id"`
|
ID uint `json:"id"`
|
||||||
ResidenceID uint `json:"residence_id"`
|
ResidenceID uint `json:"residence_id"`
|
||||||
ResidenceName string `json:"residence_name"`
|
ResidenceName string `json:"residence_name"`
|
||||||
|
CreatedByID uint `json:"created_by_id"`
|
||||||
|
CreatedByName string `json:"created_by_name"`
|
||||||
Name string `json:"name"`
|
Name string `json:"name"`
|
||||||
Company string `json:"company"`
|
Company string `json:"company"`
|
||||||
Phone string `json:"phone"`
|
Phone string `json:"phone"`
|
||||||
Email string `json:"email"`
|
Email string `json:"email"`
|
||||||
Website string `json:"website"`
|
Website string `json:"website"`
|
||||||
|
Notes string `json:"notes"`
|
||||||
|
StreetAddress string `json:"street_address"`
|
||||||
City string `json:"city"`
|
City string `json:"city"`
|
||||||
|
StateProvince string `json:"state_province"`
|
||||||
|
PostalCode string `json:"postal_code"`
|
||||||
Rating *float64 `json:"rating,omitempty"`
|
Rating *float64 `json:"rating,omitempty"`
|
||||||
Specialties []string `json:"specialties,omitempty"`
|
Specialties []string `json:"specialties,omitempty"`
|
||||||
|
SpecialtyIDs []uint `json:"specialty_ids,omitempty"`
|
||||||
IsFavorite bool `json:"is_favorite"`
|
IsFavorite bool `json:"is_favorite"`
|
||||||
IsActive bool `json:"is_active"`
|
IsActive bool `json:"is_active"`
|
||||||
CreatedAt string `json:"created_at"`
|
CreatedAt string `json:"created_at"`
|
||||||
|
UpdatedAt string `json:"updated_at"`
|
||||||
}
|
}
|
||||||
|
|
||||||
// ContractorDetailResponse includes more details for single contractor view
|
// ContractorDetailResponse includes more details for single contractor view
|
||||||
@@ -161,20 +189,35 @@ type DocumentImageResponse struct {
|
|||||||
|
|
||||||
// DocumentResponse represents a document in admin responses
|
// DocumentResponse represents a document in admin responses
|
||||||
type DocumentResponse struct {
|
type DocumentResponse struct {
|
||||||
ID uint `json:"id"`
|
ID uint `json:"id"`
|
||||||
ResidenceID uint `json:"residence_id"`
|
ResidenceID uint `json:"residence_id"`
|
||||||
ResidenceName string `json:"residence_name"`
|
ResidenceName string `json:"residence_name"`
|
||||||
Title string `json:"title"`
|
CreatedByID uint `json:"created_by_id"`
|
||||||
Description string `json:"description"`
|
CreatedByName string `json:"created_by_name"`
|
||||||
DocumentType string `json:"document_type"`
|
Title string `json:"title"`
|
||||||
FileName string `json:"file_name"`
|
Description string `json:"description"`
|
||||||
FileURL string `json:"file_url"`
|
DocumentType string `json:"document_type"`
|
||||||
Vendor string `json:"vendor"`
|
FileURL string `json:"file_url"`
|
||||||
ExpiryDate *string `json:"expiry_date,omitempty"`
|
FileName string `json:"file_name"`
|
||||||
PurchaseDate *string `json:"purchase_date,omitempty"`
|
FileSize *int64 `json:"file_size,omitempty"`
|
||||||
IsActive bool `json:"is_active"`
|
MimeType string `json:"mime_type"`
|
||||||
Images []DocumentImageResponse `json:"images"`
|
PurchaseDate *string `json:"purchase_date,omitempty"`
|
||||||
CreatedAt string `json:"created_at"`
|
ExpiryDate *string `json:"expiry_date,omitempty"`
|
||||||
|
PurchasePrice *float64 `json:"purchase_price,omitempty"`
|
||||||
|
Vendor string `json:"vendor"`
|
||||||
|
SerialNumber string `json:"serial_number"`
|
||||||
|
ModelNumber string `json:"model_number"`
|
||||||
|
Provider string `json:"provider"`
|
||||||
|
ProviderContact string `json:"provider_contact"`
|
||||||
|
ClaimPhone string `json:"claim_phone"`
|
||||||
|
ClaimEmail string `json:"claim_email"`
|
||||||
|
ClaimWebsite string `json:"claim_website"`
|
||||||
|
Notes string `json:"notes"`
|
||||||
|
TaskID *uint `json:"task_id,omitempty"`
|
||||||
|
IsActive bool `json:"is_active"`
|
||||||
|
Images []DocumentImageResponse `json:"images"`
|
||||||
|
CreatedAt string `json:"created_at"`
|
||||||
|
UpdatedAt string `json:"updated_at"`
|
||||||
}
|
}
|
||||||
|
|
||||||
// DocumentDetailResponse includes more details for single document view
|
// DocumentDetailResponse includes more details for single document view
|
||||||
|
|||||||
@@ -142,6 +142,24 @@ func (h *AdminContractorHandler) Update(c *gin.Context) {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Verify residence if changing
|
||||||
|
if req.ResidenceID != nil {
|
||||||
|
var residence models.Residence
|
||||||
|
if err := h.db.First(&residence, *req.ResidenceID).Error; err != nil {
|
||||||
|
c.JSON(http.StatusBadRequest, gin.H{"error": "Residence not found"})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
contractor.ResidenceID = *req.ResidenceID
|
||||||
|
}
|
||||||
|
// Verify created_by if changing
|
||||||
|
if req.CreatedByID != nil {
|
||||||
|
var user models.User
|
||||||
|
if err := h.db.First(&user, *req.CreatedByID).Error; err != nil {
|
||||||
|
c.JSON(http.StatusBadRequest, gin.H{"error": "Created by user not found"})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
contractor.CreatedByID = *req.CreatedByID
|
||||||
|
}
|
||||||
if req.Name != nil {
|
if req.Name != nil {
|
||||||
contractor.Name = *req.Name
|
contractor.Name = *req.Name
|
||||||
}
|
}
|
||||||
@@ -160,6 +178,21 @@ func (h *AdminContractorHandler) Update(c *gin.Context) {
|
|||||||
if req.Notes != nil {
|
if req.Notes != nil {
|
||||||
contractor.Notes = *req.Notes
|
contractor.Notes = *req.Notes
|
||||||
}
|
}
|
||||||
|
if req.StreetAddress != nil {
|
||||||
|
contractor.StreetAddress = *req.StreetAddress
|
||||||
|
}
|
||||||
|
if req.City != nil {
|
||||||
|
contractor.City = *req.City
|
||||||
|
}
|
||||||
|
if req.StateProvince != nil {
|
||||||
|
contractor.StateProvince = *req.StateProvince
|
||||||
|
}
|
||||||
|
if req.PostalCode != nil {
|
||||||
|
contractor.PostalCode = *req.PostalCode
|
||||||
|
}
|
||||||
|
if req.Rating != nil {
|
||||||
|
contractor.Rating = req.Rating
|
||||||
|
}
|
||||||
if req.IsFavorite != nil {
|
if req.IsFavorite != nil {
|
||||||
contractor.IsFavorite = *req.IsFavorite
|
contractor.IsFavorite = *req.IsFavorite
|
||||||
}
|
}
|
||||||
@@ -167,6 +200,18 @@ func (h *AdminContractorHandler) Update(c *gin.Context) {
|
|||||||
contractor.IsActive = *req.IsActive
|
contractor.IsActive = *req.IsActive
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Update specialties if provided
|
||||||
|
if req.SpecialtyIDs != nil {
|
||||||
|
// Clear existing specialties
|
||||||
|
h.db.Model(&contractor).Association("Specialties").Clear()
|
||||||
|
// Add new specialties
|
||||||
|
if len(req.SpecialtyIDs) > 0 {
|
||||||
|
var specialties []models.ContractorSpecialty
|
||||||
|
h.db.Where("id IN ?", req.SpecialtyIDs).Find(&specialties)
|
||||||
|
h.db.Model(&contractor).Association("Specialties").Append(specialties)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
if err := h.db.Save(&contractor).Error; err != nil {
|
if err := h.db.Save(&contractor).Error; err != nil {
|
||||||
c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to update contractor"})
|
c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to update contractor"})
|
||||||
return
|
return
|
||||||
@@ -278,29 +323,39 @@ func (h *AdminContractorHandler) BulkDelete(c *gin.Context) {
|
|||||||
|
|
||||||
func (h *AdminContractorHandler) toContractorResponse(contractor *models.Contractor) dto.ContractorResponse {
|
func (h *AdminContractorHandler) toContractorResponse(contractor *models.Contractor) dto.ContractorResponse {
|
||||||
response := dto.ContractorResponse{
|
response := dto.ContractorResponse{
|
||||||
ID: contractor.ID,
|
ID: contractor.ID,
|
||||||
ResidenceID: contractor.ResidenceID,
|
ResidenceID: contractor.ResidenceID,
|
||||||
Name: contractor.Name,
|
CreatedByID: contractor.CreatedByID,
|
||||||
Company: contractor.Company,
|
Name: contractor.Name,
|
||||||
Phone: contractor.Phone,
|
Company: contractor.Company,
|
||||||
Email: contractor.Email,
|
Phone: contractor.Phone,
|
||||||
Website: contractor.Website,
|
Email: contractor.Email,
|
||||||
City: contractor.City,
|
Website: contractor.Website,
|
||||||
IsFavorite: contractor.IsFavorite,
|
Notes: contractor.Notes,
|
||||||
IsActive: contractor.IsActive,
|
StreetAddress: contractor.StreetAddress,
|
||||||
CreatedAt: contractor.CreatedAt.Format("2006-01-02T15:04:05Z"),
|
City: contractor.City,
|
||||||
|
StateProvince: contractor.StateProvince,
|
||||||
|
PostalCode: contractor.PostalCode,
|
||||||
|
IsFavorite: contractor.IsFavorite,
|
||||||
|
IsActive: contractor.IsActive,
|
||||||
|
CreatedAt: contractor.CreatedAt.Format("2006-01-02T15:04:05Z"),
|
||||||
|
UpdatedAt: contractor.UpdatedAt.Format("2006-01-02T15:04:05Z"),
|
||||||
}
|
}
|
||||||
|
|
||||||
if contractor.Residence.ID != 0 {
|
if contractor.Residence.ID != 0 {
|
||||||
response.ResidenceName = contractor.Residence.Name
|
response.ResidenceName = contractor.Residence.Name
|
||||||
}
|
}
|
||||||
|
if contractor.CreatedBy.ID != 0 {
|
||||||
|
response.CreatedByName = contractor.CreatedBy.Username
|
||||||
|
}
|
||||||
if contractor.Rating != nil {
|
if contractor.Rating != nil {
|
||||||
response.Rating = contractor.Rating
|
response.Rating = contractor.Rating
|
||||||
}
|
}
|
||||||
|
|
||||||
// Add specialties
|
// Add specialties (names and IDs)
|
||||||
for _, s := range contractor.Specialties {
|
for _, s := range contractor.Specialties {
|
||||||
response.Specialties = append(response.Specialties, s.Name)
|
response.Specialties = append(response.Specialties, s.Name)
|
||||||
|
response.SpecialtyIDs = append(response.SpecialtyIDs, s.ID)
|
||||||
}
|
}
|
||||||
|
|
||||||
return response
|
return response
|
||||||
|
|||||||
@@ -144,12 +144,59 @@ func (h *AdminDocumentHandler) Update(c *gin.Context) {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Verify residence if changing
|
||||||
|
if req.ResidenceID != nil {
|
||||||
|
var residence models.Residence
|
||||||
|
if err := h.db.First(&residence, *req.ResidenceID).Error; err != nil {
|
||||||
|
c.JSON(http.StatusBadRequest, gin.H{"error": "Residence not found"})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
document.ResidenceID = *req.ResidenceID
|
||||||
|
}
|
||||||
|
// Verify created_by if changing
|
||||||
|
if req.CreatedByID != nil {
|
||||||
|
var user models.User
|
||||||
|
if err := h.db.First(&user, *req.CreatedByID).Error; err != nil {
|
||||||
|
c.JSON(http.StatusBadRequest, gin.H{"error": "Created by user not found"})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
document.CreatedByID = *req.CreatedByID
|
||||||
|
}
|
||||||
if req.Title != nil {
|
if req.Title != nil {
|
||||||
document.Title = *req.Title
|
document.Title = *req.Title
|
||||||
}
|
}
|
||||||
if req.Description != nil {
|
if req.Description != nil {
|
||||||
document.Description = *req.Description
|
document.Description = *req.Description
|
||||||
}
|
}
|
||||||
|
if req.DocumentType != nil {
|
||||||
|
document.DocumentType = models.DocumentType(*req.DocumentType)
|
||||||
|
}
|
||||||
|
if req.FileURL != nil {
|
||||||
|
document.FileURL = *req.FileURL
|
||||||
|
}
|
||||||
|
if req.FileName != nil {
|
||||||
|
document.FileName = *req.FileName
|
||||||
|
}
|
||||||
|
if req.FileSize != nil {
|
||||||
|
document.FileSize = req.FileSize
|
||||||
|
}
|
||||||
|
if req.MimeType != nil {
|
||||||
|
document.MimeType = *req.MimeType
|
||||||
|
}
|
||||||
|
if req.PurchaseDate != nil {
|
||||||
|
if purchaseDate, err := time.Parse("2006-01-02", *req.PurchaseDate); err == nil {
|
||||||
|
document.PurchaseDate = &purchaseDate
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if req.ExpiryDate != nil {
|
||||||
|
if expiryDate, err := time.Parse("2006-01-02", *req.ExpiryDate); err == nil {
|
||||||
|
document.ExpiryDate = &expiryDate
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if req.PurchasePrice != nil {
|
||||||
|
d := decimal.NewFromFloat(*req.PurchasePrice)
|
||||||
|
document.PurchasePrice = &d
|
||||||
|
}
|
||||||
if req.Vendor != nil {
|
if req.Vendor != nil {
|
||||||
document.Vendor = *req.Vendor
|
document.Vendor = *req.Vendor
|
||||||
}
|
}
|
||||||
@@ -159,6 +206,27 @@ func (h *AdminDocumentHandler) Update(c *gin.Context) {
|
|||||||
if req.ModelNumber != nil {
|
if req.ModelNumber != nil {
|
||||||
document.ModelNumber = *req.ModelNumber
|
document.ModelNumber = *req.ModelNumber
|
||||||
}
|
}
|
||||||
|
if req.Provider != nil {
|
||||||
|
document.Provider = *req.Provider
|
||||||
|
}
|
||||||
|
if req.ProviderContact != nil {
|
||||||
|
document.ProviderContact = *req.ProviderContact
|
||||||
|
}
|
||||||
|
if req.ClaimPhone != nil {
|
||||||
|
document.ClaimPhone = *req.ClaimPhone
|
||||||
|
}
|
||||||
|
if req.ClaimEmail != nil {
|
||||||
|
document.ClaimEmail = *req.ClaimEmail
|
||||||
|
}
|
||||||
|
if req.ClaimWebsite != nil {
|
||||||
|
document.ClaimWebsite = *req.ClaimWebsite
|
||||||
|
}
|
||||||
|
if req.Notes != nil {
|
||||||
|
document.Notes = *req.Notes
|
||||||
|
}
|
||||||
|
if req.TaskID != nil {
|
||||||
|
document.TaskID = req.TaskID
|
||||||
|
}
|
||||||
if req.IsActive != nil {
|
if req.IsActive != nil {
|
||||||
document.IsActive = *req.IsActive
|
document.IsActive = *req.IsActive
|
||||||
}
|
}
|
||||||
@@ -289,22 +357,38 @@ func (h *AdminDocumentHandler) BulkDelete(c *gin.Context) {
|
|||||||
|
|
||||||
func (h *AdminDocumentHandler) toDocumentResponse(doc *models.Document) dto.DocumentResponse {
|
func (h *AdminDocumentHandler) toDocumentResponse(doc *models.Document) dto.DocumentResponse {
|
||||||
response := dto.DocumentResponse{
|
response := dto.DocumentResponse{
|
||||||
ID: doc.ID,
|
ID: doc.ID,
|
||||||
ResidenceID: doc.ResidenceID,
|
ResidenceID: doc.ResidenceID,
|
||||||
Title: doc.Title,
|
CreatedByID: doc.CreatedByID,
|
||||||
Description: doc.Description,
|
Title: doc.Title,
|
||||||
DocumentType: string(doc.DocumentType),
|
Description: doc.Description,
|
||||||
FileName: doc.FileName,
|
DocumentType: string(doc.DocumentType),
|
||||||
FileURL: doc.FileURL,
|
FileURL: doc.FileURL,
|
||||||
Vendor: doc.Vendor,
|
FileName: doc.FileName,
|
||||||
IsActive: doc.IsActive,
|
FileSize: doc.FileSize,
|
||||||
Images: make([]dto.DocumentImageResponse, 0),
|
MimeType: doc.MimeType,
|
||||||
CreatedAt: doc.CreatedAt.Format("2006-01-02T15:04:05Z"),
|
Vendor: doc.Vendor,
|
||||||
|
SerialNumber: doc.SerialNumber,
|
||||||
|
ModelNumber: doc.ModelNumber,
|
||||||
|
Provider: doc.Provider,
|
||||||
|
ProviderContact: doc.ProviderContact,
|
||||||
|
ClaimPhone: doc.ClaimPhone,
|
||||||
|
ClaimEmail: doc.ClaimEmail,
|
||||||
|
ClaimWebsite: doc.ClaimWebsite,
|
||||||
|
Notes: doc.Notes,
|
||||||
|
TaskID: doc.TaskID,
|
||||||
|
IsActive: doc.IsActive,
|
||||||
|
Images: make([]dto.DocumentImageResponse, 0),
|
||||||
|
CreatedAt: doc.CreatedAt.Format("2006-01-02T15:04:05Z"),
|
||||||
|
UpdatedAt: doc.UpdatedAt.Format("2006-01-02T15:04:05Z"),
|
||||||
}
|
}
|
||||||
|
|
||||||
if doc.Residence.ID != 0 {
|
if doc.Residence.ID != 0 {
|
||||||
response.ResidenceName = doc.Residence.Name
|
response.ResidenceName = doc.Residence.Name
|
||||||
}
|
}
|
||||||
|
if doc.CreatedBy.ID != 0 {
|
||||||
|
response.CreatedByName = doc.CreatedBy.Username
|
||||||
|
}
|
||||||
if doc.ExpiryDate != nil {
|
if doc.ExpiryDate != nil {
|
||||||
expiryDate := doc.ExpiryDate.Format("2006-01-02")
|
expiryDate := doc.ExpiryDate.Format("2006-01-02")
|
||||||
response.ExpiryDate = &expiryDate
|
response.ExpiryDate = &expiryDate
|
||||||
@@ -313,6 +397,10 @@ func (h *AdminDocumentHandler) toDocumentResponse(doc *models.Document) dto.Docu
|
|||||||
purchaseDate := doc.PurchaseDate.Format("2006-01-02")
|
purchaseDate := doc.PurchaseDate.Format("2006-01-02")
|
||||||
response.PurchaseDate = &purchaseDate
|
response.PurchaseDate = &purchaseDate
|
||||||
}
|
}
|
||||||
|
if doc.PurchasePrice != nil {
|
||||||
|
price, _ := doc.PurchasePrice.Float64()
|
||||||
|
response.PurchasePrice = &price
|
||||||
|
}
|
||||||
|
|
||||||
// Convert images
|
// Convert images
|
||||||
for _, img := range doc.Images {
|
for _, img := range doc.Images {
|
||||||
|
|||||||
@@ -3,6 +3,7 @@ package handlers
|
|||||||
import (
|
import (
|
||||||
"net/http"
|
"net/http"
|
||||||
"strconv"
|
"strconv"
|
||||||
|
"time"
|
||||||
|
|
||||||
"github.com/gin-gonic/gin"
|
"github.com/gin-gonic/gin"
|
||||||
"github.com/shopspring/decimal"
|
"github.com/shopspring/decimal"
|
||||||
@@ -153,12 +154,27 @@ func (h *AdminResidenceHandler) Update(c *gin.Context) {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if req.OwnerID != nil {
|
||||||
|
// Verify owner exists
|
||||||
|
var owner models.User
|
||||||
|
if err := h.db.First(&owner, *req.OwnerID).Error; err != nil {
|
||||||
|
c.JSON(http.StatusBadRequest, gin.H{"error": "Owner not found"})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
residence.OwnerID = *req.OwnerID
|
||||||
|
}
|
||||||
if req.Name != nil {
|
if req.Name != nil {
|
||||||
residence.Name = *req.Name
|
residence.Name = *req.Name
|
||||||
}
|
}
|
||||||
|
if req.PropertyTypeID != nil {
|
||||||
|
residence.PropertyTypeID = req.PropertyTypeID
|
||||||
|
}
|
||||||
if req.StreetAddress != nil {
|
if req.StreetAddress != nil {
|
||||||
residence.StreetAddress = *req.StreetAddress
|
residence.StreetAddress = *req.StreetAddress
|
||||||
}
|
}
|
||||||
|
if req.ApartmentUnit != nil {
|
||||||
|
residence.ApartmentUnit = *req.ApartmentUnit
|
||||||
|
}
|
||||||
if req.City != nil {
|
if req.City != nil {
|
||||||
residence.City = *req.City
|
residence.City = *req.City
|
||||||
}
|
}
|
||||||
@@ -171,6 +187,35 @@ func (h *AdminResidenceHandler) Update(c *gin.Context) {
|
|||||||
if req.Country != nil {
|
if req.Country != nil {
|
||||||
residence.Country = *req.Country
|
residence.Country = *req.Country
|
||||||
}
|
}
|
||||||
|
if req.Bedrooms != nil {
|
||||||
|
residence.Bedrooms = req.Bedrooms
|
||||||
|
}
|
||||||
|
if req.Bathrooms != nil {
|
||||||
|
d := decimal.NewFromFloat(*req.Bathrooms)
|
||||||
|
residence.Bathrooms = &d
|
||||||
|
}
|
||||||
|
if req.SquareFootage != nil {
|
||||||
|
residence.SquareFootage = req.SquareFootage
|
||||||
|
}
|
||||||
|
if req.LotSize != nil {
|
||||||
|
d := decimal.NewFromFloat(*req.LotSize)
|
||||||
|
residence.LotSize = &d
|
||||||
|
}
|
||||||
|
if req.YearBuilt != nil {
|
||||||
|
residence.YearBuilt = req.YearBuilt
|
||||||
|
}
|
||||||
|
if req.Description != nil {
|
||||||
|
residence.Description = *req.Description
|
||||||
|
}
|
||||||
|
if req.PurchaseDate != nil {
|
||||||
|
if purchaseDate, err := time.Parse("2006-01-02", *req.PurchaseDate); err == nil {
|
||||||
|
residence.PurchaseDate = &purchaseDate
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if req.PurchasePrice != nil {
|
||||||
|
d := decimal.NewFromFloat(*req.PurchasePrice)
|
||||||
|
residence.PurchasePrice = &d
|
||||||
|
}
|
||||||
if req.IsActive != nil {
|
if req.IsActive != nil {
|
||||||
residence.IsActive = *req.IsActive
|
residence.IsActive = *req.IsActive
|
||||||
}
|
}
|
||||||
@@ -287,17 +332,21 @@ func (h *AdminResidenceHandler) BulkDelete(c *gin.Context) {
|
|||||||
|
|
||||||
func (h *AdminResidenceHandler) toResidenceResponse(res *models.Residence) dto.ResidenceResponse {
|
func (h *AdminResidenceHandler) toResidenceResponse(res *models.Residence) dto.ResidenceResponse {
|
||||||
response := dto.ResidenceResponse{
|
response := dto.ResidenceResponse{
|
||||||
ID: res.ID,
|
ID: res.ID,
|
||||||
Name: res.Name,
|
Name: res.Name,
|
||||||
OwnerID: res.OwnerID,
|
OwnerID: res.OwnerID,
|
||||||
StreetAddress: res.StreetAddress,
|
PropertyTypeID: res.PropertyTypeID,
|
||||||
City: res.City,
|
StreetAddress: res.StreetAddress,
|
||||||
StateProvince: res.StateProvince,
|
ApartmentUnit: res.ApartmentUnit,
|
||||||
PostalCode: res.PostalCode,
|
City: res.City,
|
||||||
Country: res.Country,
|
StateProvince: res.StateProvince,
|
||||||
IsPrimary: res.IsPrimary,
|
PostalCode: res.PostalCode,
|
||||||
IsActive: res.IsActive,
|
Country: res.Country,
|
||||||
CreatedAt: res.CreatedAt.Format("2006-01-02T15:04:05Z"),
|
Description: res.Description,
|
||||||
|
IsPrimary: res.IsPrimary,
|
||||||
|
IsActive: res.IsActive,
|
||||||
|
CreatedAt: res.CreatedAt.Format("2006-01-02T15:04:05Z"),
|
||||||
|
UpdatedAt: res.UpdatedAt.Format("2006-01-02T15:04:05Z"),
|
||||||
}
|
}
|
||||||
|
|
||||||
if res.Owner.ID != 0 {
|
if res.Owner.ID != 0 {
|
||||||
@@ -316,6 +365,21 @@ func (h *AdminResidenceHandler) toResidenceResponse(res *models.Residence) dto.R
|
|||||||
if res.SquareFootage != nil {
|
if res.SquareFootage != nil {
|
||||||
response.SquareFootage = res.SquareFootage
|
response.SquareFootage = res.SquareFootage
|
||||||
}
|
}
|
||||||
|
if res.LotSize != nil {
|
||||||
|
f, _ := res.LotSize.Float64()
|
||||||
|
response.LotSize = &f
|
||||||
|
}
|
||||||
|
if res.YearBuilt != nil {
|
||||||
|
response.YearBuilt = res.YearBuilt
|
||||||
|
}
|
||||||
|
if res.PurchaseDate != nil {
|
||||||
|
purchaseDate := res.PurchaseDate.Format("2006-01-02")
|
||||||
|
response.PurchaseDate = &purchaseDate
|
||||||
|
}
|
||||||
|
if res.PurchasePrice != nil {
|
||||||
|
f, _ := res.PurchasePrice.Float64()
|
||||||
|
response.PurchasePrice = &f
|
||||||
|
}
|
||||||
|
|
||||||
return response
|
return response
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -215,3 +215,270 @@ func isCommentOnly(stmt string) bool {
|
|||||||
}
|
}
|
||||||
return true
|
return true
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ClearAllDataResponse represents the response after clearing data
|
||||||
|
type ClearAllDataResponse struct {
|
||||||
|
Message string `json:"message"`
|
||||||
|
UsersDeleted int64 `json:"users_deleted"`
|
||||||
|
PreservedUsers int64 `json:"preserved_users"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// ClearAllData handles POST /api/admin/settings/clear-all-data
|
||||||
|
// This clears all data except super admin accounts and lookup tables
|
||||||
|
func (h *AdminSettingsHandler) ClearAllData(c *gin.Context) {
|
||||||
|
// Start a transaction
|
||||||
|
tx := h.db.Begin()
|
||||||
|
if tx.Error != nil {
|
||||||
|
c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to start transaction"})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
defer func() {
|
||||||
|
if r := recover(); r != nil {
|
||||||
|
tx.Rollback()
|
||||||
|
}
|
||||||
|
}()
|
||||||
|
|
||||||
|
// Get IDs of users to preserve (superusers)
|
||||||
|
var preservedUserIDs []uint
|
||||||
|
if err := tx.Model(&models.User{}).
|
||||||
|
Where("is_superuser = ?", true).
|
||||||
|
Pluck("id", &preservedUserIDs).Error; err != nil {
|
||||||
|
tx.Rollback()
|
||||||
|
c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to get superuser IDs"})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// Count users that will be deleted
|
||||||
|
var usersToDelete int64
|
||||||
|
if err := tx.Model(&models.User{}).
|
||||||
|
Where("is_superuser = ?", false).
|
||||||
|
Count(&usersToDelete).Error; err != nil {
|
||||||
|
tx.Rollback()
|
||||||
|
c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to count users"})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// Delete in order to respect foreign key constraints
|
||||||
|
// Order matters: delete from child tables first
|
||||||
|
|
||||||
|
// 1. Delete task completion images
|
||||||
|
if err := tx.Exec("DELETE FROM task_taskcompletionimage").Error; err != nil {
|
||||||
|
tx.Rollback()
|
||||||
|
c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to delete task completion images: " + err.Error()})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// 2. Delete task completions
|
||||||
|
if err := tx.Exec("DELETE FROM task_taskcompletion").Error; err != nil {
|
||||||
|
tx.Rollback()
|
||||||
|
c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to delete task completions: " + err.Error()})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// 3. Delete document images
|
||||||
|
if err := tx.Exec("DELETE FROM task_documentimage").Error; err != nil {
|
||||||
|
tx.Rollback()
|
||||||
|
c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to delete document images: " + err.Error()})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// 4. Delete documents
|
||||||
|
if err := tx.Exec("DELETE FROM task_document").Error; err != nil {
|
||||||
|
tx.Rollback()
|
||||||
|
c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to delete documents: " + err.Error()})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// 5. Delete tasks (must be before contractors since tasks reference contractors)
|
||||||
|
if err := tx.Exec("DELETE FROM task_task").Error; err != nil {
|
||||||
|
tx.Rollback()
|
||||||
|
c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to delete tasks: " + err.Error()})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// 6. Delete contractor specialties (many-to-many)
|
||||||
|
if err := tx.Exec("DELETE FROM task_contractor_specialties").Error; err != nil {
|
||||||
|
tx.Rollback()
|
||||||
|
c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to delete contractor specialties: " + err.Error()})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// 7. Delete contractors
|
||||||
|
if err := tx.Exec("DELETE FROM task_contractor").Error; err != nil {
|
||||||
|
tx.Rollback()
|
||||||
|
c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to delete contractors: " + err.Error()})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// 8. Delete residence_users (many-to-many for shared residences)
|
||||||
|
if err := tx.Exec("DELETE FROM residence_residence_users").Error; err != nil {
|
||||||
|
tx.Rollback()
|
||||||
|
c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to delete residence users: " + err.Error()})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// 9. Delete residences
|
||||||
|
if err := tx.Exec("DELETE FROM residence_residence").Error; err != nil {
|
||||||
|
tx.Rollback()
|
||||||
|
c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to delete residences: " + err.Error()})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// 10. Delete notifications for non-superusers
|
||||||
|
if len(preservedUserIDs) > 0 {
|
||||||
|
if err := tx.Exec("DELETE FROM notifications_notification WHERE user_id NOT IN (?)", preservedUserIDs).Error; err != nil {
|
||||||
|
tx.Rollback()
|
||||||
|
c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to delete notifications: " + err.Error()})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
if err := tx.Exec("DELETE FROM notifications_notification").Error; err != nil {
|
||||||
|
tx.Rollback()
|
||||||
|
c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to delete notifications: " + err.Error()})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 11. Delete push devices for non-superusers (both APNS and GCM)
|
||||||
|
if len(preservedUserIDs) > 0 {
|
||||||
|
if err := tx.Exec("DELETE FROM push_notifications_apnsdevice WHERE user_id NOT IN (?)", preservedUserIDs).Error; err != nil {
|
||||||
|
tx.Rollback()
|
||||||
|
c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to delete APNS devices: " + err.Error()})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if err := tx.Exec("DELETE FROM push_notifications_gcmdevice WHERE user_id NOT IN (?)", preservedUserIDs).Error; err != nil {
|
||||||
|
tx.Rollback()
|
||||||
|
c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to delete GCM devices: " + err.Error()})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
if err := tx.Exec("DELETE FROM push_notifications_apnsdevice").Error; err != nil {
|
||||||
|
tx.Rollback()
|
||||||
|
c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to delete APNS devices: " + err.Error()})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if err := tx.Exec("DELETE FROM push_notifications_gcmdevice").Error; err != nil {
|
||||||
|
tx.Rollback()
|
||||||
|
c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to delete GCM devices: " + err.Error()})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 12. Delete notification preferences for non-superusers
|
||||||
|
if len(preservedUserIDs) > 0 {
|
||||||
|
if err := tx.Exec("DELETE FROM notifications_notificationpreference WHERE user_id NOT IN (?)", preservedUserIDs).Error; err != nil {
|
||||||
|
tx.Rollback()
|
||||||
|
c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to delete notification preferences: " + err.Error()})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
if err := tx.Exec("DELETE FROM notifications_notificationpreference").Error; err != nil {
|
||||||
|
tx.Rollback()
|
||||||
|
c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to delete notification preferences: " + err.Error()})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 13. Delete user subscriptions for non-superusers
|
||||||
|
if len(preservedUserIDs) > 0 {
|
||||||
|
if err := tx.Exec("DELETE FROM subscription_usersubscription WHERE user_id NOT IN (?)", preservedUserIDs).Error; err != nil {
|
||||||
|
tx.Rollback()
|
||||||
|
c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to delete subscriptions: " + err.Error()})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
if err := tx.Exec("DELETE FROM subscription_usersubscription").Error; err != nil {
|
||||||
|
tx.Rollback()
|
||||||
|
c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to delete subscriptions: " + err.Error()})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 14. Delete password reset codes for non-superusers
|
||||||
|
if len(preservedUserIDs) > 0 {
|
||||||
|
if err := tx.Exec("DELETE FROM user_passwordresetcode WHERE user_id NOT IN (?)", preservedUserIDs).Error; err != nil {
|
||||||
|
tx.Rollback()
|
||||||
|
c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to delete password reset codes: " + err.Error()})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
if err := tx.Exec("DELETE FROM user_passwordresetcode").Error; err != nil {
|
||||||
|
tx.Rollback()
|
||||||
|
c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to delete password reset codes: " + err.Error()})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 15. Delete confirmation codes for non-superusers
|
||||||
|
if len(preservedUserIDs) > 0 {
|
||||||
|
if err := tx.Exec("DELETE FROM user_confirmationcode WHERE user_id NOT IN (?)", preservedUserIDs).Error; err != nil {
|
||||||
|
tx.Rollback()
|
||||||
|
c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to delete confirmation codes: " + err.Error()})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
if err := tx.Exec("DELETE FROM user_confirmationcode").Error; err != nil {
|
||||||
|
tx.Rollback()
|
||||||
|
c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to delete confirmation codes: " + err.Error()})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 16. Delete auth tokens for non-superusers
|
||||||
|
if len(preservedUserIDs) > 0 {
|
||||||
|
if err := tx.Exec("DELETE FROM user_authtoken WHERE user_id NOT IN (?)", preservedUserIDs).Error; err != nil {
|
||||||
|
tx.Rollback()
|
||||||
|
c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to delete auth tokens: " + err.Error()})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
if err := tx.Exec("DELETE FROM user_authtoken").Error; err != nil {
|
||||||
|
tx.Rollback()
|
||||||
|
c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to delete auth tokens: " + err.Error()})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 17. Delete user profiles for non-superusers
|
||||||
|
if len(preservedUserIDs) > 0 {
|
||||||
|
if err := tx.Exec("DELETE FROM user_userprofile WHERE user_id NOT IN (?)", preservedUserIDs).Error; err != nil {
|
||||||
|
tx.Rollback()
|
||||||
|
c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to delete user profiles: " + err.Error()})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
if err := tx.Exec("DELETE FROM user_userprofile").Error; err != nil {
|
||||||
|
tx.Rollback()
|
||||||
|
c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to delete user profiles: " + err.Error()})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 18. Finally, delete non-superuser users
|
||||||
|
if len(preservedUserIDs) > 0 {
|
||||||
|
if err := tx.Exec("DELETE FROM auth_user WHERE id NOT IN (?)", preservedUserIDs).Error; err != nil {
|
||||||
|
tx.Rollback()
|
||||||
|
c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to delete users: " + err.Error()})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
if err := tx.Exec("DELETE FROM auth_user WHERE is_superuser = false").Error; err != nil {
|
||||||
|
tx.Rollback()
|
||||||
|
c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to delete users: " + err.Error()})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Commit the transaction
|
||||||
|
if err := tx.Commit().Error; err != nil {
|
||||||
|
c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to commit transaction: " + err.Error()})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
c.JSON(http.StatusOK, ClearAllDataResponse{
|
||||||
|
Message: "All data cleared successfully (superadmin accounts preserved)",
|
||||||
|
UsersDeleted: usersToDelete,
|
||||||
|
PreservedUsers: int64(len(preservedUserIDs)),
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|||||||
@@ -162,6 +162,33 @@ func (h *AdminTaskHandler) Update(c *gin.Context) {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Verify residence if changing
|
||||||
|
if req.ResidenceID != nil {
|
||||||
|
var residence models.Residence
|
||||||
|
if err := h.db.First(&residence, *req.ResidenceID).Error; err != nil {
|
||||||
|
c.JSON(http.StatusBadRequest, gin.H{"error": "Residence not found"})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
task.ResidenceID = *req.ResidenceID
|
||||||
|
}
|
||||||
|
// Verify created_by if changing
|
||||||
|
if req.CreatedByID != nil {
|
||||||
|
var user models.User
|
||||||
|
if err := h.db.First(&user, *req.CreatedByID).Error; err != nil {
|
||||||
|
c.JSON(http.StatusBadRequest, gin.H{"error": "Created by user not found"})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
task.CreatedByID = *req.CreatedByID
|
||||||
|
}
|
||||||
|
// Verify assigned_to if changing
|
||||||
|
if req.AssignedToID != nil {
|
||||||
|
var user models.User
|
||||||
|
if err := h.db.First(&user, *req.AssignedToID).Error; err != nil {
|
||||||
|
c.JSON(http.StatusBadRequest, gin.H{"error": "Assigned to user not found"})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
task.AssignedToID = req.AssignedToID
|
||||||
|
}
|
||||||
if req.Title != nil {
|
if req.Title != nil {
|
||||||
task.Title = *req.Title
|
task.Title = *req.Title
|
||||||
}
|
}
|
||||||
@@ -177,6 +204,28 @@ func (h *AdminTaskHandler) Update(c *gin.Context) {
|
|||||||
if req.StatusID != nil {
|
if req.StatusID != nil {
|
||||||
task.StatusID = req.StatusID
|
task.StatusID = req.StatusID
|
||||||
}
|
}
|
||||||
|
if req.FrequencyID != nil {
|
||||||
|
task.FrequencyID = req.FrequencyID
|
||||||
|
}
|
||||||
|
if req.DueDate != nil {
|
||||||
|
if dueDate, err := time.Parse("2006-01-02", *req.DueDate); err == nil {
|
||||||
|
task.DueDate = &dueDate
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if req.EstimatedCost != nil {
|
||||||
|
d := decimal.NewFromFloat(*req.EstimatedCost)
|
||||||
|
task.EstimatedCost = &d
|
||||||
|
}
|
||||||
|
if req.ActualCost != nil {
|
||||||
|
d := decimal.NewFromFloat(*req.ActualCost)
|
||||||
|
task.ActualCost = &d
|
||||||
|
}
|
||||||
|
if req.ContractorID != nil {
|
||||||
|
task.ContractorID = req.ContractorID
|
||||||
|
}
|
||||||
|
if req.ParentTaskID != nil {
|
||||||
|
task.ParentTaskID = req.ParentTaskID
|
||||||
|
}
|
||||||
if req.IsCancelled != nil {
|
if req.IsCancelled != nil {
|
||||||
task.IsCancelled = *req.IsCancelled
|
task.IsCancelled = *req.IsCancelled
|
||||||
}
|
}
|
||||||
@@ -303,11 +352,19 @@ func (h *AdminTaskHandler) toTaskResponse(task *models.Task) dto.TaskResponse {
|
|||||||
response := dto.TaskResponse{
|
response := dto.TaskResponse{
|
||||||
ID: task.ID,
|
ID: task.ID,
|
||||||
ResidenceID: task.ResidenceID,
|
ResidenceID: task.ResidenceID,
|
||||||
|
CreatedByID: task.CreatedByID,
|
||||||
Title: task.Title,
|
Title: task.Title,
|
||||||
Description: task.Description,
|
Description: task.Description,
|
||||||
|
CategoryID: task.CategoryID,
|
||||||
|
PriorityID: task.PriorityID,
|
||||||
|
StatusID: task.StatusID,
|
||||||
|
FrequencyID: task.FrequencyID,
|
||||||
|
ContractorID: task.ContractorID,
|
||||||
|
ParentTaskID: task.ParentTaskID,
|
||||||
IsCancelled: task.IsCancelled,
|
IsCancelled: task.IsCancelled,
|
||||||
IsArchived: task.IsArchived,
|
IsArchived: task.IsArchived,
|
||||||
CreatedAt: task.CreatedAt.Format("2006-01-02T15:04:05Z"),
|
CreatedAt: task.CreatedAt.Format("2006-01-02T15:04:05Z"),
|
||||||
|
UpdatedAt: task.UpdatedAt.Format("2006-01-02T15:04:05Z"),
|
||||||
}
|
}
|
||||||
|
|
||||||
if task.Residence.ID != 0 {
|
if task.Residence.ID != 0 {
|
||||||
@@ -316,6 +373,12 @@ func (h *AdminTaskHandler) toTaskResponse(task *models.Task) dto.TaskResponse {
|
|||||||
if task.CreatedBy.ID != 0 {
|
if task.CreatedBy.ID != 0 {
|
||||||
response.CreatedByName = task.CreatedBy.Username
|
response.CreatedByName = task.CreatedBy.Username
|
||||||
}
|
}
|
||||||
|
if task.AssignedToID != nil {
|
||||||
|
response.AssignedToID = task.AssignedToID
|
||||||
|
if task.AssignedTo != nil {
|
||||||
|
response.AssignedToName = &task.AssignedTo.Username
|
||||||
|
}
|
||||||
|
}
|
||||||
if task.Category != nil {
|
if task.Category != nil {
|
||||||
response.CategoryName = &task.Category.Name
|
response.CategoryName = &task.Category.Name
|
||||||
}
|
}
|
||||||
@@ -325,6 +388,9 @@ func (h *AdminTaskHandler) toTaskResponse(task *models.Task) dto.TaskResponse {
|
|||||||
if task.Status != nil {
|
if task.Status != nil {
|
||||||
response.StatusName = &task.Status.Name
|
response.StatusName = &task.Status.Name
|
||||||
}
|
}
|
||||||
|
if task.Frequency != nil {
|
||||||
|
response.FrequencyName = &task.Frequency.Name
|
||||||
|
}
|
||||||
if task.DueDate != nil {
|
if task.DueDate != nil {
|
||||||
dueDate := task.DueDate.Format("2006-01-02")
|
dueDate := task.DueDate.Format("2006-01-02")
|
||||||
response.DueDate = &dueDate
|
response.DueDate = &dueDate
|
||||||
@@ -333,6 +399,10 @@ func (h *AdminTaskHandler) toTaskResponse(task *models.Task) dto.TaskResponse {
|
|||||||
cost, _ := task.EstimatedCost.Float64()
|
cost, _ := task.EstimatedCost.Float64()
|
||||||
response.EstimatedCost = &cost
|
response.EstimatedCost = &cost
|
||||||
}
|
}
|
||||||
|
if task.ActualCost != nil {
|
||||||
|
cost, _ := task.ActualCost.Float64()
|
||||||
|
response.ActualCost = &cost
|
||||||
|
}
|
||||||
|
|
||||||
return response
|
return response
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -254,12 +254,27 @@ func (h *AdminUserHandler) Update(c *gin.Context) {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
// Update phone number in profile if provided
|
// Update profile fields if provided
|
||||||
if req.PhoneNumber != nil {
|
if req.PhoneNumber != nil || req.Verified != nil {
|
||||||
var profile models.UserProfile
|
var profile models.UserProfile
|
||||||
if err := h.db.Where("user_id = ?", user.ID).First(&profile).Error; err == nil {
|
if err := h.db.Where("user_id = ?", user.ID).First(&profile).Error; err == nil {
|
||||||
profile.PhoneNumber = *req.PhoneNumber
|
if req.PhoneNumber != nil {
|
||||||
|
profile.PhoneNumber = *req.PhoneNumber
|
||||||
|
}
|
||||||
|
if req.Verified != nil {
|
||||||
|
profile.Verified = *req.Verified
|
||||||
|
}
|
||||||
h.db.Save(&profile)
|
h.db.Save(&profile)
|
||||||
|
} else {
|
||||||
|
// Create profile if it doesn't exist
|
||||||
|
profile = models.UserProfile{UserID: user.ID}
|
||||||
|
if req.PhoneNumber != nil {
|
||||||
|
profile.PhoneNumber = *req.PhoneNumber
|
||||||
|
}
|
||||||
|
if req.Verified != nil {
|
||||||
|
profile.Verified = *req.Verified
|
||||||
|
}
|
||||||
|
h.db.Create(&profile)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -258,6 +258,7 @@ func SetupRoutes(router *gin.Engine, db *gorm.DB, cfg *config.Config, deps *Depe
|
|||||||
settings.PUT("", settingsHandler.UpdateSettings)
|
settings.PUT("", settingsHandler.UpdateSettings)
|
||||||
settings.POST("/seed-lookups", settingsHandler.SeedLookups)
|
settings.POST("/seed-lookups", settingsHandler.SeedLookups)
|
||||||
settings.POST("/seed-test-data", settingsHandler.SeedTestData)
|
settings.POST("/seed-test-data", settingsHandler.SeedTestData)
|
||||||
|
settings.POST("/clear-all-data", settingsHandler.ClearAllData)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Limitations management (tier limits, upgrade triggers)
|
// Limitations management (tier limits, upgrade triggers)
|
||||||
|
|||||||
Reference in New Issue
Block a user