mirror of
https://github.com/renee-png/acmcc.git
synced 2026-06-21 09:50:01 +00:00
183fe0a93c
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
561 lines
24 KiB
TypeScript
561 lines
24 KiB
TypeScript
import { useEffect, useState } from "react";
|
|
import { supabase } from "@/integrations/supabase/client";
|
|
import { Button } from "@/components/ui/button";
|
|
import { Input } from "@/components/ui/input";
|
|
import { Label } from "@/components/ui/label";
|
|
import { Textarea } from "@/components/ui/textarea";
|
|
import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card";
|
|
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "@/components/ui/select";
|
|
import {
|
|
Dialog, DialogContent, DialogDescription, DialogFooter, DialogHeader, DialogTitle, DialogTrigger,
|
|
} from "@/components/ui/dialog";
|
|
import { Table, TableBody, TableCell, TableHead, TableHeader, TableRow } from "@/components/ui/table";
|
|
import { Badge } from "@/components/ui/badge";
|
|
import { useToast } from "@/hooks/use-toast";
|
|
import { useAuth } from "@/contexts/AuthContext";
|
|
import { Users, Plus, Trash2, UserPlus, Pencil, Link2 } from "lucide-react";
|
|
|
|
type Association = { id: string; name: string };
|
|
type Committee = { id: string; association_id: string; name: string; description: string | null; is_active: boolean };
|
|
type Member = { id: string; committee_id: string | null; association_id: string; name: string; email: string | null; role: string | null; start_date: string | null; end_date: string | null; is_active: boolean; user_id: string | null };
|
|
type LinkableUser = { user_id: string; full_name: string | null; email: string | null };
|
|
|
|
function termStatus(m: { start_date: string | null; end_date: string | null; is_active: boolean }) {
|
|
const today = new Date().toISOString().slice(0, 10);
|
|
if (m.start_date && m.start_date > today) return { label: "Pending", variant: "secondary" as const };
|
|
if (m.end_date && m.end_date < today) return { label: "Term ended", variant: "outline" as const };
|
|
return { label: "Active", variant: "default" as const };
|
|
}
|
|
|
|
export default function CommitteesPage() {
|
|
const { user, isAdmin } = useAuth();
|
|
const { toast } = useToast();
|
|
|
|
const [associations, setAssociations] = useState<Association[]>([]);
|
|
const [assocFilter, setAssocFilter] = useState<string>("all");
|
|
const [committees, setCommittees] = useState<Committee[]>([]);
|
|
const [members, setMembers] = useState<Member[]>([]);
|
|
const [loading, setLoading] = useState(true);
|
|
const [linkableUsers, setLinkableUsers] = useState<LinkableUser[]>([]);
|
|
const [assocUserIds, setAssocUserIds] = useState<Set<string> | null>(null);
|
|
|
|
// Create committee
|
|
const [newOpen, setNewOpen] = useState(false);
|
|
const [newName, setNewName] = useState("");
|
|
const [newDesc, setNewDesc] = useState("");
|
|
const [newAssoc, setNewAssoc] = useState<string>("");
|
|
const [saving, setSaving] = useState(false);
|
|
|
|
// Add member
|
|
const [memberDialog, setMemberDialog] = useState<Committee | null>(null);
|
|
const [mName, setMName] = useState("");
|
|
const [mEmail, setMEmail] = useState("");
|
|
const [mRole, setMRole] = useState("");
|
|
const [mStart, setMStart] = useState("");
|
|
const [mEnd, setMEnd] = useState("");
|
|
const [mUserId, setMUserId] = useState<string>("");
|
|
|
|
// Edit member
|
|
const [editingMember, setEditingMember] = useState<Member | null>(null);
|
|
const [emName, setEmName] = useState("");
|
|
const [emEmail, setEmEmail] = useState("");
|
|
const [emRole, setEmRole] = useState("");
|
|
const [emStart, setEmStart] = useState("");
|
|
const [emEnd, setEmEnd] = useState("");
|
|
const [emUserId, setEmUserId] = useState<string>("");
|
|
|
|
// Edit committee
|
|
const [editing, setEditing] = useState<Committee | null>(null);
|
|
const [editName, setEditName] = useState("");
|
|
const [editDesc, setEditDesc] = useState("");
|
|
|
|
const fetchAll = async () => {
|
|
setLoading(true);
|
|
const [{ data: assocs }, { data: cs }, { data: ms }] = await Promise.all([
|
|
supabase.from("associations").select("id, name").eq("status", "active").order("name"),
|
|
supabase.from("committees").select("*").order("name"),
|
|
supabase.from("arc_committee_members").select("id, committee_id, association_id, name, email, role, start_date, end_date, is_active, user_id").order("name"),
|
|
]);
|
|
setAssociations((assocs ?? []) as Association[]);
|
|
setCommittees((cs ?? []) as Committee[]);
|
|
setMembers((ms ?? []) as Member[]);
|
|
setLoading(false);
|
|
};
|
|
|
|
useEffect(() => { fetchAll(); }, []);
|
|
|
|
// Load list of users for linking (admin-only)
|
|
useEffect(() => {
|
|
if (!isAdmin) return;
|
|
(async () => {
|
|
try {
|
|
const { data, error } = await supabase.functions.invoke("admin-user-management", {
|
|
body: { action: "list_users" },
|
|
});
|
|
if (error) throw error;
|
|
const list: LinkableUser[] = (data?.users ?? data ?? []).map((u: any) => ({
|
|
user_id: u.id,
|
|
full_name: u.user_metadata?.full_name || u.full_name || null,
|
|
email: u.email || null,
|
|
})).filter((u: LinkableUser) => u.user_id);
|
|
list.sort((a, b) => (a.full_name || a.email || "").localeCompare(b.full_name || b.email || ""));
|
|
setLinkableUsers(list);
|
|
} catch (err) {
|
|
console.warn("Could not load users for linking", err);
|
|
}
|
|
})();
|
|
}, [isAdmin]);
|
|
|
|
// Load user_ids of owners belonging to the active dialog's association
|
|
useEffect(() => {
|
|
const assocId = memberDialog?.association_id || editingMember?.association_id;
|
|
if (!assocId) { setAssocUserIds(null); return; }
|
|
(async () => {
|
|
const { data } = await supabase
|
|
.from("owners")
|
|
.select("user_id")
|
|
.eq("association_id", assocId)
|
|
.neq("status", "archived")
|
|
.not("user_id", "is", null);
|
|
setAssocUserIds(new Set((data ?? []).map((o: any) => o.user_id).filter(Boolean)));
|
|
})();
|
|
}, [memberDialog, editingMember]);
|
|
|
|
const dialogLinkableUsers = assocUserIds
|
|
? linkableUsers.filter((u) => assocUserIds.has(u.user_id))
|
|
: linkableUsers;
|
|
|
|
const userLabel = (u: LinkableUser) => u.full_name ? `${u.full_name}${u.email ? ` (${u.email})` : ""}` : (u.email || u.user_id);
|
|
const linkedUserName = (uid: string | null) => {
|
|
if (!uid) return null;
|
|
const u = linkableUsers.find((x) => x.user_id === uid);
|
|
return u ? userLabel(u) : "Linked user";
|
|
};
|
|
|
|
const visibleCommittees = assocFilter === "all"
|
|
? committees
|
|
: committees.filter((c) => c.association_id === assocFilter);
|
|
|
|
const associationName = (id: string) => associations.find((a) => a.id === id)?.name || "—";
|
|
const committeeMembers = (cid: string) => members.filter((m) => m.committee_id === cid);
|
|
|
|
const createCommittee = async () => {
|
|
if (!newName.trim() || !newAssoc) {
|
|
toast({ title: "Name and association required", variant: "destructive" });
|
|
return;
|
|
}
|
|
setSaving(true);
|
|
try {
|
|
const { error } = await supabase.from("committees").insert({
|
|
name: newName.trim(),
|
|
description: newDesc.trim() || null,
|
|
association_id: newAssoc,
|
|
created_by: user?.id ?? null,
|
|
});
|
|
if (error) throw error;
|
|
toast({ title: "Committee created" });
|
|
setNewOpen(false);
|
|
setNewName(""); setNewDesc(""); setNewAssoc("");
|
|
fetchAll();
|
|
} catch (err: any) {
|
|
toast({ title: "Error", description: err.message, variant: "destructive" });
|
|
} finally {
|
|
setSaving(false);
|
|
}
|
|
};
|
|
|
|
const saveEdit = async () => {
|
|
if (!editing) return;
|
|
const { error } = await supabase.from("committees").update({
|
|
name: editName.trim() || editing.name,
|
|
description: editDesc.trim() || null,
|
|
}).eq("id", editing.id);
|
|
if (error) {
|
|
toast({ title: "Error", description: error.message, variant: "destructive" });
|
|
return;
|
|
}
|
|
toast({ title: "Saved" });
|
|
setEditing(null);
|
|
fetchAll();
|
|
};
|
|
|
|
const deleteCommittee = async (c: Committee) => {
|
|
if (!confirm(`Delete "${c.name}" and all its members?`)) return;
|
|
const { error } = await supabase.from("committees").delete().eq("id", c.id);
|
|
if (error) {
|
|
toast({ title: "Error", description: error.message, variant: "destructive" });
|
|
return;
|
|
}
|
|
fetchAll();
|
|
};
|
|
|
|
const addMember = async () => {
|
|
if (!memberDialog || !mName.trim()) return;
|
|
const { error } = await supabase.from("arc_committee_members").insert({
|
|
committee_id: memberDialog.id,
|
|
association_id: memberDialog.association_id,
|
|
name: mName.trim(),
|
|
email: mEmail.trim() || null,
|
|
role: mRole.trim() || null,
|
|
start_date: mStart || null,
|
|
end_date: mEnd || null,
|
|
user_id: mUserId || null,
|
|
created_by: user?.id ?? null,
|
|
});
|
|
if (error) {
|
|
toast({ title: "Error", description: error.message, variant: "destructive" });
|
|
return;
|
|
}
|
|
setMName(""); setMEmail(""); setMRole(""); setMStart(""); setMEnd(""); setMUserId("");
|
|
fetchAll();
|
|
};
|
|
|
|
const openEditMember = (m: Member) => {
|
|
setEditingMember(m);
|
|
setEmName(m.name);
|
|
setEmEmail(m.email || "");
|
|
setEmRole(m.role || "");
|
|
setEmStart(m.start_date || "");
|
|
setEmEnd(m.end_date || "");
|
|
setEmUserId(m.user_id || "");
|
|
};
|
|
|
|
const saveEditMember = async () => {
|
|
if (!editingMember) return;
|
|
const { error } = await supabase.from("arc_committee_members").update({
|
|
name: emName.trim() || editingMember.name,
|
|
email: emEmail.trim() || null,
|
|
role: emRole.trim() || null,
|
|
start_date: emStart || null,
|
|
end_date: emEnd || null,
|
|
user_id: emUserId || null,
|
|
}).eq("id", editingMember.id);
|
|
if (error) {
|
|
toast({ title: "Error", description: error.message, variant: "destructive" });
|
|
return;
|
|
}
|
|
setEditingMember(null);
|
|
fetchAll();
|
|
};
|
|
|
|
const removeMember = async (id: string) => {
|
|
const { error } = await supabase.from("arc_committee_members").delete().eq("id", id);
|
|
if (error) {
|
|
toast({ title: "Error", description: error.message, variant: "destructive" });
|
|
return;
|
|
}
|
|
fetchAll();
|
|
};
|
|
|
|
return (
|
|
<div className="space-y-5">
|
|
<div className="flex items-start justify-between">
|
|
<div className="flex items-center gap-3">
|
|
<div className="flex h-10 w-10 items-center justify-center rounded-xl bg-primary/10">
|
|
<Users className="h-5 w-5 text-primary" />
|
|
</div>
|
|
<div>
|
|
<h1 className="text-[22px] font-bold tracking-tight text-foreground">Committees</h1>
|
|
<p className="text-[13px] text-muted-foreground">
|
|
Manage committees and members per association. Members don't need user accounts — useful for recording historical votes.
|
|
</p>
|
|
</div>
|
|
</div>
|
|
{isAdmin && (
|
|
<Dialog open={newOpen} onOpenChange={setNewOpen}>
|
|
<DialogTrigger asChild>
|
|
<Button size="sm" className="gap-1.5"><Plus className="h-3.5 w-3.5" /> New Committee</Button>
|
|
</DialogTrigger>
|
|
<DialogContent>
|
|
<DialogHeader>
|
|
<DialogTitle>Create Committee</DialogTitle>
|
|
<DialogDescription>Add a committee for a specific association.</DialogDescription>
|
|
</DialogHeader>
|
|
<div className="space-y-3">
|
|
<div className="space-y-1.5">
|
|
<Label>Association *</Label>
|
|
<Select value={newAssoc} onValueChange={setNewAssoc}>
|
|
<SelectTrigger><SelectValue placeholder="Select association" /></SelectTrigger>
|
|
<SelectContent>
|
|
{associations.map((a) => <SelectItem key={a.id} value={a.id}>{a.name}</SelectItem>)}
|
|
</SelectContent>
|
|
</Select>
|
|
</div>
|
|
<div className="space-y-1.5">
|
|
<Label>Committee Name *</Label>
|
|
<Input value={newName} onChange={(e) => setNewName(e.target.value)} placeholder="e.g. Architectural Review Committee" />
|
|
</div>
|
|
<div className="space-y-1.5">
|
|
<Label>Description</Label>
|
|
<Textarea value={newDesc} onChange={(e) => setNewDesc(e.target.value)} placeholder="Optional" />
|
|
</div>
|
|
</div>
|
|
<DialogFooter>
|
|
<Button variant="outline" onClick={() => setNewOpen(false)}>Cancel</Button>
|
|
<Button onClick={createCommittee} disabled={saving}>{saving ? "Saving…" : "Create"}</Button>
|
|
</DialogFooter>
|
|
</DialogContent>
|
|
</Dialog>
|
|
)}
|
|
</div>
|
|
|
|
<div className="flex items-center gap-3">
|
|
<Select value={assocFilter} onValueChange={setAssocFilter}>
|
|
<SelectTrigger className="w-[260px] h-9 text-[13px]"><SelectValue /></SelectTrigger>
|
|
<SelectContent>
|
|
<SelectItem value="all">All Associations</SelectItem>
|
|
{associations.map((a) => <SelectItem key={a.id} value={a.id}>{a.name}</SelectItem>)}
|
|
</SelectContent>
|
|
</Select>
|
|
</div>
|
|
|
|
{loading ? (
|
|
<div className="flex justify-center py-16">
|
|
<div className="h-8 w-8 animate-spin rounded-full border-4 border-primary border-t-transparent" />
|
|
</div>
|
|
) : visibleCommittees.length === 0 ? (
|
|
<Card>
|
|
<CardContent className="py-16 text-center text-muted-foreground">
|
|
<Users className="mx-auto h-10 w-10 mb-3 opacity-30" />
|
|
<p className="font-medium">No committees yet.</p>
|
|
{isAdmin && <p className="text-[12px] mt-1">Click "New Committee" to add one.</p>}
|
|
</CardContent>
|
|
</Card>
|
|
) : (
|
|
<div className="grid gap-4">
|
|
{visibleCommittees.map((c) => {
|
|
const cm = committeeMembers(c.id);
|
|
return (
|
|
<Card key={c.id}>
|
|
<CardHeader className="flex flex-row items-start justify-between gap-2 space-y-0">
|
|
<div>
|
|
<CardTitle className="text-base flex items-center gap-2">
|
|
{c.name}
|
|
<Badge variant="outline" className="text-[10px]">{associationName(c.association_id)}</Badge>
|
|
</CardTitle>
|
|
{c.description && <p className="text-xs text-muted-foreground mt-1">{c.description}</p>}
|
|
</div>
|
|
{isAdmin && (
|
|
<div className="flex gap-1">
|
|
<Button size="sm" variant="ghost" className="gap-1 h-8" onClick={() => { setMemberDialog(c); }}>
|
|
<UserPlus className="h-3.5 w-3.5" /> Add Member
|
|
</Button>
|
|
<Button size="icon" variant="ghost" className="h-8 w-8" onClick={() => { setEditing(c); setEditName(c.name); setEditDesc(c.description || ""); }}>
|
|
<Pencil className="h-3.5 w-3.5" />
|
|
</Button>
|
|
<Button size="icon" variant="ghost" className="h-8 w-8" onClick={() => deleteCommittee(c)}>
|
|
<Trash2 className="h-3.5 w-3.5" />
|
|
</Button>
|
|
</div>
|
|
)}
|
|
</CardHeader>
|
|
<CardContent>
|
|
{cm.length === 0 ? (
|
|
<p className="text-xs text-muted-foreground">No members yet.</p>
|
|
) : (
|
|
<Table>
|
|
<TableHeader>
|
|
<TableRow>
|
|
<TableHead className="text-[11px]">Name</TableHead>
|
|
<TableHead className="text-[11px]">Email</TableHead>
|
|
<TableHead className="text-[11px]">Role</TableHead>
|
|
<TableHead className="text-[11px]">Term</TableHead>
|
|
<TableHead className="text-[11px]">Status</TableHead>
|
|
{isAdmin && <TableHead className="w-[80px]" />}
|
|
</TableRow>
|
|
</TableHeader>
|
|
<TableBody>
|
|
{cm.map((m) => {
|
|
const status = termStatus(m);
|
|
const term = [m.start_date || "—", m.end_date || "Present"].join(" → ");
|
|
return (
|
|
<TableRow key={m.id}>
|
|
<TableCell className="text-sm font-medium">
|
|
<div className="flex items-center gap-1.5">
|
|
{m.name}
|
|
{m.user_id && (
|
|
<Badge variant="secondary" className="text-[10px] gap-0.5" title={linkedUserName(m.user_id) || "Linked"}>
|
|
<Link2 className="h-2.5 w-2.5" /> Linked
|
|
</Badge>
|
|
)}
|
|
</div>
|
|
</TableCell>
|
|
<TableCell className="text-sm text-muted-foreground">{m.email || "—"}</TableCell>
|
|
<TableCell className="text-sm text-muted-foreground">{m.role || "—"}</TableCell>
|
|
<TableCell className="text-xs text-muted-foreground">{term}</TableCell>
|
|
<TableCell><Badge variant={status.variant} className="text-[10px]">{status.label}</Badge></TableCell>
|
|
{isAdmin && (
|
|
<TableCell>
|
|
<div className="flex gap-1">
|
|
<Button size="icon" variant="ghost" className="h-7 w-7" onClick={() => openEditMember(m)}>
|
|
<Pencil className="h-3.5 w-3.5" />
|
|
</Button>
|
|
<Button size="icon" variant="ghost" className="h-7 w-7" onClick={() => removeMember(m.id)}>
|
|
<Trash2 className="h-3.5 w-3.5" />
|
|
</Button>
|
|
</div>
|
|
</TableCell>
|
|
)}
|
|
</TableRow>
|
|
);
|
|
})}
|
|
</TableBody>
|
|
</Table>
|
|
)}
|
|
</CardContent>
|
|
</Card>
|
|
);
|
|
})}
|
|
</div>
|
|
)}
|
|
|
|
{/* Add Member Dialog */}
|
|
<Dialog open={!!memberDialog} onOpenChange={(o) => !o && setMemberDialog(null)}>
|
|
<DialogContent>
|
|
<DialogHeader>
|
|
<DialogTitle>Add Member to {memberDialog?.name}</DialogTitle>
|
|
<DialogDescription>Optionally link this member to an existing user account so their portal access updates automatically.</DialogDescription>
|
|
</DialogHeader>
|
|
<div className="space-y-3">
|
|
<div className="space-y-1.5">
|
|
<Label>Link to User Account (optional)</Label>
|
|
<Select
|
|
value={mUserId || "__none__"}
|
|
onValueChange={(v) => {
|
|
if (v === "__none__") { setMUserId(""); return; }
|
|
setMUserId(v);
|
|
const u = linkableUsers.find((x) => x.user_id === v);
|
|
if (u) {
|
|
if (!mName.trim() && u.full_name) setMName(u.full_name);
|
|
if (!mEmail.trim() && u.email) setMEmail(u.email);
|
|
}
|
|
}}
|
|
>
|
|
<SelectTrigger><SelectValue placeholder="No linked user" /></SelectTrigger>
|
|
<SelectContent className="max-h-72">
|
|
<SelectItem value="__none__">No linked user</SelectItem>
|
|
{dialogLinkableUsers.map((u) => (
|
|
<SelectItem key={u.user_id} value={u.user_id}>{userLabel(u)}</SelectItem>
|
|
))}
|
|
</SelectContent>
|
|
</Select>
|
|
</div>
|
|
<div className="space-y-1.5">
|
|
<Label>Name *</Label>
|
|
<Input value={mName} onChange={(e) => setMName(e.target.value)} />
|
|
</div>
|
|
<div className="space-y-1.5">
|
|
<Label>Email</Label>
|
|
<Input value={mEmail} onChange={(e) => setMEmail(e.target.value)} />
|
|
</div>
|
|
<div className="space-y-1.5">
|
|
<Label>Role</Label>
|
|
<Input value={mRole} onChange={(e) => setMRole(e.target.value)} placeholder="e.g. Chair, Member" />
|
|
</div>
|
|
<div className="grid grid-cols-2 gap-3">
|
|
<div className="space-y-1.5">
|
|
<Label>Start Date</Label>
|
|
<Input type="date" value={mStart} onChange={(e) => setMStart(e.target.value)} />
|
|
</div>
|
|
<div className="space-y-1.5">
|
|
<Label>End Date</Label>
|
|
<Input type="date" value={mEnd} onChange={(e) => setMEnd(e.target.value)} />
|
|
</div>
|
|
</div>
|
|
<p className="text-[11px] text-muted-foreground">Outside the term window the member is auto-deactivated and loses committee access; their past actions remain on record.</p>
|
|
</div>
|
|
<DialogFooter>
|
|
<Button variant="outline" onClick={() => setMemberDialog(null)}>Close</Button>
|
|
<Button onClick={addMember} disabled={!mName.trim()}>Add</Button>
|
|
</DialogFooter>
|
|
</DialogContent>
|
|
</Dialog>
|
|
|
|
{/* Edit Committee Dialog */}
|
|
<Dialog open={!!editing} onOpenChange={(o) => !o && setEditing(null)}>
|
|
<DialogContent>
|
|
<DialogHeader>
|
|
<DialogTitle>Edit Committee</DialogTitle>
|
|
<DialogDescription>Update committee details.</DialogDescription>
|
|
</DialogHeader>
|
|
<div className="space-y-3">
|
|
<div className="space-y-1.5">
|
|
<Label>Name</Label>
|
|
<Input value={editName} onChange={(e) => setEditName(e.target.value)} />
|
|
</div>
|
|
<div className="space-y-1.5">
|
|
<Label>Description</Label>
|
|
<Textarea value={editDesc} onChange={(e) => setEditDesc(e.target.value)} />
|
|
</div>
|
|
</div>
|
|
<DialogFooter>
|
|
<Button variant="outline" onClick={() => setEditing(null)}>Cancel</Button>
|
|
<Button onClick={saveEdit}>Save</Button>
|
|
</DialogFooter>
|
|
</DialogContent>
|
|
</Dialog>
|
|
|
|
{/* Edit Member Dialog */}
|
|
<Dialog open={!!editingMember} onOpenChange={(o) => !o && setEditingMember(null)}>
|
|
<DialogContent>
|
|
<DialogHeader>
|
|
<DialogTitle>Edit Committee Member</DialogTitle>
|
|
<DialogDescription>Update term dates to schedule access. Past actions are preserved.</DialogDescription>
|
|
</DialogHeader>
|
|
<div className="space-y-3">
|
|
<div className="space-y-1.5">
|
|
<Label>Link to User Account</Label>
|
|
<Select
|
|
value={emUserId || "__none__"}
|
|
onValueChange={(v) => {
|
|
if (v === "__none__") { setEmUserId(""); return; }
|
|
setEmUserId(v);
|
|
const u = linkableUsers.find((x) => x.user_id === v);
|
|
if (u) {
|
|
if (!emName.trim() && u.full_name) setEmName(u.full_name);
|
|
if (!emEmail.trim() && u.email) setEmEmail(u.email);
|
|
}
|
|
}}
|
|
>
|
|
<SelectTrigger><SelectValue placeholder="No linked user" /></SelectTrigger>
|
|
<SelectContent className="max-h-72">
|
|
<SelectItem value="__none__">No linked user</SelectItem>
|
|
{dialogLinkableUsers.map((u) => (
|
|
<SelectItem key={u.user_id} value={u.user_id}>{userLabel(u)}</SelectItem>
|
|
))}
|
|
</SelectContent>
|
|
</Select>
|
|
</div>
|
|
<div className="space-y-1.5">
|
|
<Label>Name</Label>
|
|
<Input value={emName} onChange={(e) => setEmName(e.target.value)} />
|
|
</div>
|
|
<div className="space-y-1.5">
|
|
<Label>Email</Label>
|
|
<Input value={emEmail} onChange={(e) => setEmEmail(e.target.value)} />
|
|
</div>
|
|
<div className="space-y-1.5">
|
|
<Label>Role</Label>
|
|
<Input value={emRole} onChange={(e) => setEmRole(e.target.value)} />
|
|
</div>
|
|
<div className="grid grid-cols-2 gap-3">
|
|
<div className="space-y-1.5">
|
|
<Label>Start Date</Label>
|
|
<Input type="date" value={emStart} onChange={(e) => setEmStart(e.target.value)} />
|
|
</div>
|
|
<div className="space-y-1.5">
|
|
<Label>End Date</Label>
|
|
<Input type="date" value={emEnd} onChange={(e) => setEmEnd(e.target.value)} />
|
|
</div>
|
|
</div>
|
|
</div>
|
|
<DialogFooter>
|
|
<Button variant="outline" onClick={() => setEditingMember(null)}>Cancel</Button>
|
|
<Button onClick={saveEditMember}>Save</Button>
|
|
</DialogFooter>
|
|
</DialogContent>
|
|
</Dialog>
|
|
</div>
|
|
);
|
|
}
|