mirror of
https://github.com/renee-png/acmcc.git
synced 2026-06-21 01:40:01 +00:00
Add ACMCC app source, Supabase backend, and project config
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,231 @@
|
||||
import React, { useState, useEffect } from 'react';
|
||||
import { Dialog, DialogContent, DialogHeader, DialogTitle } from '@/components/ui/dialog';
|
||||
import { Button } from '@/components/ui/button';
|
||||
import { Label } from '@/components/ui/label';
|
||||
import { supabase } from '@/integrations/supabase/client';
|
||||
import { useToast } from '@/hooks/use-toast';
|
||||
|
||||
const STATUS_OPTIONS = [
|
||||
"open",
|
||||
"in_progress",
|
||||
"resolved",
|
||||
"closed"
|
||||
];
|
||||
|
||||
const normalizeCollectionStatus = (status) => status === 'resolved' ? 'closed' : status;
|
||||
|
||||
function CollectionDialog({ open, onOpenChange, collection, onSuccess }) {
|
||||
const { toast } = useToast();
|
||||
const db = supabase;
|
||||
const [associations, setAssociations] = useState([]);
|
||||
const [owners, setOwners] = useState([]);
|
||||
const [units, setUnits] = useState([]);
|
||||
const [formData, setFormData] = useState({
|
||||
association_id: '',
|
||||
owner_id: '',
|
||||
unit_id: '',
|
||||
notes: '',
|
||||
status: 'open',
|
||||
last_notice_date: '',
|
||||
amount_owed: ''
|
||||
});
|
||||
const [loading, setLoading] = useState(false);
|
||||
|
||||
useEffect(() => {
|
||||
const fetchAssociations = async () => {
|
||||
const { data, error } = await db.from('associations').select('id, name').eq('status', 'active').order('name');
|
||||
if (!error) setAssociations(data || []);
|
||||
};
|
||||
if (open) fetchAssociations();
|
||||
}, [open]);
|
||||
|
||||
useEffect(() => {
|
||||
if (formData.association_id) {
|
||||
const fetchOwnersAndUnits = async () => {
|
||||
const [ownersRes, unitsRes] = await Promise.all([
|
||||
db.from('owners').select('id, first_name, last_name').eq('association_id', formData.association_id).eq('status', 'active').order('last_name'),
|
||||
db.from('units').select('id, unit_number, address').eq('association_id', formData.association_id).order('unit_number'),
|
||||
]);
|
||||
if (!ownersRes.error) setOwners(ownersRes.data || []);
|
||||
if (!unitsRes.error) setUnits(unitsRes.data || []);
|
||||
};
|
||||
fetchOwnersAndUnits();
|
||||
} else {
|
||||
setOwners([]);
|
||||
setUnits([]);
|
||||
}
|
||||
}, [formData.association_id]);
|
||||
|
||||
useEffect(() => {
|
||||
if (collection) {
|
||||
setFormData({
|
||||
association_id: collection.association_id || '',
|
||||
owner_id: collection.owner_id || '',
|
||||
unit_id: collection.unit_id || '',
|
||||
notes: collection.notes || '',
|
||||
status: collection.status || 'open',
|
||||
last_notice_date: collection.last_notice_date || '',
|
||||
amount_owed: collection.amount_owed || ''
|
||||
});
|
||||
} else {
|
||||
setFormData({
|
||||
association_id: '', owner_id: '', unit_id: '', notes: '', status: 'open', last_notice_date: '', amount_owed: ''
|
||||
});
|
||||
}
|
||||
}, [collection, open]);
|
||||
|
||||
const handleSubmit = async (e) => {
|
||||
e.preventDefault();
|
||||
setLoading(true);
|
||||
|
||||
if (!formData.association_id) {
|
||||
toast({ variant: 'destructive', title: 'Missing Field', description: 'Please select an association.' });
|
||||
setLoading(false);
|
||||
return;
|
||||
}
|
||||
|
||||
const dataToSubmit = {
|
||||
...formData,
|
||||
status: normalizeCollectionStatus(formData.status),
|
||||
last_notice_date: formData.last_notice_date || null,
|
||||
amount_owed: formData.amount_owed || 0,
|
||||
owner_id: formData.owner_id || null,
|
||||
unit_id: formData.unit_id || null,
|
||||
};
|
||||
|
||||
const { error } = collection
|
||||
? await db.from('collections').update(dataToSubmit).eq('id', collection.id)
|
||||
: await db.from('collections').insert([dataToSubmit]);
|
||||
|
||||
if (!error) {
|
||||
toast({
|
||||
title: collection ? 'Collection Updated' : 'Collection Created',
|
||||
description: `Collection has been successfully ${collection ? 'updated' : 'created'}.`,
|
||||
});
|
||||
onSuccess();
|
||||
onOpenChange(false);
|
||||
} else {
|
||||
toast({ variant: 'destructive', title: 'Error', description: error.message });
|
||||
}
|
||||
setLoading(false);
|
||||
};
|
||||
|
||||
return (
|
||||
<Dialog open={open} onOpenChange={onOpenChange}>
|
||||
<DialogContent className="sm:max-w-[500px]">
|
||||
<DialogHeader>
|
||||
<DialogTitle>{collection ? 'Edit Collection' : 'Add New Collection'}</DialogTitle>
|
||||
</DialogHeader>
|
||||
<form onSubmit={handleSubmit} className="space-y-4">
|
||||
<div>
|
||||
<Label htmlFor="association_id">Association *</Label>
|
||||
<select
|
||||
id="association_id"
|
||||
value={formData.association_id}
|
||||
onChange={(e) => setFormData({ ...formData, association_id: e.target.value, owner_id: '' })}
|
||||
required
|
||||
className="w-full mt-1 px-3 py-2 border border-slate-300 rounded-lg focus:ring-2 focus:ring-teal-500 focus:border-transparent"
|
||||
>
|
||||
<option value="">Select an association...</option>
|
||||
{associations.map((a) => (
|
||||
<option key={a.id} value={a.id}>{a.name}</option>
|
||||
))}
|
||||
</select>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<Label htmlFor="unit_id">Unit</Label>
|
||||
<select
|
||||
id="unit_id"
|
||||
value={formData.unit_id}
|
||||
onChange={(e) => setFormData({ ...formData, unit_id: e.target.value })}
|
||||
className="w-full mt-1 px-3 py-2 border border-slate-300 rounded-lg focus:ring-2 focus:ring-teal-500 focus:border-transparent"
|
||||
disabled={!formData.association_id}
|
||||
>
|
||||
<option value="">Select a unit...</option>
|
||||
{units.map((u) => (
|
||||
<option key={u.id} value={u.id}>{u.unit_number}{u.address ? ` - ${u.address}` : ''}</option>
|
||||
))}
|
||||
</select>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<Label htmlFor="owner_id">Owner</Label>
|
||||
<select
|
||||
id="owner_id"
|
||||
value={formData.owner_id}
|
||||
onChange={(e) => setFormData({ ...formData, owner_id: e.target.value })}
|
||||
className="w-full mt-1 px-3 py-2 border border-slate-300 rounded-lg focus:ring-2 focus:ring-teal-500 focus:border-transparent"
|
||||
disabled={!formData.association_id}
|
||||
>
|
||||
<option value="">Select an owner...</option>
|
||||
{owners.map((o) => (
|
||||
<option key={o.id} value={o.id}>{o.first_name} {o.last_name}</option>
|
||||
))}
|
||||
</select>
|
||||
</div>
|
||||
|
||||
<div className="grid grid-cols-2 gap-4">
|
||||
<div>
|
||||
<Label htmlFor="status">Status *</Label>
|
||||
<select
|
||||
id="status"
|
||||
value={formData.status}
|
||||
onChange={(e) => setFormData({ ...formData, status: e.target.value })}
|
||||
required
|
||||
className="w-full mt-1 px-3 py-2 border border-slate-300 rounded-lg focus:ring-2 focus:ring-teal-500 focus:border-transparent"
|
||||
>
|
||||
{STATUS_OPTIONS.map((status) => (
|
||||
<option key={status} value={status}>{status}</option>
|
||||
))}
|
||||
</select>
|
||||
</div>
|
||||
<div>
|
||||
<Label htmlFor="last_notice_date">Last Notice Date</Label>
|
||||
<input
|
||||
id="last_notice_date"
|
||||
type="date"
|
||||
value={formData.last_notice_date}
|
||||
onChange={(e) => setFormData({ ...formData, last_notice_date: e.target.value })}
|
||||
className="w-full mt-1 px-3 py-2 border border-slate-300 rounded-lg focus:ring-2 focus:ring-teal-500 focus:border-transparent"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<Label htmlFor="amount_owed">Amount Owed ($)</Label>
|
||||
<input
|
||||
id="amount_owed"
|
||||
type="number"
|
||||
step="0.01"
|
||||
min="0"
|
||||
placeholder="0.00"
|
||||
value={formData.amount_owed}
|
||||
onChange={(e) => setFormData({ ...formData, amount_owed: e.target.value })}
|
||||
className="w-full mt-1 px-3 py-2 border border-slate-300 rounded-lg focus:ring-2 focus:ring-teal-500 focus:border-transparent"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<Label htmlFor="notes">Notes</Label>
|
||||
<textarea
|
||||
id="notes"
|
||||
value={formData.notes}
|
||||
onChange={(e) => setFormData({ ...formData, notes: e.target.value })}
|
||||
rows={3}
|
||||
className="w-full mt-1 px-3 py-2 border border-slate-300 rounded-lg focus:ring-2 focus:ring-teal-500 focus:border-transparent"
|
||||
/>
|
||||
</div>
|
||||
<div className="flex justify-end space-x-2 pt-4">
|
||||
<Button type="button" variant="outline" onClick={() => onOpenChange(false)}>Cancel</Button>
|
||||
<Button type="submit" disabled={loading}>
|
||||
{loading ? 'Saving...' : collection ? 'Update' : 'Create'}
|
||||
</Button>
|
||||
</div>
|
||||
</form>
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
);
|
||||
}
|
||||
|
||||
export default CollectionDialog;
|
||||
Reference in New Issue
Block a user