taks-d.2.1

This commit is contained in:
phaichayon
2026-06-17 14:46:52 +07:00
parent 0a484e0b45
commit 5be6c54272
39 changed files with 6254 additions and 316 deletions

View 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>
);
}