From 1ab643900b5e27dd1516badd2e8f4c3d402ae5ab Mon Sep 17 00:00:00 2001 From: Niko Lindroos Date: Thu, 10 Oct 2024 12:46:47 +0300 Subject: [PATCH] feat: arrival status and mark as attended on ticket verification page KK-1224 --- src/common/translation/en.json | 17 +- src/common/translation/fi.json | 17 +- src/common/translation/sv.json | 590 ++++++++++++++---- src/domain/api/generatedTypes/graphql.tsx | 89 ++- .../ticketValidation/TicketValidationPage.tsx | 85 ++- src/domain/ticketValidation/mutations.ts | 19 + src/domain/ticketValidation/queries.ts | 17 + .../useUpdateTicketAttendedMutation.ts | 58 ++ .../ticketValidation/useVerifyTicketQuery.ts | 16 +- 9 files changed, 762 insertions(+), 146 deletions(-) create mode 100644 src/domain/ticketValidation/mutations.ts create mode 100644 src/domain/ticketValidation/queries.ts create mode 100644 src/domain/ticketValidation/useUpdateTicketAttendedMutation.ts diff --git a/src/common/translation/en.json b/src/common/translation/en.json index f5abbcb2..5a7625cd 100644 --- a/src/common/translation/en.json +++ b/src/common/translation/en.json @@ -510,7 +510,22 @@ "ticketValidation": { "valid": "Ticket is valid", "invalid": "Ticket is not valid", - "error": "Ticket validation failed" + "error": "Ticket validation failed", + "updateTicketAttended": { + "onSuccess": { + "attendedMessage": "Ticket owner is now marked as attended", + "unattendedMessage": "Ticket owner is now marked as unattended" + }, + "onError": { + "message": "Ticket processing ended with an error, and the arrival status could not be updated" + }, + "switchButton": { + "label": "Ticket owner has arrived" + } + }, + "arrivalStatus": { + "label": "Arrival status: %{attendedEnrolmentCount} / %{enrolmentCount}" + } }, "ticketSystemPassword": { "passwordsTab": { diff --git a/src/common/translation/fi.json b/src/common/translation/fi.json index f9aebdd4..00a7d093 100644 --- a/src/common/translation/fi.json +++ b/src/common/translation/fi.json @@ -510,7 +510,22 @@ "ticketValidation": { "valid": "Lippu on voimassa", "invalid": "Lippu ei ole voimassa", - "error": "Lipun tarkistaminen epäonnistui" + "error": "Lipun tarkistaminen epäonnistui", + "updateTicketAttended": { + "onSuccess": { + "attendedMessage": "Lipun omistaja on nyt merkitty saapuneeksi", + "unattendedMessage": "Lipun omistaja on nyt merkitty saapumattomaksi" + }, + "onError": { + "message": "Lipun käsittely päätyi virheeseen, eikä saapumisen tilaa voitu päivittää" + }, + "switchButton": { + "label": "Lipun omistaja saapunut paikalle" + } + }, + "arrivalStatus": { + "label": "Paikalle saapunut: %{attendedEnrolmentCount} / %{enrolmentCount}" + } }, "ticketSystemPassword": { "passwordsTab": { diff --git a/src/common/translation/sv.json b/src/common/translation/sv.json index cf247549..5a7625cd 100644 --- a/src/common/translation/sv.json +++ b/src/common/translation/sv.json @@ -1,108 +1,161 @@ { - "dashboard": { - "title": "Culture Kids admin" - }, - "languages": { - "FI": "Finnish", - "SV": "Swedish", - "EN": "English" + "application": { + "test": "Test" }, - "venues": { - "list": { - "title": "Venues" + "authentication": { + "callbackPage": { + "finishingAuthentication": "Finishing authentication" }, - "create": { - "title": "Create venue" + "authError": { + "title": "Login Failed", + "description": "Log out and try again", + "logOut": "Log out" }, + "unauthorizedPage": { + "backToLoginPage": "Back to login page", + "contactEmail": "kulttuurin.kummilapset@hel.fi", + "content": "Your account is not authorized to use Culture Kids Admin UI. If you need access, please contact", + "title": "Not authorized." + } + }, + "children": { "fields": { - "name": { - "label": "Name" - }, - "address": { - "label": "Address" - }, - "description": { - "label": "Description" + "birthyear": { + "label": "Year of birth" }, - "accessibilityInfo": { - "label": "Accessibility info" + "guardians": { + "fields": { + "email": { + "label": "Email" + }, + "phoneNumber": { + "label": "Phone number" + }, + "postalCode": { + "label": "Postal code" + } + }, + "label": "Guardian" }, - "arrivalInstructions": { - "label": "Arrival instructions" + "name": { + "label": "Name" }, - "additionalInfo": { - "label": "Additional info" + "occurrences": { + "label": "Events" } - } - }, - "events": { + }, "list": { - "title": "Events" + "title": "Culture Kids" }, "show": { - "tab": { - "label": "Event detail" - }, - "publish": { - "button": { - "label": "Publish" - }, - "onSuccess": { - "message": "Event published!" - }, - "onFailure": { - "message": "Event publish error." - }, - "confirm": { - "title": "Publish event \"%{eventName}\"", - "content": "Are you sure that you want to publish this event?" - } + "title": "Culture Kid" + } + }, + "dashboard": { + "title": "Culture Kids Admin Interface" + }, + "enrolments": { + "fields": { + "attended": { + "label": "" } + } + }, + "events": { + "actions": { + "create": "New Event" }, "create": { + "aside": { + "content": "" + }, "title": "Create event" }, "edit": { "title": "Edit event" }, "fields": { + "capacityPerOccurrence": { + "helperText": "", + "label": "Capacity per occurrence" + }, + "description": { + "helperText": "", + "label": "Description" + }, + "duration": { + "helperText": "", + "label": "Duration" + }, "id": { "label": "ID" }, + "image": { + "helperText": "", + "label": "Image" + }, + "imageAltText": { + "helperText": "", + "label": "Image alt text" + }, + "imageInput": { + "label": "Select image to upload" + }, + "language": { + "label": "Language" + }, "name": { "label": "Name" }, + "numOfOccurrences": { + "label": "Occcurences" + }, + "numOfEnrolments": { + "label": "Enrolments" + }, + "occurrences": { + "label": "Occurrences" + }, "participantsPerInvite": { - "label": "Participants per invite", "choices": { "CHILD_AND_GUARDIAN": { - "label": "Child and guardian" + "label": "1 child + 1 adult" + }, + "CHILD_AND_1_OR_2_GUARDIANS": { + "label": "1 child + 1–2 adults" }, "FAMILY": { "label": "Family" } - } - }, - "duration": { - "label": "Duration" - }, - "occurrences": { - "label": "Occurrences" - }, - "capacityPerOccurrence": { - "label": "Capacity per occurrence" + }, + "helperText": "", + "label": "Participants per invite" }, "publishedAt": { - "label": "Published at" - }, - "description": { - "label": "Description" + "label": "Published at", + "published": { + "label": "Published" + }, + "values": { + "NOT_PUBLISHED": "Not published" + } }, "shortDescription": { + "helperText": "", "label": "Short Description" }, - "language": { - "label": "Language" + "totalCapacity": { + "label": "Total capacity", + "unknown": "unknown" + }, + "ready": { + "label": "Ready to be published", + "label2": "Status", + "options": { + "ready": "Ready", + "notReady": "Unfinished", + "published": "Published" + } }, "ticketSystem": { "label": "Ticket system", @@ -112,94 +165,401 @@ }, "TICKETMASTER": { "label": "Ticketmaster" + }, + "LIPPUPISTE": { + "label": "Lippupiste" } } + }, + "ticketSystemEndTime": { + "label": "End time", + "helperText": "After this time, the event will be removed from event invites and upcoming events." + }, + "ticketSystemUrl": { + "label": "The event's URL address in the external ticket system" + }, + "validationErrors": { + "url": "Invalid URL address" + } + }, + "list": { + "title": "Events" + }, + "show": { + "publish": { + "button": { + "label": "Publish" + }, + "confirm": { + "content": "Are you sure that you want to publish this event?", + "title": "Publish event \"%{eventName}\"" + }, + "onFailure": { + "message": "Event publish error." + }, + "onSuccess": { + "message": "Event published!" + } + }, + "tab": { + "label": "Event detail" } } }, - "occurrences": { - "create": { - "title": "Add occurrence" + "guardian": { + "name": "Guardian's name", + "doesNotExist": "" + }, + "eventGroups": { + "actions": { + "create": { + "do": "New Event Group" + }, + "publish": { + "redo": "Republish Event Group", + "do": "Publish Event Group", + "success": "Event group published!", + "error": "Event group publish error", + "eventsNotReadyError": "All events are not ready for event group publishing.", + "confirm": { + "title": "Publish event group \"%{eventGroupName}\"", + "content": "Are you sure that you want to publish this event group?" + } + }, + "addEvent": { + "do": "Add Event" + } }, - "show": { - "title": "Occurrence" + "fields": { + "name": { + "label": "Name" + }, + "shortDescription": { + "label": "Short Description", + "helperText": "This will appear on the invite that is visible on the godchild's own page. Maximum 140 characters." + }, + "description": { + "label": "Description", + "helperText": "This description will appear on the event page and in the event invitation email sent to the godchild." + } + }, + "translations": { + "FI": { + "name": { + "required": "A Finnish name is required. Check the message's Finnish language version." + } + } + }, + "create": { + "title": { + "label": "New Event Group" + } + } + }, + "eventsAndEventGroups": { + "list": { + "label": "Event list", + "type": { + "label": "Type", + "event": { + "label": "Event" + }, + "eventGroup": { + "label": "Event Group" + } + }, + "eventCount": { + "label": "Events" + } + } + }, + "languages": { + "EN": "English", + "FI": "Finnish", + "SV": "Swedish" + }, + "messages": { + "send": { + "do": "Send", + "success": "Message sent", + "error": "Message send failed", + "confirm": { + "title": "Send message \"%{messageSubject}\"", + "content": "%{recipientCount} recipients. Are you sure that you want to send this message?" + } }, "fields": { - "time": { - "label": "Time", - "fields": { - "date": { - "label": "Date" + "recipientSelection": { + "choices": { + "ALL": { + "label": "All" }, - "time": { - "label": "Time" + "INVITED": { + "label": "Invited" + }, + "ENROLLED": { + "label": "Enrolled" + }, + "ATTENDED": { + "label": "Attended" + }, + "SUBSCRIBED_TO_FREE_SPOT_NOTIFICATION": { + "label": "Subscribed to free spot message" } + }, + "label": "Recipients" + }, + "bodyText": { + "label": "Message text", + "label2": "Message", + "error": { + "required": "Required" } }, - "venue": { - "label": "Venue" + "subject": { + "label": "Name", + "label2": "Message subject" }, "event": { - "label": "Event" + "label": "Event", + "all": "All Events" }, - "capacity": { - "label": "Capacity" + "recipientCount": { + "label": "Recipient count", + "label2": "Recipients" }, - "enrolmentsCount": { - "label": "Number of enrolments" + "sentAt": { + "label": "Published", + "sent": "Sent", + "notSent": "Not sent" }, - "children": { - "label": "Enrolled children" + "occurrences": { + "label": "Occurrences", + "all": "All Occurrences" + }, + "protocol": { + "label": "Type" } }, - "addOccurrenceButton": { - "label": "Add occurrence" + "create": { + "title": { + "email": "New email", + "sms": "New sms" + }, + "do": { + "email": "New email", + "sms": "New sms" + } + }, + "list": { + "title": "Messages" } }, - "children": { - "list": { - "title": "Culture kids" + "message": { + "translations": { + "FI": { + "subject": { + "required": "A Finnish subject is required. Check the message's Finnish language version." + }, + "bodyText": { + "required": "A Finnish message text is required. Check the message's Finnish language version." + } + } + } + }, + "occurrences": { + "addOccurrenceButton": { + "label": "Add occurrence" + }, + "create": { + "title": "Add occurrence" + }, + "edit": { + "delete": { + "confirm": { + "content": "This occurrence has been published already. Are you sure you still want to delete it?", + "title": "Delete occurrence" + } + } }, "fields": { - "name": { - "label": "Name" + "capacity": { + "label": "Capacity" }, - "birthyear": { - "label": "Year of birth" + "capacityOverride": { + "label": "Occurrence capacity", + "helperText": "" }, - "guardians": { - "label": "Guardian", + "children": { + "label": "Enrolled children" + }, + "enrolments": { "fields": { - "postalCode": { - "label": "Postal code" - }, - "email": { - "label": "Email" - }, - "phoneNumber": { - "label": "Phone number" + "attended": { + "choices": { + "null": "", + "true": "", + "false": "" + } } } }, - "occurrences": { - "label": "Events" + "enrolmentsCount": { + "label": "Number of enrolments" + }, + "attendedEnrolmentsCount": { + "label": "Attendee count" + }, + "event": { + "label": "Event" + }, + "freeSpotNotificationSubscriptions": { + "label": "In Queue" + }, + "ticketSystemUrl": { + "label": "Ticket system URL" + }, + "time": { + "fields": { + "date": { + "errorMessage": "Date invalid, use format dd.mm.yyyy", + "format": "dd.mm.yyyy", + "helperText": "", + "label": "Date" + }, + "time": { + "errorMessage": "Time invalid, use format hh:mm", + "format": "hh:mm", + "label": "Time" + } + }, + "helperText": "", + "label": "Time" + }, + "venue": { + "helperText": "", + "label": "Venue" } + }, + "show": { + "title": "Occurrence" } }, - "authentication": { - "redirect": { - "text": "Authenticating..." + "sms": { + "create": { + "do": "New sms", + "messageSentImmediatelyNotice": "The SMS is not saved, but sent immediately after send-button is clicked" + } + }, + "venues": { + "create": { + "aside": { + "content": "" + }, + "title": "Create venue" }, - "unauthorizedPage": { - "title": "Not authorized.", - "content": "Your account is not authorized to use Culture Kids Admin UI. If you need access, please contact", - "contactEmail": "kulttuurin.kummilapset@hel.fi", - "backToLoginPage": "Back to login page" + "edit": { + "title": "Edit venue" + }, + "fields": { + "accessibilityInfo": { + "helperText": "", + "label": "Accessibility info" + }, + "additionalInfo": { + "helperText": "", + "label": "Additional info" + }, + "address": { + "helperText": "", + "label": "Address" + }, + "arrivalInstructions": { + "helperText": "", + "label": "Arrival instructions" + }, + "description": { + "label": "Description" + }, + "name": { + "helperText": "", + "label": "Name" + }, + "wcAndFacilities": { + "label": "Toilets and child care facilities" + } + }, + "list": { + "aside": { + "content": "" + }, + "title": "Venues" } }, + "action": { + "send": "Send" + }, "ra": { "message": { "delete_title": "Delete" } + }, + "generic": { + "all": "All" + }, + "ticketValidation": { + "valid": "Ticket is valid", + "invalid": "Ticket is not valid", + "error": "Ticket validation failed", + "updateTicketAttended": { + "onSuccess": { + "attendedMessage": "Ticket owner is now marked as attended", + "unattendedMessage": "Ticket owner is now marked as unattended" + }, + "onError": { + "message": "Ticket processing ended with an error, and the arrival status could not be updated" + }, + "switchButton": { + "label": "Ticket owner has arrived" + } + }, + "arrivalStatus": { + "label": "Arrival status: %{attendedEnrolmentCount} / %{enrolmentCount}" + } + }, + "ticketSystemPassword": { + "passwordsTab": { + "label": "Passwords" + }, + "fields": { + "usedPasswordCount": { + "label": "Used external ticket system passwords" + }, + "freePasswordCount": { + "label": "Free passwords" + } + }, + "import": { + "dialog": { + "openButton": "Add passwords", + "title": "Add passwords", + "text": "Copy the passwords you received and paste them in the text field below in text form as a list below" + }, + "passwords": { + "ariaLabel": "Import passwords", + "placeholder": "0A1BC3D4E5\nABC123DEF\nXYZ098ASD" + }, + "action": { + "import": "Import", + "cancel": "Cancel" + }, + "submit": { + "passwords": { + "error": "Some passwords could not be imported: %{passwords}" + }, + "error": "Critical error in passwords importing", + "success": "Ticket system passwords imported!" + } + } } } diff --git a/src/domain/api/generatedTypes/graphql.tsx b/src/domain/api/generatedTypes/graphql.tsx index f13000ea..723e0b92 100644 --- a/src/domain/api/generatedTypes/graphql.tsx +++ b/src/domain/api/generatedTypes/graphql.tsx @@ -126,7 +126,7 @@ export type AdminNode = Node & { /** The ID of the object */ id: Scalars['ID']['output']; projects: Maybe; - /** Vaaditaan. Enintään 150 merkkiä. Vain kirjaimet, numerot ja @/./+/-/_ ovat sallittuja. */ + /** Required. 150 characters or fewer. Letters, digits and @/./+/-/_ only. */ username: Scalars['String']['output']; }; @@ -842,11 +842,11 @@ export type LanguageTranslationType = { /** An enumeration. */ export enum LanguagesLanguageTranslationLanguageCodeChoices { - /** englanti */ + /** English */ En = 'EN', - /** suomi */ + /** Finnish */ Fi = 'FI', - /** ruotsi */ + /** Swedish */ Sv = 'SV' } @@ -948,7 +948,7 @@ export type MessageTranslationsInput = { /** An enumeration. */ export enum MessagingMessageProtocolChoices { - /** Sähköposti */ + /** Email */ Email = 'EMAIL', /** SMS */ Sms = 'SMS' @@ -956,11 +956,11 @@ export enum MessagingMessageProtocolChoices { /** An enumeration. */ export enum MessagingMessageTranslationLanguageCodeChoices { - /** englanti */ + /** English */ En = 'EN', - /** suomi */ + /** Finnish */ Fi = 'FI', - /** ruotsi */ + /** Swedish */ Sv = 'SV' } @@ -1012,6 +1012,7 @@ export type Mutation = { updateMyEmail: Maybe; updateMyProfile: Maybe; updateOccurrence: Maybe; + updateTicketAttended: Maybe; updateVenue: Maybe; }; @@ -1186,6 +1187,11 @@ export type MutationUpdateOccurrenceArgs = { }; +export type MutationUpdateTicketAttendedArgs = { + input: UpdateTicketAttendedMutationInput; +}; + + export type MutationUpdateVenueArgs = { input: UpdateVenueMutationInput; }; @@ -1196,6 +1202,20 @@ export type Node = { id: Scalars['ID']['output']; }; +export type OccurrenceArrivalStatusNode = { + __typename?: 'OccurrenceArrivalStatusNode'; + /** + * **DEPRECATED:** Number of enrolments marked as attended for this occurrence. + * @deprecated This field exposes potentially sensitive data and will be removed in a future release. Consider using a more secure method to access this information. + */ + attendedEnrolmentCount: Scalars['Int']['output']; + /** + * **DEPRECATED:** Total number of enrolments for this occurrence. + * @deprecated This field exposes potentially sensitive data and will be removed in a future release. Consider using a more secure method to access this information. + */ + enrolmentCount: Scalars['Int']['output']; +}; + export type OccurrenceNode = Node & { __typename?: 'OccurrenceNode'; attendedEnrolmentCount: Scalars['Int']['output']; @@ -1664,8 +1684,15 @@ export enum TicketSystem { export type TicketVerificationNode = { __typename?: 'TicketVerificationNode'; + /** Indicates whether the ticket holder has arrived. If null, the status is unset. */ + attended: Maybe; /** The name of the event */ eventName: Scalars['String']['output']; + /** + * **DEPRECATED:** Use `OccurrenceNode` instead (requires authorization). Provides a summary of arrivals for this occurrence. This field will be removed in a future release to protect sensitive attendance data. + * @deprecated This field exposes potentially sensitive data and will be removed in a future release. The attendance information should not be publicly available. + */ + occurrenceArrivalStatus: Maybe; /** The time of the event occurrence */ occurrenceTime: Scalars['DateTime']['output']; validity: Scalars['Boolean']['output']; @@ -1894,6 +1921,18 @@ export type UpdateOccurrenceMutationPayload = { occurrence: Maybe; }; +export type UpdateTicketAttendedMutationInput = { + attended: Scalars['Boolean']['input']; + clientMutationId?: InputMaybe; + referenceId: Scalars['String']['input']; +}; + +export type UpdateTicketAttendedMutationPayload = { + __typename?: 'UpdateTicketAttendedMutationPayload'; + clientMutationId: Maybe; + ticket: Maybe; +}; + export type UpdateVenueMutationInput = { clientMutationId?: InputMaybe; id: Scalars['ID']['input']; @@ -2203,12 +2242,19 @@ export type ImportTicketSystemPasswordsMutationVariables = Exact<{ export type ImportTicketSystemPasswordsMutation = { __typename?: 'Mutation', importTicketSystemPasswords: { __typename?: 'ImportTicketSystemPasswordsMutationPayload', errors: Array<{ __typename?: 'ErrorType', field: string, message: string, value: string } | null> | null } | null }; +export type UpdateTicketAttendedMutationVariables = Exact<{ + input: UpdateTicketAttendedMutationInput; +}>; + + +export type UpdateTicketAttendedMutation = { __typename?: 'Mutation', updateTicketAttended: { __typename?: 'UpdateTicketAttendedMutationPayload', ticket: { __typename?: 'TicketVerificationNode', occurrenceTime: any, eventName: string, venueName: string | null, validity: boolean, attended: boolean | null, occurrenceArrivalStatus: { __typename?: 'OccurrenceArrivalStatusNode', enrolmentCount: number, attendedEnrolmentCount: number } | null } | null } | null }; + export type VerifyTicketQueryVariables = Exact<{ referenceId: Scalars['String']['input']; }>; -export type VerifyTicketQuery = { __typename?: 'Query', verifyTicket: { __typename?: 'TicketVerificationNode', occurrenceTime: any, eventName: string, venueName: string | null, validity: boolean } | null }; +export type VerifyTicketQuery = { __typename?: 'Query', verifyTicket: { __typename?: 'TicketVerificationNode', occurrenceTime: any, eventName: string, venueName: string | null, validity: boolean, attended: boolean | null, occurrenceArrivalStatus: { __typename?: 'OccurrenceArrivalStatusNode', enrolmentCount: number, attendedEnrolmentCount: number } | null } | null }; export type AddVenueMutationVariables = Exact<{ input: AddVenueMutationInput; @@ -3023,6 +3069,26 @@ export const ImportTicketSystemPasswordsMutationDocument = gql` export type ImportTicketSystemPasswordsMutationMutationFn = Apollo.MutationFunction; export type ImportTicketSystemPasswordsMutationMutationResult = Apollo.MutationResult; export type ImportTicketSystemPasswordsMutationMutationOptions = Apollo.BaseMutationOptions; +export const UpdateTicketAttendedDocument = gql` + mutation UpdateTicketAttended($input: UpdateTicketAttendedMutationInput!) { + updateTicketAttended(input: $input) { + ticket { + occurrenceTime + eventName + venueName + validity + attended + occurrenceArrivalStatus { + enrolmentCount + attendedEnrolmentCount + } + } + } +} + `; +export type UpdateTicketAttendedMutationFn = Apollo.MutationFunction; +export type UpdateTicketAttendedMutationResult = Apollo.MutationResult; +export type UpdateTicketAttendedMutationOptions = Apollo.BaseMutationOptions; export const VerifyTicketDocument = gql` query VerifyTicket($referenceId: String!) { verifyTicket(referenceId: $referenceId) { @@ -3030,6 +3096,11 @@ export const VerifyTicketDocument = gql` eventName venueName validity + attended + occurrenceArrivalStatus { + enrolmentCount + attendedEnrolmentCount + } } } `; diff --git a/src/domain/ticketValidation/TicketValidationPage.tsx b/src/domain/ticketValidation/TicketValidationPage.tsx index 0d8ae705..c20ac325 100644 --- a/src/domain/ticketValidation/TicketValidationPage.tsx +++ b/src/domain/ticketValidation/TicketValidationPage.tsx @@ -1,6 +1,12 @@ import React from 'react'; import { makeStyles } from '@mui/styles'; -import type { Theme } from '@mui/material'; +import { + FormControl, + FormControlLabel, + FormGroup, + Checkbox, + type Theme, +} from '@mui/material'; import CheckCircleIcon from '@mui/icons-material/CheckCircle'; import CancelIcon from '@mui/icons-material/Cancel'; import { useParams } from 'react-router'; @@ -12,6 +18,7 @@ import { ApolloProvider } from '@apollo/client'; import unauthenticatedClient from '../../api/apolloClient/unauthenticatedClient'; import useVerifyTicketQuery from './useVerifyTicketQuery'; import OccurrenceCard from './OccurrenceCard'; +import useUpdateTicketAttendedMutation from './useUpdateTicketAttendedMutation'; const containerStyles = { display: 'flex', @@ -31,6 +38,17 @@ const useStyles = makeStyles((theme) => ({ backgroundColor: theme.palette.grey[200], }, + formControl: { + margin: theme.spacing(1), + marginTop: theme.spacing(3), + }, + arrivalStatus: { + color: theme.palette.grey[600], + display: 'block', + textAlign: 'center', + fontStyle: 'italic', + fontWeight: 'lighter', + }, })); type Params = { @@ -41,17 +59,30 @@ const TicketValidationPage = () => { const t = useTranslate(); const styles = useStyles(); const { cryptographicallySignedCode } = useParams(); - const { data, error, loading } = useVerifyTicketQuery({ - // eslint-disable-next-line @typescript-eslint/no-non-null-assertion - referenceId: cryptographicallySignedCode!, + const { data, error, loading, refetch } = useVerifyTicketQuery({ + referenceId: cryptographicallySignedCode ?? '', + }); + const [updateAttended] = useUpdateTicketAttendedMutation({ + sideEffect: () => { + refetch({ referenceId: cryptographicallySignedCode ?? '' }); + }, }); + const { - validity: isValid, - eventName, - venueName, + validity: isValid = false, + eventName = '', + venueName = '', occurrenceTime, + attended = null, + occurrenceArrivalStatus, } = data?.verifyTicket ?? {}; + const { enrolmentCount, attendedEnrolmentCount } = + occurrenceArrivalStatus ?? { + enrolmentCount: '?', + attendedEnrolmentCount: '?', + }; + if (loading) { return ; } @@ -64,14 +95,52 @@ const TicketValidationPage = () => { ); } + const handleAttended = (event: React.ChangeEvent) => { + updateAttended({ + variables: { + input: { + referenceId: cryptographicallySignedCode ?? '', + attended: event.target.checked, + }, + }, + }); + }; + return (
+ {isValid && ( +
+
+ + + + } + label={t( + 'ticketValidation.updateTicketAttended.switchButton.label' + )} + /> + + +
+
+ {t('ticketValidation.arrivalStatus.label', { + attendedEnrolmentCount, + enrolmentCount, + })} +
+
+ )}
); }; diff --git a/src/domain/ticketValidation/mutations.ts b/src/domain/ticketValidation/mutations.ts new file mode 100644 index 00000000..0827bdb5 --- /dev/null +++ b/src/domain/ticketValidation/mutations.ts @@ -0,0 +1,19 @@ +import { gql } from '@apollo/client'; + +export const updateTicketAttendedMutation = gql` + mutation UpdateTicketAttended($input: UpdateTicketAttendedMutationInput!) { + updateTicketAttended(input: $input) { + ticket { + occurrenceTime + eventName + venueName + validity + attended + occurrenceArrivalStatus { + enrolmentCount + attendedEnrolmentCount + } + } + } + } +`; diff --git a/src/domain/ticketValidation/queries.ts b/src/domain/ticketValidation/queries.ts new file mode 100644 index 00000000..43d05b8f --- /dev/null +++ b/src/domain/ticketValidation/queries.ts @@ -0,0 +1,17 @@ +import { gql } from '@apollo/client'; + +export const verifyTicketQuery = gql` + query VerifyTicket($referenceId: String!) { + verifyTicket(referenceId: $referenceId) { + occurrenceTime + eventName + venueName + validity + attended + occurrenceArrivalStatus { + enrolmentCount + attendedEnrolmentCount + } + } + } +`; diff --git a/src/domain/ticketValidation/useUpdateTicketAttendedMutation.ts b/src/domain/ticketValidation/useUpdateTicketAttendedMutation.ts new file mode 100644 index 00000000..d610c8a1 --- /dev/null +++ b/src/domain/ticketValidation/useUpdateTicketAttendedMutation.ts @@ -0,0 +1,58 @@ +import { useMutation } from '@apollo/client'; +import { useNotify } from 'react-admin'; +import * as Sentry from '@sentry/browser'; + +import { UpdateTicketAttendedDocument } from '../api/generatedTypes/graphql'; +import type { UpdateTicketAttendedMutationResult } from '../api/generatedTypes/graphql'; + +type Config = { + sideEffect?: (data: UpdateTicketAttendedMutationResult['data']) => void; +}; + +export default function useUpdateTicketAttendedMutation({ + sideEffect, +}: Config) { + const notify = useNotify(); + + return useMutation( + UpdateTicketAttendedDocument, + { + onCompleted: (data) => { + const attended = data?.updateTicketAttended?.ticket?.attended; + if (attended === true) { + // eslint-disable-next-line no-console + console.info('The ticket attendance status set to', true); + notify( + 'ticketValidation.updateTicketAttended.onSuccess.attendedMessage' + ); + } else if (attended === false) { + // eslint-disable-next-line no-console + console.info('The ticket attendance status set to', false); + notify( + 'ticketValidation.updateTicketAttended.onSuccess.unattendedMessage' + ); + } else { + // eslint-disable-next-line no-console + console.error('Error while updating the ticket attendance status', { + data, + }); + notify('ticketValidation.updateTicketAttended.onError.message', { + type: 'warning', + }); + } + sideEffect && sideEffect(data); + }, + onError: (error: Error) => { + // eslint-disable-next-line no-console + console.error( + 'Error while updating the ticket attendance status', + error + ); + Sentry.captureException(error); + notify('ticketValidation.updateTicketAttended.onError.message', { + type: 'warning', + }); + }, + } + ); +} diff --git a/src/domain/ticketValidation/useVerifyTicketQuery.ts b/src/domain/ticketValidation/useVerifyTicketQuery.ts index d389217a..61704e90 100644 --- a/src/domain/ticketValidation/useVerifyTicketQuery.ts +++ b/src/domain/ticketValidation/useVerifyTicketQuery.ts @@ -1,22 +1,14 @@ -import { gql, useQuery } from '@apollo/client'; +import { useQuery } from '@apollo/client'; -const verifyTicketQuery = gql` - query VerifyTicket($referenceId: String!) { - verifyTicket(referenceId: $referenceId) { - occurrenceTime - eventName - venueName - validity - } - } -`; +import type { VerifyTicketQuery } from '../api/generatedTypes/graphql'; +import { VerifyTicketDocument } from '../api/generatedTypes/graphql'; type Config = { referenceId: string; }; export default function useVerifyTicketQuery({ referenceId }: Config) { - return useQuery(verifyTicketQuery, { + return useQuery(VerifyTicketDocument, { skip: !referenceId, variables: { referenceId }, });