/** * QuickWins Component. */ import { useDate } from '@/store/useDateStore'; import { useFilters } from '@/hooks/useFilters'; import { useQuery, useMutation, useQueryClient } from '@tanstack/react-query'; import { __ } from '@wordpress/i18n'; import { Block } from '@/components/Blocks/Block'; import { BlockHeading } from '@/components/Blocks/BlockHeading'; import { BlockContent } from '@/components/Blocks/BlockContent'; import getQuickWins from '@/api/getQuickWins'; import Icon from '@/utils/Icon'; import { Root, Trigger, Content, Close } from '@radix-ui/react-popover'; import { useMemo } from 'react'; import { doAction } from '@/utils/api'; import { toast } from '@/utils/toast'; import { motion, AnimatePresence } from 'framer-motion'; import { formatUnixToDate } from '@/utils/formatting'; import Tooltip from '@/components/Common/Tooltip'; type QuickWinsKeys = 'critical' | 'opportunity' | 'recommended'; interface QuickWinItem { type: QuickWinsKeys; key: string; title: string; message: string; recommendation: string; url: string | null; } interface DateRange { date_start: number; date_end: number; } interface QuickWinsData { quickWins: QuickWinItem[]; dateRange: DateRange | null; } const blockContentProps = { className: 'p-0' }; const iconMap = { critical: { icon: 'zap', backgroundColor: 'bg-red' }, opportunity: { icon: 'sun', backgroundColor: 'bg-green' }, recommended: { icon: 'bulb', backgroundColor: 'bg-blue' } }; /** * QuickWins component. * * @return {JSX.Element} The QuickWins component. */ const QuickWins = (): JSX.Element => { const { startDate, endDate, range } = useDate( ( state ) => state ); const { filters } = useFilters(); const queryClient = useQueryClient(); const quickWinsQuery = useQuery({ queryKey: [ 'quickWins' ], queryFn: () => getQuickWins({ startDate, endDate, range, filters }), placeholderData: null, gcTime: 10000 }); const dismissMutation = useMutation({ mutationFn: async( key: string ) => { const response = await doAction( 'ecommerce/quick-win/dismiss', { id: key }); if ( ! response?.success ) { throw new Error( response?.message || 'Failed to dismiss quick win' ); } return response; }, onMutate: async( key: string ) => { await queryClient.cancelQueries({ queryKey: [ 'quickWins' ] }); const previousData = queryClient.getQueryData( [ 'quickWins' ] ); if ( previousData?.quickWins ) { const newData = { ...previousData, quickWins: previousData.quickWins.filter( ( item ) => item.key !== key ) }; queryClient.setQueryData([ 'quickWins' ], newData ); } return { previousData }; }, onError: ( err, key, context ) => { // Rollback if mutation fails or backend returned success=false if ( context?.previousData ) { queryClient.setQueryData([ 'quickWins' ], context.previousData ); } toast.error( err instanceof Error ? err.message : __( 'An error occurred', 'burst-statistics' ) ); }, onSuccess: () => {} // eslint-disable-line @typescript-eslint/no-empty-function }); const snoozeMutation = useMutation({ mutationFn: async( key: string ) => { const response = await doAction( 'ecommerce/quick-win/snooze', { id: key }); if ( ! response?.success ) { throw new Error( response?.message || 'Failed to snooze quick win' ); } return response; }, onMutate: async( key: string ) => { await queryClient.cancelQueries({ queryKey: [ 'quickWins' ] }); const previousData = queryClient.getQueryData( [ 'quickWins' ] ); if ( previousData?.quickWins ) { const newData = { ...previousData, quickWins: previousData.quickWins.filter( ( item ) => item.key !== key ) }; queryClient.setQueryData([ 'quickWins' ], newData ); } return { previousData }; }, onError: ( err, key, context ) => { // Rollback if mutation fails or backend returned success=false if ( context?.previousData ) { queryClient.setQueryData([ 'quickWins' ], context.previousData ); } toast.error( err instanceof Error ? err.message : __( 'An error occurred', 'burst-statistics' ) ); }, onSuccess: () => {} // eslint-disable-line @typescript-eslint/no-empty-function }); const quickWinsData = quickWinsQuery.data || { quickWins: [], dateRange: null }; // Format date range for display. const dateControl = useMemo( () => { if ( ! quickWinsData.dateRange ) { return null; } const { date_start, date_end } = quickWinsData.dateRange; const formattedDateStart = formatUnixToDate( date_start ); const formattedDateEnd = formatUnixToDate( date_end ); return ( {formattedDateStart} - {formattedDateEnd} ); }, [ quickWinsData.dateRange ]); const blockHeadingProps = { title: __( 'Opportunities', 'burst-statistics' ), controls: dateControl, isLoading: quickWinsQuery.isFetching }; const quickWinVariants = { hidden: { opacity: 0, y: 0 }, visible: { opacity: 1, y: 0 }, exit: { opacity: 0, x: 400 } }; return ( {quickWinsQuery.isFetching ? (

{__( 'Loading…', 'burst-statistics' )}

) : quickWinsData.quickWins && 0 < quickWinsData.quickWins.length ? (
{quickWinsData.quickWins.map( ( quickWin ) => { if ( ! quickWin.type ) { return null; } const { type, key, message, recommendation, url, title } = quickWin; return (
{iconMap[type] && ( )}

{title}

{message}

{recommendation && ( )}
); })}
) : (

{__( 'Crunching data. Check back later to see if there are quick wins!', 'burst-statistics' )}

)}
); }; export default QuickWins;