mirror of
https://git.sr.ht/~ashkeel/strimertul
synced 2024-09-20 02:00:49 +00:00
406 lines
12 KiB
TypeScript
406 lines
12 KiB
TypeScript
import React, { useState } from 'react';
|
|
import { useTranslation } from 'react-i18next';
|
|
import { useModule, useUserPoints } from '~/lib/react-utils';
|
|
import { SortFunction } from '~/lib/type-utils';
|
|
import { useAppDispatch } from '~/store';
|
|
import { modules, removeRedeem, setUserPoints } from '~/store/api/reducer';
|
|
import { DataTable } from '../components/DataTable';
|
|
import DialogContent from '../components/DialogContent';
|
|
import {
|
|
Button,
|
|
Dialog,
|
|
DialogActions,
|
|
Field,
|
|
FlexRow,
|
|
InputBox,
|
|
Label,
|
|
PageContainer,
|
|
PageHeader,
|
|
PageTitle,
|
|
TabButton,
|
|
TabContainer,
|
|
TabContent,
|
|
TabList,
|
|
TextBlock,
|
|
} from '../theme';
|
|
import { TableCell, TableRow } from '../theme/table';
|
|
|
|
function RewardQueue() {
|
|
const { t } = useTranslation();
|
|
const [queue] = useModule(modules.loyaltyRedeemQueue);
|
|
const dispatch = useAppDispatch();
|
|
|
|
// Big hack but this is required or refunds break
|
|
useUserPoints();
|
|
|
|
const data = queue?.map((q) => ({ ...q, date: new Date(q.when) })) ?? [];
|
|
type Redeem = typeof data[0];
|
|
|
|
const sortfn = (key: keyof Redeem) => (a: Redeem, b: Redeem) => {
|
|
switch (key) {
|
|
case 'display_name': {
|
|
return a.display_name?.localeCompare(b.display_name);
|
|
}
|
|
case 'when': {
|
|
return a.date && b.date ? a.date.getTime() - b.date.getTime() : 0;
|
|
}
|
|
case 'reward': {
|
|
return a.reward?.name?.localeCompare(b.reward.name);
|
|
}
|
|
default:
|
|
return 0;
|
|
}
|
|
};
|
|
|
|
return (
|
|
<>
|
|
{(data.length > 0 && (
|
|
<DataTable
|
|
sort={sortfn}
|
|
data={data}
|
|
columns={[
|
|
{
|
|
key: 'when',
|
|
title: t('pages.loyalty-queue.date'),
|
|
sortable: true,
|
|
},
|
|
{
|
|
key: 'username',
|
|
title: t('pages.loyalty-queue.username'),
|
|
sortable: true,
|
|
},
|
|
{
|
|
key: 'reward',
|
|
title: t('pages.loyalty-queue.reward'),
|
|
sortable: true,
|
|
attr: {
|
|
style: {
|
|
textTransform: 'capitalize',
|
|
},
|
|
},
|
|
},
|
|
{
|
|
key: 'request_text',
|
|
title: t('pages.loyalty-queue.request'),
|
|
sortable: false,
|
|
attr: {
|
|
style: {
|
|
width: '100%',
|
|
textAlign: 'left',
|
|
},
|
|
},
|
|
},
|
|
{
|
|
key: 'actions',
|
|
title: '',
|
|
sortable: false,
|
|
},
|
|
]}
|
|
defaultSort={{ key: 'when', order: 'desc' }}
|
|
view={(entry) => (
|
|
<TableRow key={`${entry.when.toString()}${entry.username}`}>
|
|
<TableCell css={{ width: '22%', fontSize: '0.8rem' }}>
|
|
{entry.date.toLocaleString()}
|
|
</TableCell>
|
|
<TableCell css={{ width: '10%' }}>{entry.username}</TableCell>
|
|
<TableCell css={{ width: '18%' }}>{entry.reward?.name}</TableCell>
|
|
<TableCell css={{ width: '40%' }}>{entry.request_text}</TableCell>
|
|
<TableCell>
|
|
<FlexRow spacing="1">
|
|
<Button
|
|
size="small"
|
|
onClick={() => {
|
|
void dispatch(removeRedeem(entry));
|
|
}}
|
|
>
|
|
{t('pages.loyalty-queue.accept')}
|
|
</Button>
|
|
<Button
|
|
size="small"
|
|
onClick={() => {
|
|
// Give points back to the viewer
|
|
void dispatch(
|
|
setUserPoints({
|
|
user: entry.username,
|
|
points: entry.reward.price,
|
|
relative: true,
|
|
}),
|
|
);
|
|
// Take the redeem off the list
|
|
void dispatch(removeRedeem(entry));
|
|
}}
|
|
>
|
|
{t('pages.loyalty-queue.refund')}
|
|
</Button>
|
|
</FlexRow>
|
|
</TableCell>
|
|
</TableRow>
|
|
)}
|
|
/>
|
|
)) || <p>{t('pages.loyalty-queue.no-redeems')}</p>}
|
|
</>
|
|
);
|
|
}
|
|
|
|
function UserList() {
|
|
const { t } = useTranslation();
|
|
const users = useUserPoints();
|
|
const dispatch = useAppDispatch();
|
|
|
|
const [currentEntry, setCurrentEntry] = useState<UserEntry>(null);
|
|
const [givePointDialog, setGivePointDialog] = useState({
|
|
open: false,
|
|
user: '',
|
|
points: 0,
|
|
});
|
|
const [config] = useModule(modules.loyaltyConfig);
|
|
const [usernameFilter, setUsernameFilter] = useState('');
|
|
const filtered = Object.entries(users ?? [])
|
|
.filter(([user]) => user.includes(usernameFilter))
|
|
.map(([username, data]) => ({
|
|
username,
|
|
...data,
|
|
}));
|
|
type UserEntry = typeof filtered[0];
|
|
|
|
const sortfn = (key: keyof UserEntry): SortFunction<UserEntry> => {
|
|
switch (key) {
|
|
case 'username': {
|
|
return (a, b) => a.username.localeCompare(b.username);
|
|
}
|
|
case 'points': {
|
|
return (a: UserEntry, b: UserEntry) => a.points - b.points;
|
|
}
|
|
}
|
|
};
|
|
|
|
return (
|
|
<>
|
|
<Dialog
|
|
open={givePointDialog.open}
|
|
onOpenChange={(state) =>
|
|
setGivePointDialog({ ...givePointDialog, open: state })
|
|
}
|
|
>
|
|
<DialogContent
|
|
title={t('pages.loyalty-queue.give-points-dialog')}
|
|
closeButton={true}
|
|
>
|
|
<form
|
|
onSubmit={(e) => {
|
|
e.preventDefault();
|
|
if ((e.target as HTMLFormElement).checkValidity()) {
|
|
void dispatch(
|
|
setUserPoints({
|
|
...givePointDialog,
|
|
relative: true,
|
|
}),
|
|
);
|
|
setGivePointDialog({ ...givePointDialog, open: false });
|
|
}
|
|
}}
|
|
>
|
|
<Field size="fullWidth" spacing="narrow">
|
|
<Label htmlFor="d-username">
|
|
{t('pages.loyalty-queue.username')}
|
|
</Label>
|
|
<InputBox
|
|
id="d-username"
|
|
required={true}
|
|
value={givePointDialog?.user ?? ''}
|
|
onChange={(e) =>
|
|
setGivePointDialog({
|
|
...givePointDialog,
|
|
user: e.target.value,
|
|
})
|
|
}
|
|
/>
|
|
</Field>
|
|
<Field size="fullWidth" spacing="narrow">
|
|
<Label htmlFor="d-points" css={{ textTransform: 'capitalize' }}>
|
|
{config?.currency || t('pages.loyalty-queue.points')}
|
|
</Label>
|
|
<InputBox
|
|
type="number"
|
|
id="d-points"
|
|
value={givePointDialog?.points ?? '0'}
|
|
onChange={(e) =>
|
|
setGivePointDialog({
|
|
...givePointDialog,
|
|
points: parseInt(e.target.value, 10),
|
|
})
|
|
}
|
|
/>
|
|
</Field>
|
|
<DialogActions>
|
|
<Button variation="primary" type="submit">
|
|
{t('form-actions.save')}
|
|
</Button>
|
|
<Button
|
|
type="button"
|
|
onClick={() =>
|
|
setGivePointDialog({ ...givePointDialog, open: false })
|
|
}
|
|
>
|
|
{t('form-actions.cancel')}
|
|
</Button>
|
|
</DialogActions>
|
|
</form>
|
|
</DialogContent>
|
|
</Dialog>
|
|
<Dialog
|
|
open={currentEntry !== null}
|
|
onOpenChange={(state) => setCurrentEntry(state ? currentEntry : null)}
|
|
>
|
|
<DialogContent
|
|
title={t('pages.loyalty-queue.modify-balance-dialog')}
|
|
closeButton={true}
|
|
>
|
|
<form
|
|
onSubmit={(e) => {
|
|
e.preventDefault();
|
|
if ((e.target as HTMLFormElement).checkValidity()) {
|
|
void dispatch(
|
|
setUserPoints({
|
|
user: currentEntry.username,
|
|
points: currentEntry.points,
|
|
relative: false,
|
|
}),
|
|
);
|
|
setCurrentEntry(null);
|
|
}
|
|
}}
|
|
>
|
|
<Field size="fullWidth" spacing="narrow">
|
|
<Label htmlFor="d-username">
|
|
{t('pages.loyalty-queue.username')}
|
|
</Label>
|
|
<InputBox
|
|
disabled={true}
|
|
id="d-username"
|
|
value={currentEntry?.username ?? ''}
|
|
/>
|
|
</Field>
|
|
<Field size="fullWidth" spacing="narrow">
|
|
<Label htmlFor="d-points" css={{ textTransform: 'capitalize' }}>
|
|
{config?.currency || t('pages.loyalty-queue.points')}
|
|
</Label>
|
|
<InputBox
|
|
type="number"
|
|
id="d-points"
|
|
value={currentEntry?.points ?? '0'}
|
|
onChange={(e) =>
|
|
setCurrentEntry({
|
|
...currentEntry,
|
|
points: parseInt(e.target.value, 10),
|
|
})
|
|
}
|
|
/>
|
|
</Field>
|
|
<DialogActions>
|
|
<Button variation="primary" type="submit">
|
|
{t('form-actions.save')}
|
|
</Button>
|
|
<Button onClick={() => setCurrentEntry(null)} type="button">
|
|
{t('form-actions.cancel')}
|
|
</Button>
|
|
</DialogActions>
|
|
</form>
|
|
</DialogContent>
|
|
</Dialog>
|
|
<Field size="fullWidth" spacing="none">
|
|
<FlexRow css={{ flex: 1, alignItems: 'stretch' }} spacing="1">
|
|
<Button
|
|
onClick={() =>
|
|
setGivePointDialog({ open: true, user: '', points: 0 })
|
|
}
|
|
>
|
|
{t('pages.loyalty-queue.give-points-dialog')}
|
|
</Button>
|
|
<InputBox
|
|
css={{ flex: 1 }}
|
|
placeholder={t('pages.loyalty-queue.username-filter')}
|
|
value={usernameFilter}
|
|
onChange={(e) => setUsernameFilter(e.target.value)}
|
|
/>
|
|
</FlexRow>
|
|
</Field>
|
|
{(filtered.length > 0 && (
|
|
<DataTable
|
|
sort={sortfn}
|
|
data={filtered}
|
|
columns={[
|
|
{
|
|
key: 'username',
|
|
title: t('pages.loyalty-queue.username'),
|
|
sortable: true,
|
|
attr: {
|
|
style: {
|
|
width: '100%',
|
|
textAlign: 'left',
|
|
},
|
|
},
|
|
},
|
|
{
|
|
key: 'points',
|
|
title: config?.currency || t('pages.loyalty-queue.points'),
|
|
sortable: true,
|
|
attr: {
|
|
style: {
|
|
textTransform: 'capitalize',
|
|
},
|
|
},
|
|
},
|
|
{
|
|
key: 'actions',
|
|
title: '',
|
|
sortable: false,
|
|
},
|
|
]}
|
|
defaultSort={{ key: 'points', order: 'desc' }}
|
|
view={(entry) => (
|
|
<TableRow key={entry.username}>
|
|
<TableCell css={{ width: '100%' }}>{entry.username}</TableCell>
|
|
<TableCell>{entry.points}</TableCell>
|
|
<TableCell>
|
|
<Button onClick={() => setCurrentEntry(entry)} size="small">
|
|
{t('form-actions.edit')}
|
|
</Button>
|
|
</TableCell>
|
|
</TableRow>
|
|
)}
|
|
/>
|
|
)) || <p>{t('pages.loyalty-queue.no-users')}</p>}
|
|
</>
|
|
);
|
|
}
|
|
|
|
export default function LoyaltyQueuePage(): React.ReactElement {
|
|
const { t } = useTranslation();
|
|
|
|
return (
|
|
<PageContainer>
|
|
<PageHeader>
|
|
<PageTitle>{t('pages.loyalty-queue.title')}</PageTitle>
|
|
<TextBlock>{t('pages.loyalty-queue.subtitle')}</TextBlock>
|
|
</PageHeader>
|
|
<TabContainer defaultValue="queue">
|
|
<TabList>
|
|
<TabButton value="queue">
|
|
{t('pages.loyalty-queue.queue-tab')}
|
|
</TabButton>
|
|
<TabButton value="users">
|
|
{t('pages.loyalty-queue.users-tab')}
|
|
</TabButton>
|
|
</TabList>
|
|
<TabContent value="queue">
|
|
<RewardQueue />
|
|
</TabContent>
|
|
<TabContent value="users">
|
|
<UserList />
|
|
</TabContent>
|
|
</TabContainer>
|
|
</PageContainer>
|
|
);
|
|
}
|