Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
379 changes: 325 additions & 54 deletions admin-ui/src/gql/types.ts

Large diffs are not rendered by default.

1 change: 1 addition & 0 deletions admin-ui/src/modules/Auth/permissionConfig.ts
Original file line number Diff line number Diff line change
Expand Up @@ -51,6 +51,7 @@ const UNRESTRICTED_PAGES = [
'/403',
'/500',
'/external',
'/ticketing/gate',
];

const PUBLIC_ONLY_PAGES = [
Expand Down
11 changes: 11 additions & 0 deletions admin-ui/src/modules/accounts/components/LogInForm.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -302,6 +302,17 @@ const LogInForm = () => {
})}
</Link>
</div>
<div className="text-sm text-slate-400 dark:text-slate-200">
<Link
href="/ticketing/gate"
className="font-medium text-slate-950 hover:text-slate-900 dark:text-slate-400 dark:hover:text-slate-300"
>
{intl.formatMessage({
id: 'open_gate_control',
defaultMessage: 'Open Gate Control',
})}
</Link>
</div>
</Form>
</FormWrapper>
</div>
Expand Down
1 change: 0 additions & 1 deletion admin-ui/src/modules/apollo/utils/createApolloClient.ts
Original file line number Diff line number Diff line change
Expand Up @@ -84,7 +84,6 @@ const createApolloClient = ({
defaultOptions: {
watchQuery: {
fetchPolicy: 'cache-and-network',
errorPolicy: 'all',
},
},

Expand Down
7 changes: 7 additions & 0 deletions admin-ui/src/modules/common/components/Layout.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ import {
CubeIcon,
DocumentTextIcon,
FolderArrowDownIcon,
TicketIcon,
} from '@heroicons/react/24/outline';
import Link from 'next/link';
import React, { useState } from 'react';
Expand Down Expand Up @@ -236,6 +237,12 @@ const Layout = ({
href: '/tokens',
requiredRole: 'viewTokens',
},
isSystemReady && {
name: formatMessage({ id: 'ticketing', defaultMessage: 'Ticketing' }),
icon: TicketIcon,
href: '/ticketing',
requiredRole: 'viewTokens',
},
{
name: formatMessage({ id: 'system', defaultMessage: 'System settings' }),
icon: Cog8ToothIcon,
Expand Down
7 changes: 1 addition & 6 deletions admin-ui/src/modules/order/components/OrderStatusBadge.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -4,12 +4,7 @@ import Badge from '@/components/ui/Badge';
import { ORDER_STATUSES } from '../../common/data/miscellaneous';

type OrderStatus =
| 'PENDING'
| 'CONFIRMED'
| 'OPEN'
| 'FULFILLED'
| 'REJECTED'
| string;
'PENDING' | 'CONFIRMED' | 'OPEN' | 'FULFILLED' | 'REJECTED' | string;

type OrderStatusBadgeProps = {
status: OrderStatus;
Expand Down
1 change: 1 addition & 0 deletions admin-ui/src/modules/product/fragments/TokenFragment.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ const TokenFragment = gql`
ercMetadata
accessKey
isInvalidateable
isCanceled
}
`;

Expand Down
52 changes: 52 additions & 0 deletions admin-ui/src/modules/product/hooks/useProduct.ts
Original file line number Diff line number Diff line change
Expand Up @@ -57,6 +57,58 @@ const GetProductQuery = (inlineFragment = '') => gql`
__typename
}
}
... on TokenizedProduct {
texts {
_id
title
subtitle
description
}
contractConfiguration {
ercMetadataProperties
supply
}
simulatedStocks {
quantity
}
tokensCount
isCanceled
scannerPassCode
tokens {
_id
tokenSerialNumber
isCanceled
invalidatedDate
isInvalidateable
quantity
status
walletAddress
user {
_id
username
isGuest
primaryEmail {
address
verified
}
avatar {
_id
url
}
profile {
displayName
address {
firstName
lastName
}
}
lastContact {
emailAddress
telNumber
}
}
}
}
}
}
${ProductDetailFragment}
Expand Down
71 changes: 71 additions & 0 deletions admin-ui/src/modules/ticketing/components/EventTokenList.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,71 @@
import { useIntl } from 'react-intl';
import Table from '../../common/components/Table';
import EventTokenListItem from './EventTokenListItem';

const EventTokenList = ({ tokens, onCancelTicket, onInvalidateTicket }) => {
const { formatMessage } = useIntl();

if (!tokens?.length) {
return (
<p className="py-4 text-sm text-slate-500 dark:text-slate-400">
{formatMessage({
id: 'no_tickets_issued',
defaultMessage: 'No tickets have been issued yet.',
})}
</p>
);
}

return (
<Table className="min-w-full">
<Table.Row header>
<Table.Cell>
{formatMessage({
id: 'ticket_number',
defaultMessage: 'Ticket #',
})}
</Table.Cell>
<Table.Cell>
{formatMessage({
id: 'attendee',
defaultMessage: 'Attendee',
})}
</Table.Cell>
<Table.Cell>
{formatMessage({
id: 'email',
defaultMessage: 'E-Mail',
})}
</Table.Cell>
<Table.Cell>
{formatMessage({
id: 'phone',
defaultMessage: 'Phone',
})}
</Table.Cell>
<Table.Cell>
{formatMessage({
id: 'redeemed',
defaultMessage: 'Redeemed',
})}
</Table.Cell>
<Table.Cell>
{formatMessage({
id: 'actions',
defaultMessage: 'Actions',
})}
</Table.Cell>
</Table.Row>
{tokens.map((token) => (
<EventTokenListItem
key={token._id}
token={token}
onCancelTicket={onCancelTicket}
onInvalidateTicket={onInvalidateTicket}
/>
))}
</Table>
);
};

export default EventTokenList;
105 changes: 105 additions & 0 deletions admin-ui/src/modules/ticketing/components/EventTokenListItem.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,105 @@
import Link from 'next/link';
import { useIntl } from 'react-intl';
import Table from '../../common/components/Table';
import Badge from '../../../components/ui/Badge';
import useFormatDateTime from '../../common/utils/useFormatDateTime';
import formatUsername from '../../common/utils/formatUsername';
import MediaAvatar from '../../common/components/MediaAvatar';

const EventTokenListItem = ({ token, onCancelTicket, onInvalidateTicket }) => {
const { formatMessage } = useIntl();
const { formatDateTime } = useFormatDateTime();

return (
<Table.Row key={token._id}>
<Table.Cell>
<Link
href={`/tokens?tokenId=${token._id}`}
className="font-medium text-slate-800 dark:text-slate-200 hover:underline"
>
{token.tokenSerialNumber || token._id?.slice(-8)}
</Link>
</Table.Cell>
<Table.Cell>
{token.user && (
<Link
href={`/users?userId=${token.user._id}`}
className="flex items-center text-sm text-slate-800 dark:text-slate-200 hover:underline"
>
<MediaAvatar file={token.user?.avatar} className="mr-2" />
<span>{formatUsername(token.user)}</span>
</Link>
)}
</Table.Cell>
<Table.Cell>
<span className="text-sm text-slate-600 dark:text-slate-400">
{token.user?.lastContact?.emailAddress ||
token.user?.primaryEmail?.address ||
'-'}
</span>
</Table.Cell>
<Table.Cell>
<span className="text-sm text-slate-600 dark:text-slate-400">
{token.user?.lastContact?.telNumber || '-'}
</span>
</Table.Cell>
<Table.Cell>
{token.invalidatedDate ? (
<Badge
text={formatDateTime(token.invalidatedDate, {
dateStyle: 'short',
timeStyle: 'short',
})}
color="emerald"
square
/>
) : (
<span className="text-sm text-slate-400">-</span>
)}
</Table.Cell>
<Table.Cell>
<div className="flex items-center gap-2">
{token.isCanceled ? (
<Badge
text={formatMessage({
id: 'cancelled',
defaultMessage: 'Cancelled',
})}
color="rose"
square
/>
) : (
<>
{!token.invalidatedDate && (
<button
type="button"
onClick={() => onCancelTicket(token._id)}
className="inline-flex items-center rounded-md border border-slate-300 dark:border-slate-600 px-3 py-1 text-xs font-medium text-rose-600 dark:text-rose-400 hover:bg-rose-50 dark:hover:bg-rose-900/20"
>
{formatMessage({
id: 'cancel_ticket',
defaultMessage: 'Cancel',
})}
</button>
)}
{token.isInvalidateable && !token.invalidatedDate && (
<button
type="button"
onClick={() => onInvalidateTicket(token._id)}
className="inline-flex items-center rounded-md border border-slate-300 dark:border-slate-600 px-3 py-1 text-xs font-medium text-emerald-600 dark:text-emerald-400 hover:bg-emerald-50 dark:hover:bg-emerald-900/20"
>
{formatMessage({
id: 'redeem_ticket',
defaultMessage: 'Redeem',
})}
</button>
)}
</>
)}
</div>
</Table.Cell>
</Table.Row>
);
};

export default EventTokenListItem;
Loading