Skip to content

Commit ac3662d

Browse files
committed
improvements for attendance - remove login requirement to join, add flag to show manual joining, improve ux for manual joining
1 parent c67fa08 commit ac3662d

File tree

6 files changed

+111
-44
lines changed

6 files changed

+111
-44
lines changed

apps/webapp/src/app/test/attendance/page.tsx

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,7 @@ export default function AttendanceTestPage() {
2323
attendanceKey="weekly-team-meeting"
2424
title="Weekly Team Meeting"
2525
expectedNames={attendeeNames}
26+
remarksPlaceholder="Here are some custom remarks"
2627
/>
2728
</div>
2829
);

apps/webapp/src/modules/attendance/components/Attendance.tsx

Lines changed: 45 additions & 29 deletions
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,7 @@ import { useCurrentUser } from '@/modules/auth/AuthProvider';
1515
import { api } from '@workspace/backend/convex/_generated/api';
1616
import type { Doc } from '@workspace/backend/convex/_generated/dataModel';
1717
import { useSessionQuery } from 'convex-helpers/react/sessions';
18-
import { CheckCircle2, ChevronDown, LogIn, XCircle } from 'lucide-react';
18+
import { CheckCircle2, ChevronDown, UserPlus, XCircle } from 'lucide-react';
1919
import { useRouter, useSearchParams } from 'next/navigation';
2020
import { Suspense, useCallback, useEffect, useState } from 'react';
2121
import { AttendanceDialog } from './AttendanceDialog';
@@ -25,13 +25,15 @@ interface AttendanceModuleProps {
2525
attendanceKey: string;
2626
title: string;
2727
expectedNames?: string[];
28+
remarksPlaceholder?: string;
2829
}
2930

