diff --git a/components/common/EmptyState.tsx b/components/common/EmptyState.tsx index edcdf559..0a66e66c 100644 --- a/components/common/EmptyState.tsx +++ b/components/common/EmptyState.tsx @@ -19,13 +19,15 @@ export const EmptyState: FC = ({ }): JSX.Element => { return ( <> -
+
{icon}
-

{title}

- {description && ( -

{description}

- )} - {children &&
{children}
} +
+

{title}

+ {description && ( +

{description}

+ )} + {children &&
{children}
} +
) diff --git a/components/common/UserRightSideMenu/SpeedDialContent.tsx b/components/common/UserRightSideMenu/SpeedDialContent.tsx index e0b0b442..67e6553c 100644 --- a/components/common/UserRightSideMenu/SpeedDialContent.tsx +++ b/components/common/UserRightSideMenu/SpeedDialContent.tsx @@ -309,17 +309,19 @@ export const SpeedDialContent = () => { ))} {/* empty state */} {isSpeedDialLoaded && !getSpeedDialError && !speedDials.length && ( - +
+ +
)} {/* Iterate through speed dial list */} {isSpeedDialLoaded && diff --git a/components/common/UserRightSideMenu/UserLastCallsContent.tsx b/components/common/UserRightSideMenu/UserLastCallsContent.tsx index df5555fe..166e6291 100644 --- a/components/common/UserRightSideMenu/UserLastCallsContent.tsx +++ b/components/common/UserRightSideMenu/UserLastCallsContent.tsx @@ -225,13 +225,15 @@ export const UserLastCallsContent = () => { ))} {/* empty state */} {lastCalls?.length === 0 && ( - - )} +
+ +
+ )} {/* Iterate through speed dial list */} {lastCalls?.length! > 0 && lastCalls?.map((call: any, key: any) => ( diff --git a/components/common/UserRightSideMenu/VoiceMailContent.tsx b/components/common/UserRightSideMenu/VoiceMailContent.tsx new file mode 100644 index 00000000..d16f1b68 --- /dev/null +++ b/components/common/UserRightSideMenu/VoiceMailContent.tsx @@ -0,0 +1,522 @@ +// Copyright (C) 2025 Nethesis S.r.l. +// SPDX-License-Identifier: AGPL-3.0-or-later + +import type { SpeedDialType } from '../../../services/types' +import { useState, useEffect, useRef, MutableRefObject } from 'react' +import { FontAwesomeIcon } from '@fortawesome/react-fontawesome' +import { + faPhone, + faPlus, + faTriangleExclamation, + faEllipsisVertical, + faPen, + faBolt, + faTrash, + faFileImport, + faFileArrowDown, + faCheckCircle, + faVoicemail, +} from '@fortawesome/free-solid-svg-icons' +import { Button, Avatar, Modal, Dropdown, InlineNotification, EmptyState } from '..' +import { + deleteSpeedDial, + deleteAllSpeedDials, + getSpeedDials, + importCsvSpeedDial, +} from '../../../services/phonebook' +import { + sortSpeedDials, + openCreateSpeedDialDrawer, + openEditSpeedDialDrawer, + exportSpeedDial, +} from '../../../lib/speedDial' +import { t } from 'i18next' +import { callPhoneNumber, transferCallToExtension } from '../../../lib/utils' +import { useSelector } from 'react-redux' +import { RootState } from '../../../store' +import { Tooltip } from 'react-tooltip' +import { store } from '../../../store' + +export const VoiceMailContent = () => { + // The state for the delete modal + const [showDeleteModal, setShowDeleteModal] = useState(false) + // The state for delete all speed dial modal + const [showDeleteAllSpeedDialModal, setShowDeleteAllSpeedDialModal] = useState(false) + // The state for the speed dials list + const [speedDials, setSpeedDials] = useState([]) + // The state for current item selected for editing or deletion + const [currentItem, setCurrentItem] = useState(null) + // The reference for the cancel button of the delete speed dial modal + const cancelDeleteButtonRef = useRef() as MutableRefObject + // The state for the name to be deleted + const [deletingName, setDeletingName] = useState('') + const [csvBase64, setCsvBase64] = useState('') + const [showImportCsvModal, setShowImportCsvModal] = useState(false) + const [importCsvError, setImportCsvError] = useState('') + + const [isSpeedDialLoaded, setSpeedDialLoaded] = useState(false) + const [deleteSpeedDialError, setDeleteSpeedDialError] = useState('') + const [deleteAllSpeedDialError, setDeleteAllSpeedDialError] = useState('') + const [getSpeedDialError, setGetSpeedDialError] = useState('') + + const { profile } = useSelector((state: RootState) => state.user) + const operators: any = useSelector((state: RootState) => state.operators) + + const authStore = useSelector((state: RootState) => state.authentication) + + const [firstRender, setFirstRender] = useState(true) + + useEffect(() => { + if (firstRender) { + setFirstRender(false) + return + } + // Initialize the speed dial list the first time + // and every time a reload is required + const initSpeedDials = async () => { + if (!isSpeedDialLoaded && profile?.macro_permissions?.phonebook?.value) { + try { + setGetSpeedDialError('') + const speedDials: SpeedDialType[] | undefined = await getSpeedDials() + // remove operators favorite contacts + const filteredSpeedDials = speedDials.filter( + (speedDial) => speedDial.notes !== 'speeddial-favorite', + ) + // Sort the speed dials and update the list + setSpeedDials(sortSpeedDials(filteredSpeedDials)) + setSpeedDialLoaded(true) + } catch (error) { + setGetSpeedDialError('Cannot retrieve speed dial') + } + } + } + initSpeedDials() + }, [firstRender, isSpeedDialLoaded, profile?.macro_permissions?.phonebook?.value]) + + const speedDialStore = useSelector((state: RootState) => state.speedDial) + + useEffect(() => { + // reload speed dial + setSpeedDialLoaded(false) + }, [speedDialStore]) + + // Handle the delete action on item + const confirmDeleteItem = (speedDial: any) => { + setCurrentItem(speedDial) + setDeletingName(speedDial.name) + setDeleteSpeedDialError('') + setShowDeleteModal(true) + } + + // Handle the delete action on item + const confirmDeleteAllItems = () => { + setDeleteAllSpeedDialError('') + setShowDeleteAllSpeedDialModal(true) + } + + // Execute the service method to delete all items + const handleDeleteAllItems = async () => { + try { + // Single delete post for every speed dial elements + for (const item of speedDials) { + if (item?.id) { + try { + await deleteSpeedDial({ + id: item.id.toString(), + }) + } catch (error) { + console.error(`Error deleting ${item.id}:`, error) + setDeleteSpeedDialError(t('SpeedDial.Cannot delete speed dial') || '') + continue + } + } + } + } catch (error) { + setDeleteSpeedDialError(t('SpeedDial.Cannot delete speed dial') || '') + return + } + setSpeedDialLoaded(false) + setShowDeleteAllSpeedDialModal(false) + } + + // Execute the service method to delete an item + const handleDeleteItem = async () => { + if (currentItem?.id) { + // Use the id to perform actions + try { + const deleted = await deleteSpeedDial({ + id: currentItem.id.toString(), + }) + } catch (error) { + setDeleteSpeedDialError(t('SpeedDial.Cannot delete speed dial') || '') + return + } + setSpeedDialLoaded(false) + setShowDeleteModal(false) + setCurrentItem(null) + } + } + + // Execute the service method to import speed dial + const handleImportCsv = async () => { + if (csvBase64) { + try { + const imported = await importCsvSpeedDial({ + file64: csvBase64.toString(), + }) + } catch (error) { + setImportCsvError(t('SpeedDial.Cannot import speed dial') || '') + return + } + setSpeedDialLoaded(false) + setShowImportCsvModal(false) + } + } + + const callSpeedDial = (speedDial: any) => { + if ( + operators?.operators[authStore?.username]?.mainPresence && + operators?.operators[authStore?.username]?.mainPresence === 'busy' + ) { + transferCallToExtension(speedDial?.speeddial_num) + } else if ( + operators?.operators[authStore?.username]?.endpoints?.mainextension[0]?.id !== + speedDial?.speeddial_num + ) { + callPhoneNumber(speedDial?.speeddial_num) + } + } + + function importSpeedDial(selectedFile: any) { + if (selectedFile?.target?.files && selectedFile.target.files[0]) { + const reader = new FileReader() + reader.onload = (ev: any) => { + setCsvBase64(ev.target?.result as string) + if (selectedFile.target) { + selectedFile.target.value = '' + } + } + reader.readAsDataURL(selectedFile.target.files[0]) + // open modal to confirm the upload + setShowImportCsvModal(true) + setImportCsvError('') + } else { + setImportCsvError(t('SpeedDial.Upload failed') || '') + } + } + + // The dropdown items for every speed dial element + const getItemsMenu = (speedDial: any) => ( + <> + openEditSpeedDialDrawer(speedDial)}> + {t('Common.Edit')} + + confirmDeleteItem(speedDial)}> + {t('Common.Delete')} + + + ) + + // The dropdown items for import or export speed dial element + const getSpeedDialMenuTemplate = () => ( + <> + { + const input = document.createElement('input') + input.type = 'file' + input.accept = '.csv' + input.onchange = (e) => { + importSpeedDial(e) + } + input.click() + }} + > + {t('SpeedDial.Import CSV')} + + {/* if the list of speed dial is not empty */} + {speedDials.length > 0 && ( + <> + exportSpeedDial(speedDials)}> + {t('SpeedDial.Export CSV')} + +
+ + confirmDeleteAllItems()}> + {t('SpeedDial.Delete all')} + + + )} + + ) + + return ( + <> +
+
+
+

+ {t('VoiceMail.Voicemail inbox')} +

+
+ {' '} + {isSpeedDialLoaded && !!speedDials.length && ( +
+ +
+ )} + + + +
+
+
+ +
    + {/* get speed dial error */} + {getSpeedDialError && ( + + )} + {/* skeleton */} + {!isSpeedDialLoaded && + !getSpeedDialError && + Array.from(Array(4)).map((e, index) => ( +
  • +
    + {/* avatar skeleton */} +
    +
    +
    + {/* line skeleton */} +
    +
    +
    +
    +
  • + ))} + {/* empty state */} + {isSpeedDialLoaded && !getSpeedDialError && !speedDials.length && ( +
    +
    + )} + {/* Iterate through speed dial list */} + {isSpeedDialLoaded && + speedDials.map((speedDial: any, key: any) => ( +
  • +
    + +
  • + ))} +
+
+ {/* Delete speed dial modal */} + setShowDeleteModal(false)} + afterLeave={() => setDeletingName('')} + > + +
+
+
+

