Skip to content

Commit ca7bffe

Browse files
committed
fix: add checks for ticket type and discount validity on submit, and fixed admin ui columns and registration delete
1 parent 31846bb commit ca7bffe

File tree

12 files changed

+120
-72
lines changed

12 files changed

+120
-72
lines changed

frontend/src/model/pycon/registrations.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -86,7 +86,7 @@ export const mapCreateRegistrationDataForPayment = (registration: RegisterFormVa
8686
futureVolunteer: registration.futureVolunteer,
8787
dietaryRestrictions: registration.dietaryRestrictions || null,
8888
accessibilityNeeds: registration.accessibilityNeeds || null,
89-
discountCode: registration.discountCode || null,
89+
discountCode: registration.discountPercentage ? registration.validCode || null : null,
9090
validIdObjectKey: registration.validIdObjectKey
9191
});
9292

frontend/src/pages/admin/event/pycon/registrations/RegistrationsColumns.tsx

Lines changed: 16 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -25,14 +25,23 @@ const getEnableHiding = (header: string) => showableHeaders.includes(header);
2525

2626
export const getRegistrationColumns = (): ColumnDef<Registration>[] => {
2727
const RegistrationColumns: ColumnDef<Registration>[] = [
28+
// {
29+
// id: 'select',
30+
// header: ({ table }) => (
31+
// <Checkbox checked={table.getIsAllPageRowsSelected()} onCheckedChange={(value) => table.toggleAllPageRowsSelected(!!value)} aria-label="Select all" />
32+
// ),
33+
// cell: ({ row }) => <Checkbox checked={row.getIsSelected()} onCheckedChange={(value) => row.toggleSelected(!!value)} aria-label="Select row" />,
34+
// enableSorting: false,
35+
// enableHiding: getEnableHiding('select')
36+
// },
2837
{
29-
id: 'select',
30-
header: ({ table }) => (
31-
<Checkbox checked={table.getIsAllPageRowsSelected()} onCheckedChange={(value) => table.toggleAllPageRowsSelected(!!value)} aria-label="Select all" />
32-
),
33-
cell: ({ row }) => <Checkbox checked={row.getIsSelected()} onCheckedChange={(value) => row.toggleSelected(!!value)} aria-label="Select row" />,
34-
enableSorting: false,
35-
enableHiding: getEnableHiding('select')
38+
id: 'actions',
39+
header: () => <span>View Regstration</span>,
40+
cell: ({ row }) => {
41+
const registrationInfo = row.original;
42+
return <RegistrationModal registrationInfo={registrationInfo} />;
43+
},
44+
enableHiding: getEnableHiding('actions')
3645
},
3746
{
3847
accessorKey: 'firstName',
@@ -245,16 +254,6 @@ export const getRegistrationColumns = (): ColumnDef<Registration>[] => {
245254
);
246255
},
247256
enableHiding: getEnableHiding('linkedInLink')
248-
},
249-
250-
{
251-
id: 'actions',
252-
header: () => <span>Actions</span>,
253-
cell: ({ row }) => {
254-
const registrationInfo = row.original;
255-
return <RegistrationModal registrationInfo={registrationInfo} />;
256-
},
257-
enableHiding: getEnableHiding('actions')
258257
}
259258
];
260259

frontend/src/pages/admin/event/pycon/registrations/useEditRegistrationForm.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -104,7 +104,7 @@ export const useEditRegistrationForm = (eventId: string, registrationInfo: Regis
104104
}
105105
});
106106

107-
const onDelete = form.handleSubmit(async () => {
107+
const onDelete = async () => {
108108
try {
109109
const response = await api.execute(deleteRegistration(eventId, registrationId));
110110

@@ -130,7 +130,7 @@ export const useEditRegistrationForm = (eventId: string, registrationInfo: Regis
130130
description: errorData?.message || 'An error occurred while deleting registration. Please try again.'
131131
});
132132
}
133-
});
133+
};
134134

