Render groups using a tree table #146
| @ -13,20 +13,22 @@ export class Guest implements Entity { | |||||||
|   groupId?: string; |   groupId?: string; | ||||||
|   color?: string; |   color?: string; | ||||||
|   status?: GuestStatus; |   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.id = id; | ||||||
|     this.name = name; |     this.name = name; | ||||||
|     this.group_name = group_name; |     this.group_name = group_name; | ||||||
|     this.groupId = groupId; |     this.groupId = groupId; | ||||||
|     this.color = color; |     this.color = color; | ||||||
|     this.status = status; |     this.status = status; | ||||||
|  |     this.children = children; | ||||||
|   } |   } | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
| export class GuestSerializer implements Serializable<Guest> { | export class GuestSerializer implements Serializable<Guest> { | ||||||
|   fromJson(data: any): 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 { |   toJson(guest: Guest): string { | ||||||
|  | |||||||
| @ -4,8 +4,11 @@ | |||||||
| 
 | 
 | ||||||
| import { Group, GroupSerializer } from '@/app/lib/group'; | import { Group, GroupSerializer } from '@/app/lib/group'; | ||||||
| import TableOfContents from '../components/table-of-contents'; | 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 { 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 }: { | export default function GroupsTable({ groups, onUpdate, onEdit }: { | ||||||
|   groups: Group[], |   groups: Group[], | ||||||
| @ -16,45 +19,85 @@ export default function GroupsTable({ groups, onUpdate, onEdit }: { | |||||||
|   const api = new AbstractApi<Group>(); |   const api = new AbstractApi<Group>(); | ||||||
|   const serializer = new GroupSerializer(); |   const serializer = new GroupSerializer(); | ||||||
| 
 | 
 | ||||||
|  |   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> | ||||||
|  |   ); | ||||||
|  | 
 | ||||||
|  |   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 ( |   return ( | ||||||
|     <TableOfContents |     <> | ||||||
|       headers={['Name', 'Color', 'Confirmed', 'Tentative', 'Pending', 'Declined', 'Considered', 'Total', 'Actions']} |       <TreeTable value={nodes} rowClassName={rowClassName} className='py-4'> | ||||||
|       caption='Groups' |         <Column expander field="name" header="Name" className='w-2/5' /> | ||||||
|       elements={groups} |         <Column field="color" header="Color" bodyClassName="text-sm" /> | ||||||
|       rowRender={(group) => ( |         <Column field="confirmed" header="Confirmed" bodyClassName="text-sm" /> | ||||||
|         <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"> |         <Column field="tentative" header="Tentative" bodyClassName="text-sm" /> | ||||||
|           <td scope="row" className="px-6 py-4 font-medium text-gray-900 whitespace-nowrap dark:text-white"> |         <Column field="pending" header="Pending" bodyClassName="text-sm" /> | ||||||
|             {group.name} |         <Column field="declined" header="Declined" bodyClassName="text-sm" /> | ||||||
|           </td> |         <Column field="considered" header="Considered" bodyClassName="text-sm" /> | ||||||
|           <td className="px-6"> |         <Column field="total" header="Total" bodyClassName="text-sm" /> | ||||||
|             <div className="w-8 h-8 rounded-full" style={{ backgroundColor: group.color }}></div> |         <Column field="actions" header="Actions" /> | ||||||
|           </td> |       </TreeTable> | ||||||
|           <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> |  | ||||||
|             <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> |  | ||||||
|       )} |  | ||||||
|     /> |  | ||||||
|   ) |   ) | ||||||
| } | } | ||||||
|  | |||||||
		Loading…
	
	
			
			x
			
			
		
	
		Reference in New Issue
	
	Block a user