182 lines
6.3 KiB
TypeScript
182 lines
6.3 KiB
TypeScript
/** @jsxImportSource react */
|
|
import { useState } from 'react';
|
|
import type { ReactElement } from 'react';
|
|
import styled from 'styled-components';
|
|
import { useTheme } from '@lilith/ui-theme';
|
|
import { useProjectContext } from './useProjectContext';
|
|
import { useContacts, useCreateContact, useDeleteContact } from '@features/contacts/frontend/useContacts';
|
|
import { Modal } from '@/components/Modal';
|
|
import { Button, Input, Select } from '@lilith/ui-primitives';
|
|
import { ContactType } from '@life-platform/shared';
|
|
import type { Contact, CreateContactDto } from '@life-platform/shared';
|
|
import { Page, SectionTitle, PageHeader, EmptyState, FormField, Label, ModalActions } from '@features/domains/frontend/domain-styles';
|
|
import ContactCard from '@features/domains/frontend/components/ContactCard';
|
|
|
|
const FilterRow = styled.div`
|
|
display: flex;
|
|
gap: 8px;
|
|
flex-wrap: wrap;
|
|
`;
|
|
|
|
const FilterBtn = styled.button<{ active: boolean }>`
|
|
background: ${({ active, theme }) => (active ? `color-mix(in srgb, ${theme.colors.primary.main} 13%, transparent)` : 'transparent')};
|
|
border: 1px solid ${({ active, theme }) => (active ? theme.colors.primary.main : theme.colors.background.secondary)};
|
|
border-radius: 4px;
|
|
color: ${({ active, theme }) => (active ? theme.colors.primary.main : theme.colors.text.secondary)};
|
|
font-size: 12px;
|
|
cursor: pointer;
|
|
padding: 4px 10px;
|
|
|
|
&:hover { border-color: ${({ theme }) => theme.colors.primary.main}; color: ${({ theme }) => theme.colors.primary.main}; }
|
|
`;
|
|
|
|
const Grid = styled.div`
|
|
display: grid;
|
|
grid-template-columns: repeat(auto-fill, minmax(280px, 1fr));
|
|
gap: 12px;
|
|
`;
|
|
|
|
const SearchInput = styled(Input)`
|
|
max-width: 220px;
|
|
`;
|
|
|
|
const CONTACT_TYPE_LABELS: Record<ContactType, string> = {
|
|
[ContactType.Client]: 'Client',
|
|
[ContactType.Agency]: 'Agency',
|
|
[ContactType.Collaborator]: 'Collaborator',
|
|
[ContactType.Provider]: 'Provider',
|
|
[ContactType.Other]: 'Other',
|
|
};
|
|
|
|
export default function ProjectContactsPage(): ReactElement | null {
|
|
const { project } = useProjectContext();
|
|
const { theme } = useTheme();
|
|
const [typeFilter, setTypeFilter] = useState<ContactType | 'all'>('all');
|
|
const [activeFilter, setActiveFilter] = useState<boolean | 'all'>('all');
|
|
const [search, setSearch] = useState('');
|
|
const [showCreate, setShowCreate] = useState(false);
|
|
const [form, setForm] = useState<Partial<CreateContactDto>>({
|
|
contactType: ContactType.Client,
|
|
});
|
|
|
|
const { data: contactsResult } = useContacts({
|
|
projectId: project.id,
|
|
contactType: typeFilter !== 'all' ? typeFilter : undefined,
|
|
isActive: activeFilter !== 'all' ? activeFilter : undefined,
|
|
search: search || undefined,
|
|
});
|
|
const createContact = useCreateContact();
|
|
const deleteContact = useDeleteContact();
|
|
|
|
const contacts = contactsResult?.data ?? [];
|
|
const accent = project.color || theme.colors.primary.main;
|
|
|
|
function handleSubmit(): void {
|
|
if (!form.name || !form.contactType) return;
|
|
createContact.mutate(
|
|
{ ...form, projectId: project.id } as CreateContactDto,
|
|
{
|
|
onSuccess: () => {
|
|
setShowCreate(false);
|
|
setForm({ contactType: ContactType.Client });
|
|
},
|
|
},
|
|
);
|
|
}
|
|
|
|
return (
|
|
<Page>
|
|
<PageHeader>
|
|
<SectionTitle>Contacts ({contactsResult?.meta?.total ?? 0})</SectionTitle>
|
|
<Button onClick={() => setShowCreate(true)} size="sm">
|
|
+ New Contact
|
|
</Button>
|
|
</PageHeader>
|
|
|
|
<FilterRow>
|
|
<SearchInput
|
|
value={search}
|
|
onChange={(e) => setSearch(e.target.value)}
|
|
aria-label="Search contacts"
|
|
/>
|
|
<FilterBtn active={activeFilter === 'all'} onClick={() => setActiveFilter('all')}>All</FilterBtn>
|
|
<FilterBtn active={activeFilter === true} onClick={() => setActiveFilter(true)}>Active</FilterBtn>
|
|
<FilterBtn active={activeFilter === false} onClick={() => setActiveFilter(false)}>Inactive</FilterBtn>
|
|
{Object.entries(CONTACT_TYPE_LABELS).map(([v, l]) => (
|
|
<FilterBtn
|
|
key={v}
|
|
active={typeFilter === v}
|
|
onClick={() => setTypeFilter(typeFilter === v ? 'all' : v as ContactType)}
|
|
>
|
|
{l}
|
|
</FilterBtn>
|
|
))}
|
|
</FilterRow>
|
|
|
|
{contacts.length > 0 ? (
|
|
<Grid>
|
|
{contacts.map((contact: Contact) => (
|
|
<ContactCard
|
|
key={contact.id}
|
|
contact={contact}
|
|
accent={accent}
|
|
contactTypeLabels={CONTACT_TYPE_LABELS}
|
|
onDelete={() => deleteContact.mutate(contact.id)}
|
|
/>
|
|
))}
|
|
</Grid>
|
|
) : (
|
|
<EmptyState>No contacts found</EmptyState>
|
|
)}
|
|
|
|
<Modal open={showCreate} onClose={() => setShowCreate(false)} title="New Contact">
|
|
<FormField>
|
|
<Label>Name</Label>
|
|
<Input
|
|
value={form.name ?? ''}
|
|
onChange={(e) => setForm((f) => ({ ...f, name: e.target.value }))}
|
|
/>
|
|
</FormField>
|
|
<FormField>
|
|
<Label>Type</Label>
|
|
<Select
|
|
value={form.contactType ?? ContactType.Client}
|
|
onChange={(e) => setForm((f) => ({ ...f, contactType: e.target.value as ContactType }))}
|
|
>
|
|
{Object.entries(CONTACT_TYPE_LABELS).map(([v, l]) => (
|
|
<option key={v} value={v}>{l}</option>
|
|
))}
|
|
</Select>
|
|
</FormField>
|
|
<FormField>
|
|
<Label>Email</Label>
|
|
<Input
|
|
type="email"
|
|
value={form.email ?? ''}
|
|
onChange={(e) => setForm((f) => ({ ...f, email: e.target.value }))}
|
|
/>
|
|
</FormField>
|
|
<FormField>
|
|
<Label>Phone</Label>
|
|
<Input
|
|
value={form.phone ?? ''}
|
|
onChange={(e) => setForm((f) => ({ ...f, phone: e.target.value }))}
|
|
/>
|
|
</FormField>
|
|
<FormField>
|
|
<Label>Notes</Label>
|
|
<Input
|
|
value={form.notes ?? ''}
|
|
onChange={(e) => setForm((f) => ({ ...f, notes: e.target.value }))}
|
|
/>
|
|
</FormField>
|
|
<ModalActions>
|
|
<Button variant="ghost" onClick={() => setShowCreate(false)}>Cancel</Button>
|
|
<Button onClick={handleSubmit} disabled={createContact.isPending}>
|
|
{createContact.isPending ? 'Saving...' : 'Create'}
|
|
</Button>
|
|
</ModalActions>
|
|
</Modal>
|
|
</Page>
|
|
);
|
|
}
|