135135
return {
136136
form,

frontend/src/pages/client/pycon/hooks/useRegisterForm.ts

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -93,6 +93,7 @@ const RegisterFormSchema = z.object({
9393
discountedPrice: z.number().optional(),
9494
total: z.number(),
9595
// ----- //
96+
validCode: z.string().optional(),
9697
agreeToDataUse: z.literal(true, { error: 'Please agree to data use policy to proceed' }),
9798
agreeToCodeOfConduct: z.literal(true, { error: 'Please agree to our code of conduct to proceed' })
9899
});
@@ -116,8 +117,10 @@ export const REGISTER_FIELDS: RegisterFieldMap = {
116117
export const useRegisterForm = (eventId: string, navigateOnSuccess: () => void) => {
117118
const { successToast, errorToast } = useNotifyToast();
118119
const api = useApi();
120+
119121
const auth = useCurrentUser();
120122
const userEmail = auth?.user?.email ?? '';
123+
121124
const [searchParams] = useSearchParams();
122125
const transactionIdFromUrl = searchParams.get('paymentTransactionId');
123126

@@ -161,6 +164,7 @@ export const useRegisterForm = (eventId: string, navigateOnSuccess: () => void)
161164
amountPaid: 0,
162165
discountCode: '',
163166
// ----- //
167+
validCode: '',
164168
transactionId: '',
165169
paymentMethod: '',
166170
paymentChannel: '',

frontend/src/pages/client/pycon/register/Register.tsx

Lines changed: 2 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -32,7 +32,7 @@ const Register: FC = () => {
3232
const [eventInfo, setEventInfo] = useState<Event | null>(null);
3333
const [isFeesLoading, setIsFeesLoading] = useState(false);
3434

35-
const { response, isPending } = useRegisterPage(eventId!, setCurrentStep);
35+
const { response, isPending } = useRegisterPage(eventId!, setCurrentStep, setEventInfo);
3636
const { form, onSubmit } = useRegisterForm(eventId!, navigateOnSuccess);
3737
const { isSuccessLoading, isRegisterSuccessful, retryRegister } = useSuccess(currentStep, form.getValues, onSubmit);
3838

@@ -48,15 +48,10 @@ const Register: FC = () => {
4848
return <Skeleton className="w-full h-full" />;
4949
}
5050

51-
if (!response || (response && !response.data && response.errorData)) {
51+
if (!response || (response && !response.data && response.errorData) || !eventInfo) {
5252
return <ErrorPage error={response} />;
5353
}
5454

55-
if (!eventInfo) {
56-
setEventInfo(response.data);
57-
return null;
58-
}
59-
6055
if (eventInfo.status === 'draft') {
6156
return <ErrorPage />;
6257
}

frontend/src/pages/client/pycon/register/footer/useRegisterFooter.ts

Lines changed: 55 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -2,14 +2,15 @@ import { useState } from 'react';
22
import { useNavigate } from 'react-router-dom';
33
import { useFormContext, useWatch } from 'react-hook-form';
44
import { ulid } from 'ulid';
5-
import { getEventRegCountStatus } from '@/api/events';
5+
import { getDiscount } from '@/api/discounts';
6+
import { getEvent, getEventRegCountStatus } from '@/api/events';
67
import { Event } from '@/model/events';
78
import { getPathFromUrl, isEmpty, reloadPage, scrollToView } from '@/utils/functions';
89
import { useApi } from '@/hooks/useApi';
910
import { useNotifyToast } from '@/hooks/useNotifyToast';
1011
import { RegisterField, RegisterFormValues } from '../../hooks/useRegisterForm';
1112
import { calculateTotalPrice } from '../pricing';
12-
import { RegisterStep, STEP_SUCCESS } from '../steps/RegistrationSteps';
13+
import { RegisterStep, STEP_PAYMENT, STEP_SUCCESS, STEP_TICKET_SELECTION } from '../steps/RegistrationSteps';
1314
import { usePayment } from '../usePayment';
1415

1516
export const useRegisterFooter = (
@@ -77,6 +78,7 @@ export const useRegisterFooter = (
7778
sprintDayPrice: sprintDay && event.sprintDayPrice ? event.sprintDayPrice : 0
7879
}).toFixed(2)
7980
);
81+
8082
setValue('total', total);
8183
};
8284

@@ -167,7 +169,7 @@ export const useRegisterFooter = (
167169
return;
168170
}
169171

170-
const total = getValues('total');
172+
const [total, selectedTicket, discountCode] = getValues(['total', 'ticketType', 'validCode']);
171173

172174
if (event.isApprovalFlow && event.status === 'preregistration') {
173175
setCurrentStep(STEP_SUCCESS);
@@ -190,6 +192,56 @@ export const useRegisterFooter = (
190192
setPaymentTotal();
191193
}
192194

195+
if (selectedTicket) {
196+
// refetch event to get latest info
197+
const event = await api.execute(getEvent(eventId));
198+
199+
if (!event.data || event.status !== 200) {
200+
return;
201+
}
202+
203+
const ticketTypeFromEvent = event.data.ticketTypes?.find((x) => x.id === selectedTicket);
204+
205+
// return error if ticket type is not found
206+
if (!ticketTypeFromEvent) {
207+
errorToast({
208+
title: 'Ticket type not found',
209+
description: 'Your ticket type is invalid. Please select another ticket type.'
210+
});
211+
setCurrentStep(STEP_TICKET_SELECTION);
212+
return;
213+
}
214+
215+
// return error if ticket type is sold out
216+
if (ticketTypeFromEvent.maximumQuantity === ticketTypeFromEvent.currentSales) {
217+
errorToast({
218+
title: 'Ticket type is sold out',
219+
description: 'Sorry, but your selected ticket type is already sold out. Please select another ticket type to register.'
220+
});
221+
setCurrentStep(STEP_TICKET_SELECTION);
222+
return;
223+
}
224+
}
225+
226+
// recheck discount code
227+
if (discountCode) {
228+
const response = await api.execute(getDiscount(discountCode, eventId));
229+
const discount = response.data;
230+
231+
const isDiscountUsedUp = discount.maxDiscountUses !== null ? discount.remainingUses === 0 : discount.claimed;
232+
233+
// return error if discount code is used up
234+
if (isDiscountUsedUp) {
235+
errorToast({
236+
title: 'Discount Code is already used up',
237+
description: 'The discount code you entered has already been claimed to its maximum. Please enter a different discount code.'
238+
});
239+
setValue('validCode', '');
240+
setCurrentStep(STEP_PAYMENT);
241+
return;
242+
}
243+
}
244+
193245
try {
194246
setIsFormSubmitting(true);
195247
await onRequestPayment();

frontend/src/pages/client/pycon/register/steps/PaymentAndVerificationStep.tsx

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -19,7 +19,7 @@ interface Props {
1919
}
2020

2121
const PaymentAndVerificationStep = ({ event: { eventId, price, platformFee, sprintDayPrice }, isFeesLoading, setIsFeesLoading }: Props) => {
22-
const { control, setValue } = useFormContext<RegisterFormValues>();
22+
const { control, setValue, getValues } = useFormContext<RegisterFormValues>();
2323
const [transactionFee, sprintDay] = useWatch({ name: ['transactionFee', 'sprintDay'], control });
2424
const { discountPercentage, isValidatingDiscountCode, validateDiscountCode } = useDiscount(price);
2525
const { getTransactionFee } = useTransactionFee(price, platformFee, setIsFeesLoading, discountPercentage, sprintDayPrice);
@@ -44,7 +44,7 @@ const PaymentAndVerificationStep = ({ event: { eventId, price, platformFee, spri
4444

4545
// Recalculate transaction fee when discount changes
4646
useEffect(() => {
47-
const [paymentChannel, paymentMethod] = ['paymentChannel', 'paymentMethod'].map((name) => control._formValues?.[name as keyof RegisterFormValues]);
47+
const [paymentChannel, paymentMethod] = getValues(['paymentChannel', 'paymentMethod']);
4848

4949
if (paymentChannel && paymentMethod) {
5050
getTransactionFee();

frontend/src/pages/client/pycon/register/steps/SummaryStep.tsx

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -26,7 +26,7 @@ const SummaryStep = ({ event }: SummaryProps) => {
2626
linkedInLink,
2727
ticketTypeId,
2828
sprintDay,
29-
discountCode,
29+
validCode,
3030
discountPercentage,
3131
transactionFee,
3232
discountedPrice,
@@ -52,7 +52,7 @@ const SummaryStep = ({ event }: SummaryProps) => {
5252
'linkedInLink',
5353
'ticketType',
5454
'sprintDay',
55-
'discountCode',
55+
'validCode',
5656
'discountPercentage',
5757
'transactionFee',
5858
'discountedPrice',
@@ -181,11 +181,11 @@ const SummaryStep = ({ event }: SummaryProps) => {
181181
<span className="font-bold">Price:</span>
182182
<p>{formatMoney(event.price, 'PHP')}</p>
183183

184-
{discountPercentage && discountCode && discountedPrice ? (
184+
{discountPercentage && validCode && discountedPrice ? (
185185
<>
186186
<span className="font-bold">Discount Code: </span>
187-
<span className="break-words" title={discountCode}>
188-
{discountCode}
187+
<span className="break-words" title={validCode}>
188+
{validCode}
189189
</span>
190190

191191
<span className="font-bold">Discount</span>

frontend/src/pages/client/pycon/register/steps/TicketSelectionStep.tsx

Lines changed: 15 additions & 33 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,9 @@
11
import { FC } from 'react';
22
import { Check, Star, Zap, Calendar, Users, Coffee } from 'lucide-react';
3-
import { useFormContext, useWatch } from 'react-hook-form';
3+
import { useFormContext } from 'react-hook-form';
44
import Button from '@/components/Button';
55
import { CardContent, CardDescription, CardFooter, CardHeader, CardTitle } from '@/components/Card';
66
import { FormItem, FormLabel, FormError } from '@/components/Form';
7-
import Label from '@/components/Label';
8-
import { RadioGroup, RadioGroupItem } from '@/components/RadioGroup';
97
import { Event } from '@/model/events';
108
import { cn } from '@/utils/classes';
119
import { formatMoney, formatPercentage } from '@/utils/functions';
@@ -16,13 +14,13 @@ interface Props {
1614
updateEventPrice: (newPrice: number) => void;
1715
}
1816

19-
const shirtSizeOptions = ['XS', 'S', 'M', 'L', 'XL', 'XXL', 'XXXL'] as const;
17+
// const shirtSizeOptions = ['XS', 'S', 'M', 'L', 'XL', 'XXL', 'XXXL'] as const;
2018

2119
const TicketSelectionStep = ({ event, updateEventPrice }: Props) => {
22-
const shirtSizeLink = import.meta.env.VITE_COMMDAY_SHIRT_SIZE_LINK;
20+
// const shirtSizeLink = import.meta.env.VITE_COMMDAY_SHIRT_SIZE_LINK;
2321

2422
const { control } = useFormContext<RegisterFormValues>();
25-
const [availTShirt] = useWatch({ control, name: ['availTShirt'] });
23+
// const [availTShirt] = useWatch({ control, name: ['availTShirt'] });
2624

2725
return (
2826
<>
@@ -150,6 +148,7 @@ interface TicketTypeProps {
150148
const TicketType: FC<TicketTypeProps> = ({ ticketType, benefits, subtitle, value, backgroundClass, star, updateEventPrice, onChange }) => {
151149
const isSelected = value === ticketType.id;
152150
const isKasosyo = ticketType.id === 'kasosyo';
151+
const isSoldOut = ticketType.maximumQuantity === ticketType.currentSales;
153152

154153
return (
155154
<div
@@ -159,16 +158,21 @@ const TicketType: FC<TicketTypeProps> = ({ ticketType, benefits, subtitle, value
159158
'hover:shadow-xl hover:scale-[1.02] hover:brightness-110',
160159
isKasosyo ? 'bg-gradient-to-br from-red-500 to-red-600 shadow-red-200/50' : 'bg-gradient-to-br from-orange-400 to-orange-500 shadow-orange-200/50',
161160
isSelected && 'ring-2 ring-white/10 shadow-lg',
161+
isSoldOut && 'grayscale-100 cursor-not-allowed shadow-none hover:scale-none hover:brightness-100 hover:shadow-none select-none',
162162
backgroundClass
163163
)}
164164
onClick={() => {
165+
if (isSoldOut) {
166+
return;
167+
}
168+
165169
onChange(ticketType.id);
166170
updateEventPrice(ticketType.price);
167171
}}
168172
>
169173
<CardHeader className="pb-4">
170174
<CardTitle className={cn('flex flex-wrap font-black font-nunito text-2xl items-center gap-x-4 mb-3')}>
171-
{star && <Star fill="currentColor" className="animate-pulse" />}
175+
{star && <Star fill="currentColor" className={cn(!isSoldOut && 'animate-pulse')} />}
172176
{ticketType.name.toUpperCase()}
173177
<span className={cn('font-medium text-base opacity-90')}>{`( ${subtitle} )`}</span>
174178
</CardTitle>
@@ -201,6 +205,10 @@ const TicketType: FC<TicketTypeProps> = ({ ticketType, benefits, subtitle, value
201205
: 'bg-pycon-violet-dark/80 hover:bg-pycon-violet-light hover:shadow-xl hover:scale-105 text-white shadow-lg border border-transparent'
202206
)}
203207
onClick={(e) => {
208+
if (isSoldOut) {
209+
return;
210+
}
211+
204212
e.stopPropagation();
205213
onChange(ticketType.id);
206214
updateEventPrice(ticketType.price);
@@ -315,29 +323,3 @@ const SprintDaySection: FC<SprintDaySectionProps> = ({ isSelected, sprintDayPric
315323
};
316324

317325
export default TicketSelectionStep;
318-
319-
// <CardHeader>
320-
// <CardTitle className={cn('grid font-black font-nunito text-2xl items-center gap-x-4', 'grid-cols-[auto_auto_auto_1fr]')}>
321-
// {/* <div className="inline-flex gap-x-2 items-center">
322-
// </div> */}
323-
// <Star fill="currentColor" className={cn(!star && 'opacity-0')} />
324-
// {ticketType.name.toUpperCase()}
325-
// <span className={cn('font-medium text-base row-start-2 sm:row-start-1', 'col-start-2 sm:col-start-3')}>{`( ${subtitle} )`}</span>
326-
327-
// <div
328-
// className={cn(
329-
// 'text-pycon-custard-light font-nunito font-bold text-xl col-span-2 sm:col-span-1',
330-
// 'row-start-3 sm:row-start-1 ms-0 sm:col-start-3 sm:ms-auto',
331-
// 'col-start-2 sm:col-start-4 ms-0 sm:ms-auto'
332-
// )}
333-
// >
334-
// {formatMoney(ticketType.price, 'PHP')}
335-
// </div>
336-
// </CardTitle>
337-
// {ticketType.originalPrice && ticketType.price < ticketType.originalPrice && (
338-
// <CardDescription>
339-
// <span className="line-through text-gray-500 font-semibold">{formatMoney(ticketType.originalPrice, 'PHP')}</span>
340-
// <span className="ml-2 text-green-400 font-semibold">{formatPercentage(1 - ticketType.price / ticketType.originalPrice)} off</span>
341-
// </CardDescription>
342-
// )}
343-
// </CardHeader>

0 commit comments

Comments
 (0)