+ {t('SpeedDial.Delete speed dial')} +

+
+

+ {t('SpeedDial.Speed dial delete message', { deletingName })} +

+
+ {/* delete speed dial error */} + {deleteSpeedDialError && ( + + )} +
+
+ + + + +
+ {/* Delete all speed dials modal */} + setShowDeleteAllSpeedDialModal(false)} + > + +
+
+
+

+ {t('SpeedDial.Delete all speed dials')} +

+
+

+ + {t('SpeedDial.Delete all speed dials message')} + +

+

+ {t('SpeedDial.Are you sure?')} +

+
+ {/* delete all speed dials error */} + {deleteAllSpeedDialError && ( + + )} +
+
+ + + + +
+ {/* Upload speed dial from Csv*/} + setShowImportCsvModal(false)} + > + +
+
+
+

+ {t('SpeedDial.Speed dial import')} +

+
+

+ {t('SpeedDial.Start importing Speed Dial from csv file?')} +

+
+ {/* import csv error */} + {importCsvError !== '' && ( + + )} +
+
+ + + + +
+ + + ) +} diff --git a/components/layout/UserNavBar.tsx b/components/layout/UserNavBar.tsx index dfe836f2..146a5e19 100644 --- a/components/layout/UserNavBar.tsx +++ b/components/layout/UserNavBar.tsx @@ -5,13 +5,14 @@ import { FC, useCallback, useEffect, useState } from 'react' import { SpeedDial } from './SpeedDial' import { FontAwesomeIcon } from '@fortawesome/react-fontawesome' -import { faBolt, faPhone, type IconDefinition } from '@fortawesome/free-solid-svg-icons' +import { faBolt, faPhone, faVoicemail, type IconDefinition } from '@fortawesome/free-solid-svg-icons' import { getJSONItem, loadPreference, setJSONItem } from '../../lib/storage' import { RootState, store } from '../../store' import { useSelector } from 'react-redux' import { UserLastCalls } from './UserLastCalls' import { Tooltip } from 'react-tooltip' import { useTranslation } from 'react-i18next' +import { UserVoiceMail } from './UserVoiceMail' const activeStyles = { width: '.1875rem', @@ -43,6 +44,12 @@ export const UserNavBar: FC = () => { active: false, label: t('NavBars.Last calls'), }, + { + icon: faVoicemail, + name: 'voice_mail', + active: false, + label: t('NavBars.Voice mail'), + } ]) const rightSideStatus: any = useSelector((state: RootState) => state.rightSideMenu) @@ -177,6 +184,8 @@ export const UserNavBar: FC = () => { return } else if (tab.active && tab.name === 'last_calls') { return + } else if (tab.active && tab.name === 'voice_mail') { + return } })} {/* The side menu */} @@ -213,7 +222,7 @@ export const UserNavBar: FC = () => { type TabTypes = { icon: IconDefinition - name: 'speed_dial' | 'last_calls' + name: 'speed_dial' | 'last_calls' | 'voice_mail' active: boolean label: string } diff --git a/components/layout/UserVoiceMail.tsx b/components/layout/UserVoiceMail.tsx new file mode 100644 index 00000000..2d10ddcb --- /dev/null +++ b/components/layout/UserVoiceMail.tsx @@ -0,0 +1,20 @@ +// Copyright (C) 2025 Nethesis S.r.l. +// SPDX-License-Identifier: AGPL-3.0-or-later + +/** + * The SpeedDial component + * + * @return The fixed right bar with speed dials as the default + */ + +import { VoiceMailContent } from '../common/UserRightSideMenu/VoiceMailContent' + +export const UserVoiceMail = () => { + return ( + <> + + + ) +} diff --git a/pages/history.tsx b/pages/history.tsx index 19ea35d7..1863fba3 100644 --- a/pages/history.tsx +++ b/pages/history.tsx @@ -653,7 +653,6 @@ const History: NextPage = () => {
- {' '} {/* empty state */} {isHistoryLoaded && history?.count === 0 && ( { } > )} + {/* history table */} {isHistoryLoaded && history?.count !== 0 && (
diff --git a/pages/queuemanager.tsx b/pages/queuemanager.tsx index 78ca275e..db3a8b06 100644 --- a/pages/queuemanager.tsx +++ b/pages/queuemanager.tsx @@ -63,7 +63,7 @@ const QueueManager: NextPage = () => { section = 'Dashboard' } changeSection(section) - // eslint-disable-next-line react-hooks/exhaustive-deps + // eslint-disable-next-line react-hooks/exhaustive-deps }, [firstRender]) const changeSection = (sectionName: string) => { @@ -133,7 +133,7 @@ const QueueManager: NextPage = () => {
{/* desktop tabs */} -
+