Render groups using a tree table #146
| @ -13,20 +13,22 @@ export class Guest implements Entity { | ||||
|   groupId?: string; | ||||
|   color?: string; | ||||
|   status?: GuestStatus; | ||||
|   children?: Guest[]; | ||||
| 
 | ||||
|   constructor(id?: string, name?: string, group_name?: string, groupId?: string, color?: string, status?: GuestStatus) { | ||||
|   constructor(id?: string, name?: string, group_name?: string, groupId?: string, color?: string, status?: GuestStatus, children?: Guest[]) { | ||||
|     this.id = id; | ||||
|     this.name = name; | ||||
|     this.group_name = group_name; | ||||
|     this.groupId = groupId; | ||||
|     this.color = color; | ||||
|     this.status = status; | ||||
|     this.children = children; | ||||
|   } | ||||
| } | ||||
| 
 | ||||
| export class GuestSerializer implements Serializable<Guest> { | ||||
|   fromJson(data: any): Guest { | ||||
|     return new Guest(data.id, data.name, data.group_name, data.group_id, data.color, data.status); | ||||
|     return new Guest(data.id, data.name, data.group_name, data.group_id, data.color, data.status, data.children); | ||||
|   } | ||||
| 
 | ||||
|   toJson(guest: Guest): string { | ||||
|  | ||||
| @ -4,8 +4,11 @@ | ||||
| 
 | ||||
| import { Group, GroupSerializer } from '@/app/lib/group'; | ||||
| import TableOfContents from '../components/table-of-contents'; | ||||
| import { PencilIcon, TrashIcon } from '@heroicons/react/24/outline'; | ||||
| import { MapPinIcon, PencilIcon, TrashIcon } from '@heroicons/react/24/outline'; | ||||
| import { AbstractApi } from '@/app/api/abstract-api'; | ||||
| import { TreeTable } from 'primereact/treetable'; | ||||
| import { Column } from 'primereact/column'; | ||||
| import { TreeNode } from 'primereact/treenode'; | ||||
| 
 | ||||
| export default function GroupsTable({ groups, onUpdate, onEdit }: { | ||||
|   groups: Group[], | ||||
| @ -16,45 +19,85 @@ export default function GroupsTable({ groups, onUpdate, onEdit }: { | ||||
|   const api = new AbstractApi<Group>(); | ||||
|   const serializer = new GroupSerializer(); | ||||
| 
 | ||||
|   return ( | ||||
|     <TableOfContents | ||||
|       headers={['Name', 'Color', 'Confirmed', 'Tentative', 'Pending', 'Declined', 'Considered', 'Total', 'Actions']} | ||||
|       caption='Groups' | ||||
|       elements={groups} | ||||
|       rowRender={(group) => ( | ||||
|         <tr key={group.id} className="bg-white border-b odd:bg-white odd:dark:bg-gray-900 even:bg-gray-50 even:dark:bg-gray-800"> | ||||
|           <td scope="row" className="px-6 py-4 font-medium text-gray-900 whitespace-nowrap dark:text-white"> | ||||
|             {group.name} | ||||
|           </td> | ||||
|           <td className="px-6"> | ||||
|             <div className="w-8 h-8 rounded-full" style={{ backgroundColor: group.color }}></div> | ||||
|           </td> | ||||
|           <td className="px-6 text-lg"> | ||||
|             {group.attendance?.confirmed} | ||||
|           </td> | ||||
|           <td className="px-6 text-sm"> | ||||
|             {group.attendance?.tentative} | ||||
|           </td> | ||||
|           <td className="px-6 text-sm"> | ||||
|             {group.attendance?.invited} | ||||
|           </td> | ||||
|           <td className="px-6 text-sm"> | ||||
|             {group.attendance?.declined} | ||||
|           </td> | ||||
|           <td className="px-6 text-sm"> | ||||
|             {group.attendance?.considered} | ||||
|           </td> | ||||
|           <td className="px-6 text-sm"> | ||||
|             {group.attendance?.total} | ||||
|           </td> | ||||
|           <td> | ||||
|   const actions = (group: Group) => ( | ||||
|     <div className="flex flex-row items-center"> | ||||
|       <TrashIcon className='size-6 cursor-pointer' onClick={() => { api.destroy(serializer, group, onUpdate) }} /> | ||||
|       <PencilIcon className='size-6 cursor-pointer' onClick={() => onEdit(group)} /> | ||||
|     </div> | ||||
|           </td> | ||||
|         </tr> | ||||
|       )} | ||||
|     /> | ||||
|   ); | ||||
| 
 | ||||
|   const index = groups.reduce((acc, group) => { | ||||
|     if (group.id) { | ||||
|       acc.set(group.id, group) | ||||
|     } | ||||
| 
 | ||||
|     return acc; | ||||
|   }, new Map()); | ||||
| 
 | ||||
|   groups.forEach(group => { | ||||
|     if (group.parentId) { | ||||
|       const parent = index.get(group.parentId); | ||||
|       if (parent) { | ||||
|         if (!parent.children) { | ||||
|           parent.children = []; | ||||
|         } | ||||
|         parent.children.push(group); | ||||
|       } | ||||
|     } | ||||
|   }); | ||||
| 
 | ||||
|   const renderTree = (group: Group): TreeNode => { | ||||
|     const childrenAttendance = (group.children || []).reduce((acc, child) => { | ||||
|       acc.confirmed += child.attendance?.confirmed || 0; | ||||
|       acc.tentative += child.attendance?.tentative || 0; | ||||
|       acc.invited += child.attendance?.invited || 0; | ||||
|       acc.declined += child.attendance?.declined || 0; | ||||
|       acc.considered += child.attendance?.considered || 0; | ||||
|       acc.total += child.attendance?.total || 0; | ||||
|       return acc; | ||||
|     }, { confirmed: 0, tentative: 0, invited: 0, declined: 0, considered: 0, total: 0 }); | ||||
| 
 | ||||
|     return { | ||||
|       id: group.id, | ||||
|       key: group.id, | ||||
|       label: group.name, | ||||
|       data: { | ||||
|         name: group.name, | ||||
|         color: <div className="w-8 h-8 rounded-full" style={{ backgroundColor: group.color }} />, | ||||
|         confirmed: childrenAttendance.confirmed + (group.attendance?.confirmed || 0), | ||||
|         tentative: childrenAttendance.tentative + (group.attendance?.tentative || 0), | ||||
|         pending: childrenAttendance.invited + (group.attendance?.invited || 0), | ||||
|         declined: childrenAttendance.declined + (group.attendance?.declined || 0), | ||||
|         considered: childrenAttendance.considered + (group.attendance?.considered || 0), | ||||
|         total: childrenAttendance.total + (group.attendance?.total || 0), | ||||
|         actions: actions(group), | ||||
|       }, | ||||
|       children: group.children?.map(renderTree), | ||||
|     } | ||||
|   } | ||||
| 
 | ||||
|   const nodes: TreeNode[] = groups | ||||
|     .filter(group => !group.parentId) | ||||
|     .map(renderTree) | ||||
| 
 | ||||
|   const headers = ['Name', 'Color', 'Confirmed', 'Tentative', 'Pending', 'Declined', 'Considered', 'Total', 'Actions']; | ||||
|   const rowClassName = () => { | ||||
|     return { 'border-b odd:bg-white even:bg-gray-50 hover:bg-gray-100': true }; | ||||
|   } | ||||
| 
 | ||||
|   return ( | ||||
|     <> | ||||
|       <TreeTable value={nodes} rowClassName={rowClassName} className='py-4'> | ||||
|         <Column expander field="name" header="Name" className='w-2/5' /> | ||||
|         <Column field="color" header="Color" bodyClassName="text-sm" /> | ||||
|         <Column field="confirmed" header="Confirmed" bodyClassName="text-sm" /> | ||||
|         <Column field="tentative" header="Tentative" bodyClassName="text-sm" /> | ||||
|         <Column field="pending" header="Pending" bodyClassName="text-sm" /> | ||||
|         <Column field="declined" header="Declined" bodyClassName="text-sm" /> | ||||
|         <Column field="considered" header="Considered" bodyClassName="text-sm" /> | ||||
|         <Column field="total" header="Total" bodyClassName="text-sm" /> | ||||
|         <Column field="actions" header="Actions" /> | ||||
|       </TreeTable> | ||||
|     </> | ||||
|   ) | ||||
| } | ||||
|  | ||||
		Loading…
	
	
			
			x
			
			
		
	
		Reference in New Issue
	
	Block a user