3031
// Internal component that uses useSearchParams
3132
const AttendanceContent = ({
3233
attendanceKey,
3334
title = 'Attendance',
3435
expectedNames = [],
36+
remarksPlaceholder,
3537
}: AttendanceModuleProps) => {
3638
const router = useRouter();
3739
const searchParams = useSearchParams();
@@ -42,8 +44,8 @@ const AttendanceContent = ({
4244
const [selectedPerson, setSelectedPerson] = useState<string>('');
4345
const [searchQuery, setSearchQuery] = useState('');
4446
const [showFullListModal, setShowFullListModal] = useState(false);
45-
const [loginDialogOpen, setLoginDialogOpen] = useState(false);
4647
const [modalSearchQuery, setModalSearchQuery] = useState('');
48+
const [isManualJoin, setIsManualJoin] = useState(false);
4749
const attendanceData = useSessionQuery(api.attendance.getAttendanceData, {
4850
attendanceKey,
4951
});
@@ -102,13 +104,10 @@ const AttendanceContent = ({
102104
}
103105

104106
const handleJoin = () => {
105-
if (!isAuthenticated || !currentUser) {
106-
setLoginDialogOpen(true);
107-
return;
108-
}
109-
110-
// Open the dialog with the current user's name pre-selected
111-
setSelectedPerson(currentUser.name);
107+
// If user is authenticated, pre-fill their name, otherwise open with empty name
108+
const defaultName = isAuthenticated && currentUser ? currentUser.name : '';
109+
setSelectedPerson(defaultName);
110+
setIsManualJoin(true); // Always true when using the join button
112111
setDialogOpen(true);
113112
};
114113

@@ -118,12 +117,17 @@ const AttendanceContent = ({
118117

119118
const handlePersonClick = (name: string) => {
120119
setSelectedPerson(name);
120+
// If the name is not in the expected list, consider it a manual join
121+
// This handles cases where someone manually added themselves but we're now clicking on their name
122+
const wasInExpectedList = expectedNames?.includes(name);
123+
setIsManualJoin(!wasInExpectedList);
121124
setDialogOpen(true);
122125
};
123126

124127
const handleDialogClose = () => {
125128
setDialogOpen(false);
126129
setSelectedPerson('');
130+
setIsManualJoin(false);
127131
};
128132

129133
// Handle successful attendance submission
@@ -250,7 +254,11 @@ const AttendanceContent = ({
250254

251255
<TabsContent value="responded">
252256
{respondedNames.length === 0 ? (
253-
<AttendanceEmptyState message="No results found" onJoin={handleJoin} />
257+
<AttendanceEmptyState
258+
message="No results found"
259+
onJoin={handleJoin}
260+
showJoinButton={!searchQuery.trim()}
261+
/>
254262
) : (
255263
<div className="space-y-2">
256264
{respondedNames.slice(0, 7).map((name) => {
@@ -316,7 +324,11 @@ const AttendanceContent = ({
316324

317325
<TabsContent value="pending">
318326
{pendingNames.length === 0 ? (
319-
<AttendanceEmptyState message="No results found" onJoin={handleJoin} />
327+
<AttendanceEmptyState
328+
message="No results found"
329+
onJoin={handleJoin}
330+
showJoinButton={!searchQuery.trim()}
331+
/>
320332
) : (
321333
<div className="space-y-2">
322334
{pendingNames.slice(0, 7).map((name) => {
@@ -360,28 +372,25 @@ const AttendanceContent = ({
360372
)}
361373
</TabsContent>
362374
</Tabs>
375+
376+
{/* Join button below component */}
377+
{(!isAuthenticated || !isCurrentUserRegistered) && (
378+
<div className="mt-6 text-center">
379+
<p className="text-muted-foreground mb-2">Don't see your name?</p>
380+
<Button
381+
onClick={handleJoin}
382+
className="flex items-center justify-center mx-auto w-fit"
383+
variant="outline"
384+
>
385+
<UserPlus className="h-4 w-4 mr-2" />
386+
Join the list
387+
</Button>
388+
</div>
389+
)}
363390
</>
364391
)}
365392
</div>
366393

367-
{/* Login dialog */}
368-
<Dialog open={loginDialogOpen} onOpenChange={setLoginDialogOpen}>
369-
<DialogContent>
370-
<DialogHeader>
371-
<DialogTitle>Login Required</DialogTitle>
372-
<DialogDescription>You need to be logged in to mark your attendance.</DialogDescription>
373-
</DialogHeader>
374-
<DialogFooter className="flex justify-end gap-2 mt-4">
375-
<Button variant="outline" onClick={() => setLoginDialogOpen(false)}>
376-
Cancel
377-
</Button>
378-
<Button onClick={() => router.push('/login')} className="flex items-center">
379-
<LogIn className="h-4 w-4 mr-2" /> Go to Login
380-
</Button>
381-
</DialogFooter>
382-
</DialogContent>
383-
</Dialog>
384-
385394
{/* Full list modal - update to show based on active tab */}
386395
<Dialog open={showFullListModal} onOpenChange={setShowFullListModal}>
387396
<DialogContent className="sm:max-w-[425px]">
@@ -418,12 +427,16 @@ const AttendanceContent = ({
418427
type="button"
419428
onClick={() => {
420429
setSelectedPerson(name);
430+
const wasInExpectedList = expectedNames?.includes(name);
431+
setIsManualJoin(!wasInExpectedList);
421432
setDialogOpen(true);
422433
setShowFullListModal(false);
423434
}}
424435
onKeyDown={(e) => {
425436
if (e.key === 'Enter') {
426437
setSelectedPerson(name);
438+
const wasInExpectedList = expectedNames?.includes(name);
439+
setIsManualJoin(!wasInExpectedList);
427440
setDialogOpen(true);
428441
setShowFullListModal(false);
429442
}
@@ -465,6 +478,7 @@ const AttendanceContent = ({
465478
setShowFullListModal(false);
466479
handleJoin();
467480
}}
481+
showJoinButton={!modalSearchQuery.trim()}
468482
/>
469483
)}
470484
</ScrollArea>
@@ -480,6 +494,8 @@ const AttendanceContent = ({
480494
attendanceKey={attendanceKey}
481495
attendanceRecords={attendanceRecords}
482496
onSuccess={handleAttendanceSuccess}
497+
isManuallyJoined={isManualJoin}
498+
remarksPlaceholder={remarksPlaceholder}
483499
/>
484500
)}
485501
</>

apps/webapp/src/modules/attendance/components/AttendanceDialog.tsx

Lines changed: 47 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@ import {
77
DialogHeader,
88
DialogTitle,
99
} from '@/components/ui/dialog';
10+
import { Input } from '@/components/ui/input';
1011
import { Label } from '@/components/ui/label';
1112
import {
1213
Select,
@@ -33,6 +34,8 @@ interface AttendanceDialogProps {
3334
attendanceKey?: string;
3435
attendanceRecords: Doc<'attendanceRecords'>[];
3536
onSuccess?: () => void;
37+
isManuallyJoined: boolean;
38+
remarksPlaceholder?: string;
3639
}
3740

3841
export function AttendanceDialog({
@@ -42,6 +45,8 @@ export function AttendanceDialog({
4245
attendanceKey,
4346
attendanceRecords,
4447
onSuccess,
48+
isManuallyJoined,
49+
remarksPlaceholder,
4550
}: AttendanceDialogProps) {
4651
const currentUser = useCurrentUser();
4752
const isAuthenticated = currentUser !== undefined;
@@ -63,8 +68,6 @@ export function AttendanceDialog({
6368
? 'self'
6469
: 'other';
6570

66-
console.log({ isCurrentUserResponse, existingRecord, currentUser, defaultRespondAs });
67-
6871
const [respondAs, setRespondAs] = useState<'self' | 'other'>(defaultRespondAs);
6972
const [status, setStatus] = useState<AttendanceStatus>(
7073
(existingRecord?.status as AttendanceStatus) || AttendanceStatus.ATTENDING
@@ -73,6 +76,7 @@ export function AttendanceDialog({
7376
const [remarks, setRemarks] = useState(existingRecord?.remarks || '');
7477
const [loading, setLoading] = useState(false);
7578
const [deleteLoading, setDeleteLoading] = useState(false);
79+
const [enteredName, setEnteredName] = useState(personName || '');
7680

7781
const recordAttendance = useSessionMutation(api.attendance.recordAttendance);
7882
const deleteAttendanceRecord = useSessionMutation(api.attendance.deleteAttendanceRecord);
@@ -91,30 +95,43 @@ export function AttendanceDialog({
9195
}
9296
}, [existingRecord]);
9397

98+
useEffect(() => {
99+
setEnteredName(personName || '');
100+
}, [personName]);
101+
94102
const handleSubmit = useCallback(async () => {
103+
const nameToUse = !isAuthenticated || !personName ? enteredName : personName;
104+
105+
if (!nameToUse.trim()) {
106+
toast.error('Please enter your name');
107+
return;
108+
}
109+
95110
setLoading(true);
96111

97112
try {
98113
if (respondAs === 'self' && isAuthenticated) {
99114
await recordAttendance({
100-
name: personName,
115+
name: nameToUse,
101116
attendanceKey,
102117
status,
103118
reason: status === AttendanceStatus.NOT_ATTENDING ? reason : undefined,
104119
remarks: status === AttendanceStatus.ATTENDING ? remarks : undefined,
105120
self: true,
121+
isManuallyJoined,
106122
});
107123
toast.success('Your attendance has been recorded');
108124
} else {
109125
await recordAttendance({
110126
attendanceKey,
111-
name: personName,
127+
name: nameToUse,
112128
status,
113129
reason: status === AttendanceStatus.NOT_ATTENDING ? reason : undefined,
114130
remarks: status === AttendanceStatus.ATTENDING ? remarks : undefined,
115131
self: false,
132+
isManuallyJoined,
116133
});
117-
toast.success(`Attendance recorded for ${personName}`);
134+
toast.success(`Attendance recorded for ${nameToUse}`);
118135
}
119136

120137
// Call onSuccess callback if provided
@@ -135,11 +152,13 @@ export function AttendanceDialog({
135152
onClose,
136153
onSuccess,
137154
personName,
155+
enteredName,
138156
reason,
139157
recordAttendance,
140158
respondAs,
141159
status,
142160
remarks,
161+
isManuallyJoined,
143162
]);
144163

145164
// Handle keyboard shortcuts
@@ -160,12 +179,14 @@ export function AttendanceDialog({
160179
const handleDelete = async () => {
161180
if (!existingRecord) return;
162181

182+
const nameToUse = !isAuthenticated || !personName ? enteredName : personName;
183+
163184
setDeleteLoading(true);
164185
try {
165186
await deleteAttendanceRecord({
166187
recordId: existingRecord._id as Id<'attendanceRecords'>,
167188
});
168-
toast.success(`Deleted attendance record for ${personName}`);
189+
toast.success(`Deleted attendance record for ${nameToUse}`);
169190
onClose();
170191
} catch (error) {
171192
console.error('Failed to delete attendance record:', error);
@@ -191,13 +212,31 @@ export function AttendanceDialog({
191212
<DialogDescription className="text-sm opacity-80 mt-1">
192213
{isAuthenticated
193214
? 'Record attendance for yourself or for someone else.'
194-
: `Record attendance for ${personName}.`}
215+
: personName
216+
? `Record attendance for ${personName}.`
217+
: 'Enter your name and record your attendance.'}
195218
</DialogDescription>
196219
</DialogHeader>
197220
<div>
198221
<Separator className="mb-2" />
199222

200223
<div className="space-y-2">
224+
{/* Name input for anonymous users or when no name is provided */}
225+
{(!isAuthenticated || !personName) && (
226+
<div className="space-y-3 p-4 bg-muted/30 rounded-lg">
227+
<Label htmlFor="name-input" className="text-sm font-medium">
228+
Your Name
229+
</Label>
230+
<Input
231+
id="name-input"
232+
value={enteredName}
233+
onChange={(e) => setEnteredName(e.target.value)}
234+
placeholder="Enter your name"
235+
className="w-full"
236+
/>
237+
</div>
238+
)}
239+
201240
{isAuthenticated && (
202241
<div className="space-y-3 p-4 bg-muted/30 rounded-lg">
203242
<Label htmlFor="respond-as" className="text-sm font-medium">
@@ -272,7 +311,7 @@ export function AttendanceDialog({
272311
id="remarks"
273312
value={remarks}
274313
onChange={(e) => setRemarks(e.target.value)}
275-
placeholder="Any remarks or suggestions?"
314+
placeholder={remarksPlaceholder || 'Any remarks or suggestions?'}
276315
rows={3}
277316
className="resize-none"
278317
/>

apps/webapp/src/modules/attendance/components/AttendanceEmptyState.tsx

Lines changed: 14 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -4,18 +4,25 @@ import { UserPlus } from 'lucide-react';
44
interface AttendanceEmptyStateProps {
55
message: string;
66
onJoin: () => void;
7+
showJoinButton?: boolean;
78
}
89

9-
export const AttendanceEmptyState = ({ message, onJoin }: AttendanceEmptyStateProps) => {
10+
export const AttendanceEmptyState = ({
11+
message,
12+
onJoin,
13+
showJoinButton = true,
14+
}: AttendanceEmptyStateProps) => {
1015
return (
1116
<div className="h-full flex flex-col items-center justify-center space-y-4 py-10">
1217
<p className="font-bold">{message}</p>
13-
<div className="flex flex-col items-center space-y-2">
14-
<p className="text-muted-foreground text-sm">Don't see your name?</p>
15-
<Button variant="outline" onClick={onJoin} className="flex items-center">
16-
<UserPlus className="h-4 w-4 mr-2" /> Join the list now
17-
</Button>
18-
</div>
18+
{showJoinButton && (
19+
<div className="flex flex-col items-center space-y-2">
20+
<p className="text-muted-foreground text-sm">Don't see your name?</p>
21+
<Button variant="outline" onClick={onJoin} className="flex items-center">
22+
<UserPlus className="h-4 w-4 mr-2" /> Join the list now
23+
</Button>
24+
</div>
25+
)}
1926
</div>
2027
);
2128
};

0 commit comments

Comments
 (0)