implemented merging

This commit is contained in:
nvrl committed 2026-05-27 16:42:59 +02:00
1 parent 1e6a6b93b1
commit 41c1b3c574
7 files changed
+390 -43

No files matched your search

+2
View File
@@ -53,6 +53,7 @@ const AdminBanners = lazy(() => import('./pages/admin/AdminBanners'));
const AdminJobs = lazy(() => import('./pages/admin/AdminJobs'));
const AdminTickets = lazy(() => import('./pages/admin/AdminTickets'));
const AdminConfig = lazy(() => import('./pages/admin/AdminConfig'));
const AdminFlavors = lazy(() => import('./pages/admin/AdminFlavors'));
const AdminUserDetail = lazy(() => import('./pages/AdminUserDetail'));
const AdminRatingDetail = lazy(() => import('./pages/AdminRatingDetail'));
const AdminReplyDetail = lazy(() => import('./pages/AdminReplyDetail'));
@@ -338,6 +339,7 @@ const App: React.FC = () => {
<Route path="users" element={<AdminUsers />} />
<Route path="users/:id" element={<AdminUserDetail />} />
<Route path="user/:id" element={<AdminUserDetail />} />
<Route path="flavors" element={<AdminFlavors />} />
<Route path="banners" element={<AdminBanners />} />
<Route path="jobs" element={<AdminJobs />} />
<Route path="tickets" element={<AdminTickets />} />
+168
View File
@@ -0,0 +1,168 @@
import React, { useState } from 'react';
import {
Autocomplete,
Box,
Button,
Card,
CardContent,
Dialog,
DialogActions,
DialogContent,
DialogContentText,
DialogTitle,
Divider,
Grid,
TextField,
Typography,
} from '@mui/material';
import { useTranslation } from 'react-i18next';
import { useSnackbar } from '../../contexts/NotificationContext';
import { useFlavorsList } from '../../api/queries/useFlavorsList';
import api from '../../lib/api';
import { useQueryClient } from '@tanstack/react-query';
import { queryKeys } from '../../api/keys';
const AdminFlavors: React.FC = () => {
const { t } = useTranslation();
const { notify } = useSnackbar();
const queryClient = useQueryClient();
const { data: flavors = [], isLoading } = useFlavorsList();
const [keepFlavor, setKeepFlavor] = useState<any>(null);
const [removeFlavor, setRemoveFlavor] = useState<any>(null);
const [confirmOpen, setConfirmOpen] = useState(false);
const [isMerging, setIsMerging] = useState(false);
const handleMerge = async () => {
if (!keepFlavor || !removeFlavor) return;
setIsMerging(true);
try {
await api.post('admin/merge_flavors/', {
keep_id: keepFlavor.id,
remove_id: removeFlavor.id,
});
notify({ message: t('admin.mergeSuccess', { defaultValue: 'Flavors merged successfully' }), severity: 'success' });
// Invalidate queries
queryClient.invalidateQueries({ queryKey: queryKeys.flavors({ scope: 'all' }) });
// Reset selection
setKeepFlavor(null);
setRemoveFlavor(null);
setConfirmOpen(false);
} catch (err: any) {
const errorMsg = err.response?.data?.error || err.message;
notify({ message: `Error: ${errorMsg}`, severity: 'error' });
} finally {
setIsMerging(false);
}
};
const canMerge = keepFlavor && removeFlavor && keepFlavor.id !== removeFlavor.id;
return (
<Box>
<Typography variant="h5" sx={{ mb: 3, fontWeight: 'bold' }}>
{t('admin.manageFlavors', { defaultValue: 'Manage Flavors' })}
</Typography>
<Card sx={{ mb: 4 }}>
<CardContent>
<Typography variant="h6" gutterBottom>
{t('admin.mergeFlavors', { defaultValue: 'Merge Flavors' })}
</Typography>
<Typography variant="body2" color="text.secondary" sx={{ mb: 3 }}>
{t('admin.mergeFlavorsDesc', {
defaultValue: 'Combine two flavors into one. Ratings from the source flavor will be moved to the target flavor. If a user rated both, the more detailed review is kept.'
})}
</Typography>
<Grid container spacing={3} alignItems="center">
<Grid item xs={12} md={5}>
<Autocomplete
options={flavors}
getOptionLabel={(option) => `${option.name} (${option.category_name})`}
value={removeFlavor}
onChange={(_, newValue) => setRemoveFlavor(newValue)}
loading={isLoading}
renderInput={(params) => (
<TextField
{...params}
label={t('admin.flavorToRemove', { defaultValue: 'Flavor to Remove (Source)' })}
helperText={t('admin.flavorToRemoveHint', { defaultValue: 'This flavor will be DELETED' })}
/>
)}
/>
</Grid>
<Grid item xs={12} md={2} sx={{ textAlign: 'center' }}>
<Typography variant="h4">→</Typography>
</Grid>
<Grid item xs={12} md={5}>
<Autocomplete
options={flavors}
getOptionLabel={(option) => `${option.name} (${option.category_name})`}
value={keepFlavor}
onChange={(_, newValue) => setKeepFlavor(newValue)}
loading={isLoading}
renderInput={(params) => (
<TextField
{...params}
label={t('admin.flavorToKeep', { defaultValue: 'Flavor to Keep (Target)' })}
helperText={t('admin.flavorToKeepHint', { defaultValue: 'This flavor will PRESERVED' })}
/>
)}
/>
</Grid>
<Grid item xs={12}>
<Divider sx={{ my: 1 }} />
<Box sx={{ display: 'flex', justifyContent: 'flex-end', mt: 2 }}>
<Button
variant="contained"
color="warning"
disabled={!canMerge}
onClick={() => setConfirmOpen(true)}
>
{t('admin.startMerge', { defaultValue: 'Start Merge' })}
</Button>
</Box>
</Grid>
</Grid>
</CardContent>
</Card>
<Dialog open={confirmOpen} onClose={() => !isMerging && setConfirmOpen(false)}>
<DialogTitle>{t('admin.confirmMergeTitle', { defaultValue: 'Confirm Flavor Merge' })}</DialogTitle>
<DialogContent>
<DialogContentText>
{t('admin.confirmMergeText', {
defaultValue: 'Are you sure you want to merge "{{remove}}" into "{{keep}}"? This action is irreversible. "{{remove}}" will be deleted, and all its ratings will be transferred.',
remove: removeFlavor?.name,
keep: keepFlavor?.name
})}
</DialogContentText>
</DialogContent>
<DialogActions>
<Button onClick={() => setConfirmOpen(false)} disabled={isMerging}>
{t('common.cancel', { defaultValue: 'Cancel' })}
</Button>
<Button
onClick={handleMerge}
color="warning"
variant="contained"
autoFocus
disabled={isMerging}
>
{isMerging ? t('common.processing', { defaultValue: 'Processing...' }) : t('admin.confirmMerge', { defaultValue: 'Merge Flavors' })}
</Button>
</DialogActions>
</Dialog>
</Box>
);
};
export default AdminFlavors;
+2
View File
@@ -16,6 +16,7 @@ import WallpaperIcon from '@mui/icons-material/Wallpaper';
import TerminalIcon from '@mui/icons-material/Terminal';
import HelpOutlineIcon from '@mui/icons-material/HelpOutline';
import TuneIcon from '@mui/icons-material/Tune';
import LocalDrinkIcon from '@mui/icons-material/LocalDrink';
import { useTranslation } from 'react-i18next';
import { PageShell } from '../../components/ui';
import { useTitle } from '../../hooks/useTitle';
@@ -23,6 +24,7 @@ import { useTitle } from '../../hooks/useTitle';
const SECTIONS = [
{ value: 'overview', icon: <SettingsIcon /> },
{ value: 'users', icon: <PeopleIcon /> },
{ value: 'flavors', icon: <LocalDrinkIcon /> },
{ value: 'banners', icon: <WallpaperIcon /> },
{ value: 'jobs', icon: <TerminalIcon /> },
{ value: 'tickets', icon: <HelpOutlineIcon /> },