taks-d.2.1
This commit is contained in:
153
src/features/crm/components/project-parties-editor.tsx
Normal file
153
src/features/crm/components/project-parties-editor.tsx
Normal file
@@ -0,0 +1,153 @@
|
||||
'use client';
|
||||
|
||||
import { Icons } from '@/components/icons';
|
||||
import { Button } from '@/components/ui/button';
|
||||
import { Input } from '@/components/ui/input';
|
||||
import {
|
||||
Select,
|
||||
SelectContent,
|
||||
SelectItem,
|
||||
SelectTrigger,
|
||||
SelectValue
|
||||
} from '@/components/ui/select';
|
||||
|
||||
export interface ProjectPartyEditorCustomer {
|
||||
id: string;
|
||||
code: string;
|
||||
name: string;
|
||||
}
|
||||
|
||||
export interface ProjectPartyEditorRole {
|
||||
id: string;
|
||||
code: string;
|
||||
label: string;
|
||||
}
|
||||
|
||||
export interface ProjectPartyEditorItem {
|
||||
key: string;
|
||||
customerId: string;
|
||||
role: string;
|
||||
remark: string;
|
||||
}
|
||||
|
||||
export function createEmptyProjectPartyItem(
|
||||
roles: ProjectPartyEditorRole[],
|
||||
initial?: Partial<Omit<ProjectPartyEditorItem, 'key'>>
|
||||
): ProjectPartyEditorItem {
|
||||
return {
|
||||
key: crypto.randomUUID(),
|
||||
customerId: initial?.customerId ?? '',
|
||||
role: initial?.role ?? roles[0]?.id ?? '',
|
||||
remark: initial?.remark ?? ''
|
||||
};
|
||||
}
|
||||
|
||||
export function ProjectPartiesEditor({
|
||||
customers,
|
||||
roles,
|
||||
items,
|
||||
onChange
|
||||
}: {
|
||||
customers: ProjectPartyEditorCustomer[];
|
||||
roles: ProjectPartyEditorRole[];
|
||||
items: ProjectPartyEditorItem[];
|
||||
onChange: (items: ProjectPartyEditorItem[]) => void;
|
||||
}) {
|
||||
function updateItem(
|
||||
key: string,
|
||||
field: keyof Omit<ProjectPartyEditorItem, 'key'>,
|
||||
value: string
|
||||
) {
|
||||
onChange(
|
||||
items.map((item) => (item.key === key ? { ...item, [field]: value } : item))
|
||||
);
|
||||
}
|
||||
|
||||
function removeItem(key: string) {
|
||||
onChange(items.filter((item) => item.key !== key));
|
||||
}
|
||||
|
||||
function addItem() {
|
||||
onChange([...items, createEmptyProjectPartyItem(roles)]);
|
||||
}
|
||||
|
||||
return (
|
||||
<div className='space-y-3'>
|
||||
<div className='flex items-center justify-between gap-3'>
|
||||
<div>
|
||||
<div className='text-sm font-medium'>Project Parties</div>
|
||||
<p className='text-muted-foreground text-sm'>
|
||||
Add related companies with their role and optional remark.
|
||||
</p>
|
||||
</div>
|
||||
<Button type='button' variant='outline' size='sm' onClick={addItem}>
|
||||
<Icons.add className='mr-2 h-4 w-4' /> Add Party
|
||||
</Button>
|
||||
</div>
|
||||
|
||||
{!items.length ? (
|
||||
<div className='text-muted-foreground rounded-lg border border-dashed p-4 text-sm'>
|
||||
No project parties yet. Billing customer will still stay separate from this list.
|
||||
</div>
|
||||
) : null}
|
||||
|
||||
<div className='space-y-3'>
|
||||
{items.map((item, index) => (
|
||||
<div key={item.key} className='rounded-lg border p-4'>
|
||||
<div className='mb-3 flex items-center justify-between gap-3'>
|
||||
<div className='text-sm font-medium'>Party {index + 1}</div>
|
||||
<Button
|
||||
type='button'
|
||||
variant='ghost'
|
||||
size='sm'
|
||||
onClick={() => removeItem(item.key)}
|
||||
>
|
||||
<Icons.trash className='mr-2 h-4 w-4' /> Remove
|
||||
</Button>
|
||||
</div>
|
||||
<div className='grid gap-3 md:grid-cols-3'>
|
||||
<Select
|
||||
value={item.customerId || '__none__'}
|
||||
onValueChange={(value) =>
|
||||
updateItem(item.key, 'customerId', value === '__none__' ? '' : value)
|
||||
}
|
||||
>
|
||||
<SelectTrigger>
|
||||
<SelectValue placeholder='Customer' />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
<SelectItem value='__none__'>Select customer</SelectItem>
|
||||
{customers.map((customer) => (
|
||||
<SelectItem key={customer.id} value={customer.id}>
|
||||
{customer.name} ({customer.code})
|
||||
</SelectItem>
|
||||
))}
|
||||
</SelectContent>
|
||||
</Select>
|
||||
|
||||
<Select value={item.role || '__none__'} onValueChange={(value) => updateItem(item.key, 'role', value === '__none__' ? '' : value)}>
|
||||
<SelectTrigger>
|
||||
<SelectValue placeholder='Role' />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
<SelectItem value='__none__'>Select role</SelectItem>
|
||||
{roles.map((role) => (
|
||||
<SelectItem key={role.id} value={role.id}>
|
||||
{role.label}
|
||||
</SelectItem>
|
||||
))}
|
||||
</SelectContent>
|
||||
</Select>
|
||||
|
||||
<Input
|
||||
value={item.remark}
|
||||
onChange={(event) => updateItem(item.key, 'remark', event.target.value)}
|
||||
placeholder='Remark'
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user