Define create and update forms and actions for groups #140
| @ -7,6 +7,7 @@ import { loadGuests } from '@/app/api/guests'; | |||||||
| import { Group, Guest } from '@/app/lib/definitions'; | import { Group, Guest } from '@/app/lib/definitions'; | ||||||
| import { classNames } from '@/app/ui/components/button'; | import { classNames } from '@/app/ui/components/button'; | ||||||
| import GuestFormDialog from '@/app/ui/components/guest-form-dialog'; | import GuestFormDialog from '@/app/ui/components/guest-form-dialog'; | ||||||
|  | import GroupFormDialog from '@/app/ui/components/group-form-dialog'; | ||||||
| import GroupsTable from '@/app/ui/groups/table'; | import GroupsTable from '@/app/ui/groups/table'; | ||||||
| import SkeletonTable from '@/app/ui/guests/skeleton-row'; | import SkeletonTable from '@/app/ui/guests/skeleton-row'; | ||||||
| import GuestsTable from '@/app/ui/guests/table'; | import GuestsTable from '@/app/ui/guests/table'; | ||||||
| @ -31,6 +32,7 @@ export default function Page() { | |||||||
| 
 | 
 | ||||||
|   const [groupsLoaded, setGroupsLoaded] = useState(false); |   const [groupsLoaded, setGroupsLoaded] = useState(false); | ||||||
|   const [groups, setGroups] = useState<Array<Group>>([]); |   const [groups, setGroups] = useState<Array<Group>>([]); | ||||||
|  |   const [groupBeingEdited, setGroupBeingEdited] = useState<Group | undefined>(undefined); | ||||||
| 
 | 
 | ||||||
|   const [guestsLoaded, setGuestsLoaded] = useState(false); |   const [guestsLoaded, setGuestsLoaded] = useState(false); | ||||||
|   const [guests, setGuests] = useState<Array<Guest>>([]); |   const [guests, setGuests] = useState<Array<Guest>>([]); | ||||||
| @ -59,7 +61,18 @@ export default function Page() { | |||||||
|           </div> |           </div> | ||||||
|         </ TabPanel> |         </ TabPanel> | ||||||
|         <TabPanel header="Groups" leftIcon="pi pi-sitemap mx-2"> |         <TabPanel header="Groups" leftIcon="pi pi-sitemap mx-2"> | ||||||
|           <div className="flex w-full items-center justify-between"> |           <div className="flex flex-col w-full items-center justify-between"> | ||||||
|  | 
 | ||||||
|  |           <button onClick={() => setGroupBeingEdited({})} className={classNames('primary')}>Add new</button> | ||||||
|  |             <GroupFormDialog | ||||||
|  |               key={groupBeingEdited?.id} | ||||||
|  |               groups={groups} | ||||||
|  |               onCreate={() => { refreshGroups(); setGroupBeingEdited(undefined) }} | ||||||
|  |               group={groupBeingEdited} | ||||||
|  |               visible={groupBeingEdited !== undefined} | ||||||
|  |               onHide={() => { setGroupBeingEdited(undefined) }} | ||||||
|  |             /> | ||||||
|  | 
 | ||||||
|             <Suspense fallback={<SkeletonTable />}> |             <Suspense fallback={<SkeletonTable />}> | ||||||
|               <GroupsTable groups={groups} /> |               <GroupsTable groups={groups} /> | ||||||
|             </Suspense> |             </Suspense> | ||||||
|  | |||||||
| @ -1,7 +1,7 @@ | |||||||
| /* Copyright (C) 2024 Manuel Bustillo*/ | /* Copyright (C) 2024 Manuel Bustillo*/ | ||||||
| 
 | 
 | ||||||
| import { Group } from '@/app/lib/definitions'; | import { Group } from '@/app/lib/definitions'; | ||||||
| import { getSlug } from '../lib/utils'; | import { getCsrfToken, getSlug } from '../lib/utils'; | ||||||
| 
 | 
 | ||||||
| export function loadGroups(onLoad?: (groups: Group[]) => void) { | export function loadGroups(onLoad?: (groups: Group[]) => void) { | ||||||
|   fetch(`/api/${getSlug()}/groups`) |   fetch(`/api/${getSlug()}/groups`) | ||||||
| @ -26,3 +26,42 @@ export function loadGroups(onLoad?: (groups: Group[]) => void) { | |||||||
|       return []; |       return []; | ||||||
|     }); |     }); | ||||||
| } | } | ||||||
|  | 
 | ||||||
