strimertul/frontend/src/ui/pages/loyalty/Rewards/RewardsTab.tsx

458 lines
14 KiB
TypeScript

import { CheckIcon, PlusIcon } from '@radix-ui/react-icons';
import React, { useState } from 'react';
import { useTranslation } from 'react-i18next';
import { useModule } from '~/lib/react';
import { useAppDispatch } from '~/store';
import { modules } from '~/store/api/reducer';
import { LoyaltyReward } from '~/store/api/types';
import AlertContent from '../../../components/AlertContent';
import DialogContent from '../../../components/DialogContent';
import Interval from '../../../components/forms/Interval';
import {
Button,
Checkbox,
CheckboxIndicator,
ControlledInputBox,
Dialog,
DialogActions,
Field,
FieldNote,
FlexRow,
InputBox,
Label,
MultiButton,
NoneText,
styled,
Textarea,
} from '../../../theme';
import { Alert, AlertTrigger } from '../../../theme/alert';
import {
RewardItemContainer,
RewardHeader,
RewardIcon,
RewardName,
RewardID,
RewardCost,
RewardActions,
RewardDescription,
} from './theme';
const RewardList = styled('div', { marginTop: '1rem' });
interface RewardItemProps {
name: string;
item: LoyaltyReward;
currency: string;
onToggle?: () => void;
onEdit?: () => void;
onDelete?: () => void;
}
function RewardItem({
name,
item,
currency,
onToggle,
onEdit,
onDelete,
}: RewardItemProps): React.ReactElement {
const { t } = useTranslation();
return (
<RewardItemContainer status={item.enabled ? 'enabled' : 'disabled'}>
<RewardHeader>
<RewardIcon>
{item.image && (
<img
src={item.image}
style={{ width: '32px', borderRadius: '0.25rem' }}
/>
)}
</RewardIcon>
<RewardName status={item.enabled ? 'enabled' : 'disabled'}>
{item.name} (<RewardID>{name}</RewardID>)
</RewardName>
<RewardCost>
{item.price} {currency}
</RewardCost>
<RewardActions>
<MultiButton>
<Button
styling="multi"
size="small"
onClick={() => (onToggle ? onToggle() : null)}
>
{item.enabled
? t('form-actions.disable')
: t('form-actions.enable')}
</Button>
<Button
styling="multi"
size="small"
onClick={() => (onEdit ? onEdit() : null)}
>
{t('form-actions.edit')}
</Button>
<Alert>
<AlertTrigger asChild>
<Button styling="multi" size="small">
{t('form-actions.delete')}
</Button>
</AlertTrigger>
<AlertContent
variation="danger"
title={t('pages.loyalty-rewards.remove-reward-title', {
name: item.name,
})}
description={t('form-actions.warning-delete')}
actionText={t('form-actions.delete')}
actionButtonProps={{ variation: 'danger' }}
showCancel={true}
onAction={() => (onDelete ? onDelete() : null)}
/>
</Alert>
</MultiButton>
</RewardActions>
</RewardHeader>
<RewardDescription>{item.description}</RewardDescription>
</RewardItemContainer>
);
}
export function RewardsTab() {
const { t } = useTranslation();
const dispatch = useAppDispatch();
const [cursorPosition, setCursorPosition] = useState(0);
const [config] = useModule(modules.loyaltyConfig);
const [rewards, setRewards] = useModule(modules.loyaltyRewards);
const [filter, setFilter] = useState('');
const [dialogReward, setDialogReward] = useState<{
open: boolean;
new: boolean;
reward: LoyaltyReward;
}>({ open: false, new: false, reward: null });
const [requiredInfo, setRequiredInfo] = useState({
enabled: false,
text: '',
});
const filterLC = filter.toLowerCase();
const deleteReward = (id: string) => {
void dispatch(setRewards(rewards?.filter((r) => r.id !== id) ?? []));
};
const toggleReward = (id: string) => {
void dispatch(
setRewards(
rewards?.map((r) => {
if (r.id === id) {
return {
...r,
enabled: !r.enabled,
};
}
return r;
}) ?? [],
),
);
};
return (
<>
<Dialog
open={dialogReward.open}
onOpenChange={(state) =>
setDialogReward({ ...dialogReward, open: state })
}
>
<DialogContent
title={
dialogReward.new
? t('pages.loyalty-rewards.create-reward')
: t('pages.loyalty-rewards.edit-reward')
}
closeButton={true}
>
<form
onSubmit={(e) => {
e.preventDefault();
if (!(e.target as HTMLFormElement).checkValidity()) {
return;
}
const { reward } = dialogReward;
if (requiredInfo.enabled) {
reward.required_info = requiredInfo.text;
}
const index = rewards?.findIndex((r) => r.id === reward.id);
if (index >= 0) {
const newRewards = rewards.slice(0);
newRewards[index] = reward;
void dispatch(setRewards(newRewards));
} else {
void dispatch(setRewards([...(rewards ?? []), reward]));
}
setDialogReward({ ...dialogReward, open: false });
}}
>
<Field size="fullWidth" spacing="narrow">
<Label htmlFor="reward-id">
{t('pages.loyalty-rewards.reward-id')}
</Label>
<ControlledInputBox
id="reward-id"
type="text"
required
disabled={!dialogReward.new}
value={dialogReward?.reward?.id}
onFocus={(e) => {
e.target.selectionStart = cursorPosition;
}}
onChange={(e) => {
setCursorPosition(e.target.selectionStart);
setDialogReward({
...dialogReward,
reward: {
...dialogReward?.reward,
id:
e.target.value
?.toLowerCase()
.replace(/[^a-z0-9]/gi, '-') ?? '',
},
});
if (
dialogReward.new &&
rewards.find((r) => r.id === e.target.value)
) {
e.target.setCustomValidity(
t('pages.loyalty-rewards.id-already-in-use'),
);
} else {
e.target.setCustomValidity('');
}
}}
/>
<FieldNote>{t('pages.loyalty-rewards.reward-id-hint')}</FieldNote>
</Field>
<Field size="fullWidth" spacing="narrow">
<Label htmlFor="reward-name">
{t('pages.loyalty-rewards.reward-name')}
</Label>
<InputBox
id="reward-name"
type="text"
required
value={dialogReward?.reward?.name ?? ''}
onChange={(e) => {
setDialogReward({
...dialogReward,
reward: {
...dialogReward?.reward,
name: e.target.value,
},
});
}}
/>
<FieldNote>
{t('pages.loyalty-rewards.reward-name-hint')}
</FieldNote>
</Field>
<Field size="fullWidth" spacing="narrow">
<Label htmlFor="reward-icon">
{t('pages.loyalty-rewards.reward-icon')}
</Label>
<InputBox
id="reward-icon"
type="text"
value={dialogReward?.reward?.image ?? ''}
onChange={(e) => {
setDialogReward({
...dialogReward,
reward: {
...dialogReward?.reward,
image: e.target.value,
},
});
}}
/>
</Field>
<Field size="fullWidth" spacing="narrow">
<Label htmlFor="reward-desc">
{t('pages.loyalty-rewards.reward-desc')}
</Label>
<Textarea
id="reward-desc"
value={dialogReward?.reward?.description ?? ''}
onChange={(e) => {
setDialogReward({
...dialogReward,
reward: {
...dialogReward?.reward,
description: e.target.value,
},
});
}}
>
{dialogReward?.reward?.description ?? ''}
</Textarea>
</Field>
<Field size="fullWidth" spacing="narrow">
<Label htmlFor="reward-cost">
{t('pages.loyalty-rewards.reward-cost')}
</Label>
<InputBox
id="reward-cost"
type="number"
required
defaultValue={dialogReward?.reward?.price}
onChange={(e) => {
setDialogReward({
...dialogReward,
reward: {
...dialogReward?.reward,
price: parseInt(e.target.value, 10),
},
});
}}
/>
</Field>
<Field size="fullWidth" spacing="narrow">
<Label htmlFor="reward-cooldown">
{t('pages.loyalty-rewards.reward-cooldown')}
</Label>
<FlexRow align="left">
<Interval
value={dialogReward?.reward?.cooldown ?? 0}
active={true}
onChange={(cooldown) => {
setDialogReward({
...dialogReward,
reward: {
...dialogReward?.reward,
cooldown,
},
});
}}
/>
</FlexRow>
</Field>
<Field size="fullWidth" spacing="narrow">
<FlexRow align="left" spacing="1">
<Checkbox
id="reward-details"
checked={requiredInfo.enabled}
onCheckedChange={(e) => {
setRequiredInfo({
...requiredInfo,
enabled: !!e,
});
}}
>
<CheckboxIndicator>
{requiredInfo.enabled && <CheckIcon />}
</CheckboxIndicator>
</Checkbox>
<Label htmlFor="reward-details">
{t('pages.loyalty-rewards.reward-details')}
</Label>
</FlexRow>
<InputBox
id="reward-details-text"
type="text"
disabled={!requiredInfo.enabled}
required={requiredInfo.enabled}
value={dialogReward?.reward?.required_info ?? ''}
placeholder={t(
'pages.loyalty-rewards.reward-details-placeholder',
)}
onChange={(e) => {
setRequiredInfo({ ...requiredInfo, text: e.target.value });
}}
/>
</Field>
<DialogActions>
<Button variation="primary" type="submit">
{dialogReward.new
? t('form-actions.create')
: t('form-actions.edit')}
</Button>
<Button
type="button"
onClick={() =>
setDialogReward({ ...dialogReward, open: false })
}
>
{t('form-actions.cancel')}
</Button>
</DialogActions>
</form>
</DialogContent>
</Dialog>
<Field size="fullWidth" spacing="none">
<FlexRow css={{ flex: 1, alignItems: 'stretch' }} spacing="1">
<Button
variation="primary"
onClick={() => {
setRequiredInfo({
enabled: false,
text: '',
});
setDialogReward({
open: true,
new: true,
reward: {
id: '',
enabled: true,
name: '',
description: '',
image: '',
price: 0,
cooldown: 0,
},
});
}}
>
<PlusIcon /> {t('pages.loyalty-rewards.create-reward')}
</Button>
<InputBox
css={{ flex: 1 }}
placeholder={t('pages.loyalty-rewards.reward-filter')}
value={filter}
onChange={(e) => setFilter(e.target.value)}
/>
</FlexRow>
</Field>
<RewardList>
{rewards && rewards.length > 0 ? (
rewards
?.filter(
(r) =>
r.name.toLowerCase().includes(filterLC) ||
r.id.toLowerCase().includes(filterLC) ||
r.description.toLowerCase().includes(filterLC),
)
.map((r) => (
<RewardItem
key={r.id}
name={r.id}
item={r}
currency={(
config?.currency || t('pages.loyalty-queue.points')
).toLowerCase()}
onEdit={() =>
setDialogReward({
open: true,
new: false,
reward: r,
})
}
onDelete={() => deleteReward(r.id)}
onToggle={() => toggleReward(r.id)}
/>
))
) : (
<NoneText>{t('pages.loyalty-rewards.no-rewards')}</NoneText>
)}
</RewardList>
</>
);
}