All checks were successful
		
		
	
	Check usage of free licenses / build-static-assets (pull_request) Successful in 1m9s
				
			Add copyright notice / copyright_notice (pull_request) Successful in 1m33s
				
			Build Nginx-based docker image / build-static-assets (push) Successful in 3m29s
				
			Playwright Tests / test (pull_request) Successful in 4m40s
				
			
		
			
				
	
	
		
			111 lines
		
	
	
		
			4.1 KiB
		
	
	
	
		
			TypeScript
		
	
	
	
	
	
			
		
		
	
	
			111 lines
		
	
	
		
			4.1 KiB
		
	
	
	
		
			TypeScript
		
	
	
	
	
	
| /* Copyright (C) 2024-2025 LibreWeddingPlanner contributors*/
 | |
| 
 | |
| 'use client';
 | |
| 
 | |
| import { AbstractApi } from '@/app/api/abstract-api';
 | |
| import { Group, GroupSerializer } from '@/app/lib/group';
 | |
| import { AdjustmentsHorizontalIcon, PencilIcon, TrashIcon } from '@heroicons/react/24/outline';
 | |
| import { Column } from 'primereact/column';
 | |
| import { TreeNode } from 'primereact/treenode';
 | |
| import { TreeTable } from 'primereact/treetable';
 | |
| 
 | |
| export default function GroupsTable({ groups, onUpdate, onEdit, onEditAffinities }: {
 | |
|   groups: Group[],
 | |
|   onUpdate: () => void,
 | |
|   onEdit: (group: Group) => void,
 | |
|   onEditAffinities: (group: Group) => void,
 | |
| }) {
 | |
| 
 | |
|   const api = new AbstractApi<Group>();
 | |
|   const serializer = new GroupSerializer();
 | |
| 
 | |
|   const actions = (group: Group) => (
 | |
|     <div className="flex flex-row items-center">
 | |
|       <TrashIcon className='size-6 cursor-pointer' onClick={() => {
 | |
|         if (window.confirm(`Are you sure you want to delete guest "${group.name}"?`)) {
 | |
|           api.destroy(serializer, group, onUpdate)
 | |
|         }
 | |
|       }}
 | |
|       />
 | |
|       <PencilIcon className='size-6 cursor-pointer' onClick={() => onEdit(group)} />
 | |
|       <AdjustmentsHorizontalIcon className='size-6 cursor-pointer' onClick={() => onEditAffinities(group)} />
 | |
|     </div>
 | |
|   );
 | |
| 
 | |
|   const index = groups.reduce((acc, group) => {
 | |
|     if (group.id) {
 | |
|       acc.set(group.id, group)
 | |
|     }
 | |
| 
 | |
|     return acc;
 | |
|   }, new Map());
 | |
| 
 | |
|   groups.forEach(group => group.children = []);
 | |
|   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>
 | |
|     </>
 | |
|   )
 | |
| }
 |