|  | export function updateGroup(group: Group) { | ||||||
|  |   return fetch(`/api/${getSlug()}/groups/${group.id}`, | ||||||
|  |     { | ||||||
|  |       method: 'PUT', | ||||||
|  |       body: JSON.stringify({ group: {  | ||||||
|  |         name: group.name,  | ||||||
|  |         color: group.color,  | ||||||
|  |         icon: group.icon, | ||||||
|  |         parent_id: group.parentId | ||||||
|  |        } }), | ||||||
|  |       headers: { | ||||||
|  |         'Content-Type': 'application/json', | ||||||
|  |         'X-CSRF-TOKEN': getCsrfToken(), | ||||||
|  |       } | ||||||
|  |     }) | ||||||
|  |     .catch((error) => console.error(error)); | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | export function createGroup(group: Group, onCreate?: () => void) { | ||||||
|  |   fetch(`/api/${getSlug()}/groups`, { | ||||||
|  |     method: 'POST', | ||||||
|  |     body: JSON.stringify({  | ||||||
|  |       name: group.name,  | ||||||
|  |       color: group.color,  | ||||||
|  |       icon: group.icon, | ||||||
|  |       parent_id: group.parentId | ||||||
|  |      }), | ||||||
|  |     headers: { | ||||||
|  |       'Content-Type': 'application/json', | ||||||
|  |       'X-CSRF-TOKEN': getCsrfToken(), | ||||||
|  |     } | ||||||
|  |   }) | ||||||
|  |     .then((response) => response.json()) | ||||||
|  |     .then((data) => { | ||||||
|  |       onCreate && onCreate(); | ||||||
|  |     }) | ||||||
|  |     .catch((error) => console.error(error)); | ||||||
|  | } | ||||||
| @ -32,11 +32,12 @@ export type TableArrangement = { | |||||||
| } | } | ||||||
| 
 | 
 | ||||||
| export type Group = { | export type Group = { | ||||||
|   id: string; |   id?: string; | ||||||
|   name: string; |   name?: string; | ||||||
|   guest_count: number; |   guest_count?: number; | ||||||
|   icon: string; |   icon?: string; | ||||||
|   children: Group[]; |   children?: Group[]; | ||||||
|  |   parentId?: string; | ||||||
|   color?: string; |   color?: string; | ||||||
|   attendance?: AttendanceSummary |   attendance?: AttendanceSummary | ||||||
| }; | }; | ||||||
|  | |||||||
							
								
								
									
										91
									
								
								app/ui/components/group-form-dialog.tsx
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										91
									
								
								app/ui/components/group-form-dialog.tsx
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,91 @@ | |||||||
|  | /* Copyright (C) 2024 Manuel Bustillo*/ | ||||||
|  | 
 | ||||||
|  | 'use client'; | ||||||
|  | 
 | ||||||
|  | import { createGroup, updateGroup } from '@/app/api/groups'; | ||||||
|  | import { Group } from '@/app/lib/definitions'; | ||||||
|  | import { classNames } from '@/app/ui/components/button'; | ||||||
|  | import { Dialog } from 'primereact/dialog'; | ||||||
|  | import { ColorPicker } from 'primereact/colorpicker'; | ||||||
|  | import { Dropdown } from 'primereact/dropdown'; | ||||||
|  | import { FloatLabel } from 'primereact/floatlabel'; | ||||||
|  | import { InputText } from 'primereact/inputtext'; | ||||||
|  | import { useState } from 'react'; | ||||||
|  | 
 | ||||||
|  | export default function GroupFormDialog({groups, onCreate, onHide, group, visible }: { | ||||||
|  |   groups: Group[], | ||||||
|  |   onCreate?: () => void, | ||||||
|  |   onHide: () => void, | ||||||
|  |   group?: Group, | ||||||
|  |   visible: boolean, | ||||||
|  | }) { | ||||||
|  | 
 | ||||||
|  |   const [name, setName] = useState(group?.name || ''); | ||||||
|  |   const [icon, setIcon] = useState(group?.icon || ''); | ||||||
|  |   const [color, setColor] = useState<string>(group?.color || ''); | ||||||
|  |   const [parentId, setParentId] = useState(group?.parentId || ''); | ||||||
|  | 
 | ||||||
|  |   function resetForm() { | ||||||
|  |     setName(''); | ||||||
|  |     setIcon(''); | ||||||
|  |     setColor(''); | ||||||
|  |     setParentId(''); | ||||||
|  |   } | ||||||
|  | 
 | ||||||
|  |   function submitGroup() { | ||||||
|  |     if (!(name)) { | ||||||
|  |       return | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     if (group?.id !== undefined) { | ||||||
|  |       group.name = name; | ||||||
|  |       group.icon = icon; | ||||||
|  |       group.color = color; | ||||||
|  |       group.parentId = parentId; | ||||||
|  | 
 | ||||||
|  |       updateGroup(group).then(() => { | ||||||
|  |         resetForm(); | ||||||
|  |         onCreate && onCreate(); | ||||||
|  |       }); | ||||||
|  |     } else { | ||||||
|  |       group && createGroup({name, icon, color, parentId}, () => { | ||||||
|  |         resetForm(); | ||||||
|  |         onCreate && onCreate(); | ||||||
|  |       }); | ||||||
|  |     } | ||||||
|  |   } | ||||||
|  | 
 | ||||||
|  |   return ( | ||||||
|  |     <> | ||||||
|  | 
 | ||||||
|  |       <Dialog header="Add group" visible={visible} style={{ width: '60vw' }} onHide={onHide}> | ||||||
|  |         <div className="card flex justify-evenly py-5"> | ||||||
|  |           <FloatLabel> | ||||||
|  |             <InputText id="name" className='rounded-sm' value={name} onChange={(e) => setName(e.target.value)} /> | ||||||
|  |             <label htmlFor="name">Name</label> | ||||||
|  |           </FloatLabel> | ||||||
|  |           <FloatLabel> | ||||||
|  |             <InputText id="icon" className='rounded-sm' value={icon} onChange={(e) => setIcon(e.target.value)} /> | ||||||
|  |             <label htmlFor="icon">Icon</label> | ||||||
|  |           </FloatLabel> | ||||||
|  |           <FloatLabel> | ||||||
|  |             <ColorPicker value={color} format='hex' onChange={(e) => setColor(`#${e.value}`)} /> | ||||||
|  |             <label htmlFor="color" /> | ||||||
|  |           </FloatLabel> | ||||||
|  |           <FloatLabel> | ||||||
|  |             <Dropdown id="parentId" className='rounded-sm min-w-32' value={parentId} onChange={(e) => setParentId(e.target.value)} options={ | ||||||
|  |               groups.map((group) => { | ||||||
|  |                 return { label: group.name, value: group.id }; | ||||||
|  |               }) | ||||||
|  |             } /> | ||||||
|  |             <label htmlFor="parentId">Parent</label> | ||||||
|  |           </FloatLabel> | ||||||
|  | 
 | ||||||
|  |           <button className={classNames('primary')} onClick={submitGroup} disabled={!(name.length > 0)}> | ||||||
|  |             {group?.id !== undefined ? 'Update' : 'Create'} | ||||||
|  |           </button> | ||||||
|  |         </div> | ||||||
|  |       </Dialog> | ||||||
|  |     </> | ||||||
|  |   ); | ||||||
|  | } | ||||||
| @ -54,7 +54,7 @@ export default function GuestFormDialog({ groups, onCreate, onHide, guest, visib | |||||||
|   return ( |   return ( | ||||||
|     <> |     <> | ||||||
| 
 | 
 | ||||||
|       <Dialog header="Add guest" visible={visible} style={{ width: '50vw' }} onHide={onHide}> |       <Dialog header="Add guest" visible={visible} style={{ width: '60vw' }} onHide={onHide}> | ||||||
|         <div className="card flex justify-evenly py-5"> |         <div className="card flex justify-evenly py-5"> | ||||||
|           <FloatLabel> |           <FloatLabel> | ||||||
|             <InputText id="username" className='rounded-sm' value={name} onChange={(e) => setName(e.target.value)} /> |             <InputText id="username" className='rounded-sm' value={name} onChange={(e) => setName(e.target.value)} /> | ||||||
|  | |||||||
		Loading…
	
	
			
			x
			
			
		
	
		Reference in New Issue
	
	Block a user