Implemented notifications.
Some checks failed
farmcontrol/farmcontrol-ui/pipeline/head There was a failure building this commit
Some checks failed
farmcontrol/farmcontrol-ui/pipeline/head There was a failure building this commit
This commit is contained in:
parent
1e0f125f21
commit
2622fae555
@ -34,6 +34,7 @@
|
||||
.g2-tooltip-list-item,
|
||||
.ant-picker-input,
|
||||
.ant-picker-header-view button,
|
||||
.ant-badge,
|
||||
[class*=' ant-radio'] {
|
||||
font-family: 'DM Sans';
|
||||
}
|
||||
|
||||
17
src/App.jsx
17
src/App.jsx
@ -24,6 +24,7 @@ import {
|
||||
} from './components/Dashboard/context/ThemeContext'
|
||||
import AppError from './components/App/AppError'
|
||||
import { ApiServerProvider } from './components/Dashboard/context/ApiServerContext.jsx'
|
||||
import { NotificationProvider } from './components/Dashboard/context/NotificationContext.jsx'
|
||||
import { ElectronProvider } from './components/Dashboard/context/ElectronContext.jsx'
|
||||
import { MessageProvider } from './components/Dashboard/context/MessageContext.jsx'
|
||||
import AuthCallback from './components/App/AuthCallback.jsx'
|
||||
@ -59,9 +60,10 @@ const AppContent = () => {
|
||||
<AuthProvider>
|
||||
<PrintServerProvider>
|
||||
<ApiServerProvider>
|
||||
<SpotlightProvider>
|
||||
<ActionsModalProvider>
|
||||
<MessageProvider>
|
||||
<MessageProvider>
|
||||
<NotificationProvider>
|
||||
<SpotlightProvider>
|
||||
<ActionsModalProvider>
|
||||
<Routes>
|
||||
<Route
|
||||
path='/dashboard/electron/spotlightcontent'
|
||||
@ -112,10 +114,11 @@ const AppContent = () => {
|
||||
/>
|
||||
}
|
||||
/>
|
||||
</Routes>
|
||||
</MessageProvider>
|
||||
</ActionsModalProvider>
|
||||
</SpotlightProvider>
|
||||
</Routes>
|
||||
</ActionsModalProvider>
|
||||
</SpotlightProvider>
|
||||
</NotificationProvider>
|
||||
</MessageProvider>
|
||||
</ApiServerProvider>
|
||||
</PrintServerProvider>
|
||||
</AuthProvider>
|
||||
|
||||
@ -22,6 +22,7 @@ import ObjectActions from '../../common/ObjectActions.jsx'
|
||||
import ObjectTable from '../../common/ObjectTable.jsx'
|
||||
import InfoCollapsePlaceholder from '../../common/InfoCollapsePlaceholder.jsx'
|
||||
import DocumentPrintButton from '../../common/DocumentPrintButton.jsx'
|
||||
import UserNotifierToggle from '../../common/UserNotifierToggle.jsx'
|
||||
import ScrollBox from '../../common/ScrollBox.jsx'
|
||||
import {
|
||||
getModelByName,
|
||||
@ -134,6 +135,11 @@ const InvoiceInfo = () => {
|
||||
visibleState={collapseState}
|
||||
updateVisibleState={updateCollapseState}
|
||||
/>
|
||||
<UserNotifierToggle
|
||||
type='invoice'
|
||||
objectData={objectFormState.objectData}
|
||||
disabled={objectFormState.loading}
|
||||
/>
|
||||
<DocumentPrintButton
|
||||
type='invoice'
|
||||
objectData={objectFormState.objectData}
|
||||
|
||||
@ -20,6 +20,7 @@ import ObjectActions from '../../common/ObjectActions.jsx'
|
||||
import ObjectTable from '../../common/ObjectTable.jsx'
|
||||
import InfoCollapsePlaceholder from '../../common/InfoCollapsePlaceholder.jsx'
|
||||
import DocumentPrintButton from '../../common/DocumentPrintButton.jsx'
|
||||
import UserNotifierToggle from '../../common/UserNotifierToggle.jsx'
|
||||
import ScrollBox from '../../common/ScrollBox.jsx'
|
||||
import { getModelByName } from '../../../../database/ObjectModels.js'
|
||||
import PostPayment from './PostPayment.jsx'
|
||||
@ -109,6 +110,11 @@ const PaymentInfo = () => {
|
||||
visibleState={collapseState}
|
||||
updateVisibleState={updateCollapseState}
|
||||
/>
|
||||
<UserNotifierToggle
|
||||
type='payment'
|
||||
objectData={objectFormState.objectData}
|
||||
disabled={objectFormState.loading}
|
||||
/>
|
||||
<DocumentPrintButton
|
||||
type='payment'
|
||||
objectData={objectFormState.objectData}
|
||||
|
||||
@ -18,6 +18,7 @@ import NoteIcon from '../../../Icons/NoteIcon'
|
||||
import AuditLogIcon from '../../../Icons/AuditLogIcon'
|
||||
import InfoCollapsePlaceholder from '../../common/InfoCollapsePlaceholder'
|
||||
import DocumentPrintButton from '../../common/DocumentPrintButton'
|
||||
import UserNotifierToggle from '../../common/UserNotifierToggle'
|
||||
import ScrollBox from '../../common/ScrollBox'
|
||||
|
||||
const FilamentStockInfo = () => {
|
||||
@ -91,6 +92,11 @@ const FilamentStockInfo = () => {
|
||||
visibleState={collapseState}
|
||||
updateVisibleState={updateCollapseState}
|
||||
/>
|
||||
<UserNotifierToggle
|
||||
type='filamentStock'
|
||||
objectData={objectFormState.objectData}
|
||||
disabled={objectFormState.loading}
|
||||
/>
|
||||
<DocumentPrintButton
|
||||
type='filamentStock'
|
||||
objectData={objectFormState.objectData}
|
||||
|
||||
@ -19,6 +19,7 @@ import ActionHandler from '../../common/ActionHandler.jsx'
|
||||
import ObjectActions from '../../common/ObjectActions.jsx'
|
||||
import ObjectTable from '../../common/ObjectTable.jsx'
|
||||
import InfoCollapsePlaceholder from '../../common/InfoCollapsePlaceholder.jsx'
|
||||
import UserNotifierToggle from '../../common/UserNotifierToggle.jsx'
|
||||
import ScrollBox from '../../common/ScrollBox.jsx'
|
||||
|
||||
const log = loglevel.getLogger('OrderItemInfo')
|
||||
@ -99,6 +100,11 @@ const OrderItemInfo = () => {
|
||||
visibleState={collapseState}
|
||||
updateVisibleState={updateCollapseState}
|
||||
/>
|
||||
<UserNotifierToggle
|
||||
type='orderItem'
|
||||
objectData={objectFormState.objectData}
|
||||
disabled={objectFormState.loading}
|
||||
/>
|
||||
</Space>
|
||||
<LockIndicator lock={objectFormState.lock} />
|
||||
</Space>
|
||||
|
||||
@ -20,6 +20,7 @@ import ObjectActions from '../../common/ObjectActions.jsx'
|
||||
import ObjectTable from '../../common/ObjectTable.jsx'
|
||||
import InfoCollapsePlaceholder from '../../common/InfoCollapsePlaceholder.jsx'
|
||||
import DocumentPrintButton from '../../common/DocumentPrintButton.jsx'
|
||||
import UserNotifierToggle from '../../common/UserNotifierToggle.jsx'
|
||||
import ScrollBox from '../../common/ScrollBox.jsx'
|
||||
|
||||
const log = loglevel.getLogger('PartStockInfo')
|
||||
@ -97,6 +98,11 @@ const PartStockInfo = () => {
|
||||
visibleState={collapseState}
|
||||
updateVisibleState={updateCollapseState}
|
||||
/>
|
||||
<UserNotifierToggle
|
||||
type='partStock'
|
||||
objectData={objectFormState.objectData}
|
||||
disabled={objectFormState.loading}
|
||||
/>
|
||||
<DocumentPrintButton
|
||||
type='partStock'
|
||||
objectData={objectFormState.objectData}
|
||||
|
||||
@ -20,6 +20,7 @@ import ObjectActions from '../../common/ObjectActions.jsx'
|
||||
import ObjectTable from '../../common/ObjectTable.jsx'
|
||||
import InfoCollapsePlaceholder from '../../common/InfoCollapsePlaceholder.jsx'
|
||||
import DocumentPrintButton from '../../common/DocumentPrintButton.jsx'
|
||||
import UserNotifierToggle from '../../common/UserNotifierToggle.jsx'
|
||||
import ScrollBox from '../../common/ScrollBox.jsx'
|
||||
import OrderItemsIcon from '../../../Icons/OrderItemIcon.jsx'
|
||||
import NewOrderItem from '../OrderItems/NewOrderItem.jsx'
|
||||
@ -159,6 +160,11 @@ const PurchaseOrderInfo = () => {
|
||||
visibleState={collapseState}
|
||||
updateVisibleState={updateCollapseState}
|
||||
/>
|
||||
<UserNotifierToggle
|
||||
type='purchaseOrder'
|
||||
objectData={objectFormState.objectData}
|
||||
disabled={objectFormState.loading}
|
||||
/>
|
||||
<DocumentPrintButton
|
||||
type='purchaseOrder'
|
||||
objectData={objectFormState.objectData}
|
||||
|
||||
@ -20,6 +20,7 @@ import ObjectActions from '../../common/ObjectActions.jsx'
|
||||
import ObjectTable from '../../common/ObjectTable.jsx'
|
||||
import InfoCollapsePlaceholder from '../../common/InfoCollapsePlaceholder.jsx'
|
||||
import DocumentPrintButton from '../../common/DocumentPrintButton.jsx'
|
||||
import UserNotifierToggle from '../../common/UserNotifierToggle.jsx'
|
||||
import ScrollBox from '../../common/ScrollBox.jsx'
|
||||
import OrderItemIcon from '../../../Icons/OrderItemIcon.jsx'
|
||||
import ShipShipment from './ShipShipment.jsx'
|
||||
@ -119,6 +120,11 @@ const ShipmentInfo = () => {
|
||||
visibleState={collapseState}
|
||||
updateVisibleState={updateCollapseState}
|
||||
/>
|
||||
<UserNotifierToggle
|
||||
type='shipment'
|
||||
objectData={objectFormState.objectData}
|
||||
disabled={objectFormState.loading}
|
||||
/>
|
||||
<DocumentPrintButton
|
||||
type='shipment'
|
||||
objectData={objectFormState.objectData}
|
||||
|
||||
@ -20,6 +20,7 @@ import ObjectActions from '../../common/ObjectActions.jsx'
|
||||
import ObjectTable from '../../common/ObjectTable.jsx'
|
||||
import InfoCollapsePlaceholder from '../../common/InfoCollapsePlaceholder.jsx'
|
||||
import DocumentPrintButton from '../../common/DocumentPrintButton.jsx'
|
||||
import UserNotifierToggle from '../../common/UserNotifierToggle.jsx'
|
||||
import ScrollBox from '../../common/ScrollBox.jsx'
|
||||
|
||||
const log = loglevel.getLogger('StockAuditInfo')
|
||||
@ -97,6 +98,11 @@ const StockAuditInfo = () => {
|
||||
visibleState={collapseState}
|
||||
updateVisibleState={updateCollapseState}
|
||||
/>
|
||||
<UserNotifierToggle
|
||||
type='stockAudit'
|
||||
objectData={objectFormState.objectData}
|
||||
disabled={objectFormState.loading}
|
||||
/>
|
||||
<DocumentPrintButton
|
||||
type='stockAudit'
|
||||
objectData={objectFormState.objectData}
|
||||
|
||||
@ -19,6 +19,7 @@ import ObjectActions from '../../common/ObjectActions.jsx'
|
||||
import ObjectTable from '../../common/ObjectTable.jsx'
|
||||
import InfoCollapsePlaceholder from '../../common/InfoCollapsePlaceholder.jsx'
|
||||
import DocumentPrintButton from '../../common/DocumentPrintButton.jsx'
|
||||
import UserNotifierToggle from '../../common/UserNotifierToggle.jsx'
|
||||
import ScrollBox from '../../common/ScrollBox.jsx'
|
||||
|
||||
const log = loglevel.getLogger('CourierServiceInfo')
|
||||
@ -97,6 +98,11 @@ const CourierServiceInfo = () => {
|
||||
visibleState={collapseState}
|
||||
updateVisibleState={updateCollapseState}
|
||||
/>
|
||||
<UserNotifierToggle
|
||||
type='courierService'
|
||||
objectData={objectFormState.objectData}
|
||||
disabled={objectFormState.loading}
|
||||
/>
|
||||
<DocumentPrintButton
|
||||
type='courierService'
|
||||
objectData={objectFormState.objectData}
|
||||
|
||||
@ -19,6 +19,7 @@ import ObjectActions from '../../common/ObjectActions.jsx'
|
||||
import ObjectTable from '../../common/ObjectTable.jsx'
|
||||
import InfoCollapsePlaceholder from '../../common/InfoCollapsePlaceholder.jsx'
|
||||
import DocumentPrintButton from '../../common/DocumentPrintButton.jsx'
|
||||
import UserNotifierToggle from '../../common/UserNotifierToggle.jsx'
|
||||
import ScrollBox from '../../common/ScrollBox.jsx'
|
||||
import CourierServiceIcon from '../../../Icons/CourierServiceIcon.jsx'
|
||||
|
||||
@ -93,6 +94,11 @@ const CourierInfo = () => {
|
||||
visibleState={collapseState}
|
||||
updateVisibleState={updateCollapseState}
|
||||
/>
|
||||
<UserNotifierToggle
|
||||
type='courier'
|
||||
objectData={objectFormState.objectData}
|
||||
disabled={objectFormState.loading}
|
||||
/>
|
||||
<DocumentPrintButton
|
||||
type='courier'
|
||||
objectData={objectFormState.objectData}
|
||||
|
||||
@ -20,6 +20,7 @@ import ObjectActions from '../../common/ObjectActions.jsx'
|
||||
import ObjectTable from '../../common/ObjectTable.jsx'
|
||||
import InfoCollapsePlaceholder from '../../common/InfoCollapsePlaceholder.jsx'
|
||||
import DocumentPrintButton from '../../common/DocumentPrintButton.jsx'
|
||||
import UserNotifierToggle from '../../common/UserNotifierToggle.jsx'
|
||||
import ScrollBox from '../../common/ScrollBox.jsx'
|
||||
|
||||
const log = loglevel.getLogger('DocumentJobInfo')
|
||||
@ -94,6 +95,11 @@ const DocumentJobInfo = () => {
|
||||
visibleState={collapseState}
|
||||
updateVisibleState={updateCollapseState}
|
||||
/>
|
||||
<UserNotifierToggle
|
||||
type='documentJob'
|
||||
objectData={objectFormState.objectData}
|
||||
disabled={objectFormState.loading}
|
||||
/>
|
||||
<DocumentPrintButton
|
||||
type='documentJob'
|
||||
objectData={objectFormState.objectData}
|
||||
|
||||
@ -20,6 +20,7 @@ import ObjectActions from '../../common/ObjectActions.jsx'
|
||||
import ObjectTable from '../../common/ObjectTable.jsx'
|
||||
import InfoCollapsePlaceholder from '../../common/InfoCollapsePlaceholder.jsx'
|
||||
import DocumentPrintButton from '../../common/DocumentPrintButton.jsx'
|
||||
import UserNotifierToggle from '../../common/UserNotifierToggle.jsx'
|
||||
import ScrollBox from '../../common/ScrollBox.jsx'
|
||||
|
||||
const log = loglevel.getLogger('DocumentPrinterInfo')
|
||||
@ -102,6 +103,11 @@ const DocumentPrinterInfo = () => {
|
||||
visibleState={collapseState}
|
||||
updateVisibleState={updateCollapseState}
|
||||
/>
|
||||
<UserNotifierToggle
|
||||
type='documentPrinter'
|
||||
objectData={objectFormState.objectData}
|
||||
disabled={objectFormState.loading}
|
||||
/>
|
||||
<DocumentPrintButton
|
||||
type='documentPrinter'
|
||||
objectData={objectFormState.objectData}
|
||||
|
||||
@ -20,6 +20,7 @@ import ObjectActions from '../../common/ObjectActions.jsx'
|
||||
import ObjectTable from '../../common/ObjectTable.jsx'
|
||||
import InfoCollapsePlaceholder from '../../common/InfoCollapsePlaceholder.jsx'
|
||||
import DocumentPrintButton from '../../common/DocumentPrintButton.jsx'
|
||||
import UserNotifierToggle from '../../common/UserNotifierToggle.jsx'
|
||||
import ScrollBox from '../../common/ScrollBox.jsx'
|
||||
|
||||
const log = loglevel.getLogger('DocumentSizeInfo')
|
||||
@ -94,6 +95,11 @@ const DocumentSizeInfo = () => {
|
||||
visibleState={collapseState}
|
||||
updateVisibleState={updateCollapseState}
|
||||
/>
|
||||
<UserNotifierToggle
|
||||
type='documentSize'
|
||||
objectData={objectFormState.objectData}
|
||||
disabled={objectFormState.loading}
|
||||
/>
|
||||
<DocumentPrintButton
|
||||
type='documentSize'
|
||||
objectData={objectFormState.objectData}
|
||||
|
||||
@ -20,6 +20,7 @@ import ObjectActions from '../../common/ObjectActions.jsx'
|
||||
import ObjectTable from '../../common/ObjectTable.jsx'
|
||||
import InfoCollapsePlaceholder from '../../common/InfoCollapsePlaceholder.jsx'
|
||||
import DocumentPrintButton from '../../common/DocumentPrintButton.jsx'
|
||||
import UserNotifierToggle from '../../common/UserNotifierToggle.jsx'
|
||||
import ScrollBox from '../../common/ScrollBox.jsx'
|
||||
|
||||
const log = loglevel.getLogger('DocumentTemplateInfo')
|
||||
@ -99,6 +100,11 @@ const DocumentTemplateInfo = () => {
|
||||
visibleState={collapseState}
|
||||
updateVisibleState={updateCollapseState}
|
||||
/>
|
||||
<UserNotifierToggle
|
||||
type='documentTemplate'
|
||||
objectData={objectFormState.objectData}
|
||||
disabled={objectFormState.loading}
|
||||
/>
|
||||
<DocumentPrintButton
|
||||
type='documentTemplate'
|
||||
objectData={objectFormState.objectData}
|
||||
|
||||
@ -22,6 +22,7 @@ import InfoCollapsePlaceholder from '../../common/InfoCollapsePlaceholder.jsx'
|
||||
import FilamentIcon from '../../../Icons/FilamentIcon.jsx'
|
||||
import DocumentPrintButton from '../../common/DocumentPrintButton.jsx'
|
||||
import ScrollBox from '../../common/ScrollBox.jsx'
|
||||
import UserNotifierToggle from '../../common/UserNotifierToggle.jsx'
|
||||
|
||||
const log = loglevel.getLogger('FilamentInfo')
|
||||
log.setLevel(config.logLevel)
|
||||
@ -99,6 +100,11 @@ const FilamentInfo = () => {
|
||||
visibleState={collapseState}
|
||||
updateVisibleState={updateCollapseState}
|
||||
/>
|
||||
<UserNotifierToggle
|
||||
type='filament'
|
||||
objectData={objectFormState.objectData}
|
||||
disabled={objectFormState.loading}
|
||||
/>
|
||||
<DocumentPrintButton
|
||||
type='filament'
|
||||
objectData={objectFormState.objectData}
|
||||
|
||||
@ -19,6 +19,7 @@ import ObjectActions from '../../common/ObjectActions.jsx'
|
||||
import ObjectTable from '../../common/ObjectTable.jsx'
|
||||
import InfoCollapsePlaceholder from '../../common/InfoCollapsePlaceholder.jsx'
|
||||
import DocumentPrintButton from '../../common/DocumentPrintButton.jsx'
|
||||
import UserNotifierToggle from '../../common/UserNotifierToggle.jsx'
|
||||
import FileIcon from '../../../Icons/FileIcon.jsx'
|
||||
import FilePreview from '../../common/FilePreview.jsx'
|
||||
import MissingPlaceholder from '../../common/MissingPlaceholder.jsx'
|
||||
@ -104,6 +105,11 @@ const FileInfo = () => {
|
||||
visibleState={collapseState}
|
||||
updateVisibleState={updateCollapseState}
|
||||
/>
|
||||
<UserNotifierToggle
|
||||
type='file'
|
||||
objectData={objectFormState.objectData}
|
||||
disabled={objectFormState.loading}
|
||||
/>
|
||||
<DocumentPrintButton
|
||||
type='file'
|
||||
objectData={objectFormState.objectData}
|
||||
|
||||
@ -21,6 +21,7 @@ import ObjectTable from '../../common/ObjectTable.jsx'
|
||||
import InfoCollapsePlaceholder from '../../common/InfoCollapsePlaceholder.jsx'
|
||||
import HostOTP from './HostOtp.jsx'
|
||||
import DocumentPrintButton from '../../common/DocumentPrintButton.jsx'
|
||||
import UserNotifierToggle from '../../common/UserNotifierToggle.jsx'
|
||||
import PrinterIcon from '../../../Icons/PrinterIcon.jsx'
|
||||
import DocumentPrinterIcon from '../../../Icons/DocumentPrinterIcon.jsx'
|
||||
import ScrollBox from '../../common/ScrollBox.jsx'
|
||||
@ -105,6 +106,11 @@ const HostInfo = () => {
|
||||
visibleState={collapseState}
|
||||
updateVisibleState={updateCollapseState}
|
||||
/>
|
||||
<UserNotifierToggle
|
||||
type='host'
|
||||
objectData={objectFormState.objectData}
|
||||
disabled={objectFormState.loading}
|
||||
/>
|
||||
<DocumentPrintButton
|
||||
type='host'
|
||||
objectData={objectFormState.objectData}
|
||||
|
||||
@ -16,6 +16,7 @@ import ObjectActions from '../../common/ObjectActions.jsx'
|
||||
import ObjectTable from '../../common/ObjectTable.jsx'
|
||||
import InfoCollapsePlaceholder from '../../common/InfoCollapsePlaceholder.jsx'
|
||||
import DocumentPrintButton from '../../common/DocumentPrintButton.jsx'
|
||||
import UserNotifierToggle from '../../common/UserNotifierToggle.jsx'
|
||||
import ScrollBox from '../../common/ScrollBox.jsx'
|
||||
|
||||
const NoteTypeInfo = () => {
|
||||
@ -83,6 +84,11 @@ const NoteTypeInfo = () => {
|
||||
visibleState={collapseState}
|
||||
updateVisibleState={updateCollapseState}
|
||||
/>
|
||||
<UserNotifierToggle
|
||||
type='noteType'
|
||||
objectData={objectFormState.objectData}
|
||||
disabled={objectFormState.loading}
|
||||
/>
|
||||
<DocumentPrintButton
|
||||
type='noteType'
|
||||
objectData={objectFormState.objectData}
|
||||
|
||||
@ -19,6 +19,7 @@ import ObjectActions from '../../common/ObjectActions.jsx'
|
||||
import ObjectTable from '../../common/ObjectTable.jsx'
|
||||
import InfoCollapsePlaceholder from '../../common/InfoCollapsePlaceholder.jsx'
|
||||
import DocumentPrintButton from '../../common/DocumentPrintButton.jsx'
|
||||
import UserNotifierToggle from '../../common/UserNotifierToggle.jsx'
|
||||
import ScrollBox from '../../common/ScrollBox.jsx'
|
||||
|
||||
const log = loglevel.getLogger('NoteInfo')
|
||||
@ -92,6 +93,11 @@ const NoteInfo = () => {
|
||||
visibleState={collapseState}
|
||||
updateVisibleState={updateCollapseState}
|
||||
/>
|
||||
<UserNotifierToggle
|
||||
type='note'
|
||||
objectData={objectFormState.objectData}
|
||||
disabled={objectFormState.loading}
|
||||
/>
|
||||
<DocumentPrintButton
|
||||
type='note'
|
||||
objectData={objectFormState.objectData}
|
||||
|
||||
@ -17,6 +17,7 @@ import ObjectActions from '../../common/ObjectActions.jsx'
|
||||
import ObjectTable from '../../common/ObjectTable.jsx'
|
||||
import InfoCollapsePlaceholder from '../../common/InfoCollapsePlaceholder.jsx'
|
||||
import DocumentPrintButton from '../../common/DocumentPrintButton.jsx'
|
||||
import UserNotifierToggle from '../../common/UserNotifierToggle.jsx'
|
||||
import ScrollBox from '../../common/ScrollBox.jsx'
|
||||
|
||||
const PartInfo = () => {
|
||||
@ -35,7 +36,8 @@ const PartInfo = () => {
|
||||
editLoading: false,
|
||||
formValid: false,
|
||||
lock: null,
|
||||
loading: false
|
||||
loading: false,
|
||||
objectData: {}
|
||||
})
|
||||
|
||||
const actions = {
|
||||
@ -83,6 +85,11 @@ const PartInfo = () => {
|
||||
visibleState={collapseState}
|
||||
updateVisibleState={updateCollapseState}
|
||||
/>
|
||||
<UserNotifierToggle
|
||||
type='part'
|
||||
objectData={objectFormState.objectData}
|
||||
disabled={objectFormState.loading}
|
||||
/>
|
||||
<DocumentPrintButton
|
||||
type='part'
|
||||
objectData={objectFormState.objectData}
|
||||
|
||||
@ -17,6 +17,7 @@ import ActionHandler from '../../common/ActionHandler.jsx'
|
||||
import ObjectActions from '../../common/ObjectActions.jsx'
|
||||
import InfoCollapsePlaceholder from '../../common/InfoCollapsePlaceholder.jsx'
|
||||
import DocumentPrintButton from '../../common/DocumentPrintButton.jsx'
|
||||
import UserNotifierToggle from '../../common/UserNotifierToggle.jsx'
|
||||
import ScrollBox from '../../common/ScrollBox.jsx'
|
||||
import ObjectProperty from '../../common/ObjectProperty.jsx'
|
||||
import { getModelProperty } from '../../../../database/ObjectModels.js'
|
||||
@ -38,7 +39,8 @@ const ProductInfo = () => {
|
||||
editLoading: false,
|
||||
formValid: false,
|
||||
lock: null,
|
||||
loading: false
|
||||
loading: false,
|
||||
objectData: {}
|
||||
})
|
||||
|
||||
const actions = {
|
||||
@ -87,6 +89,11 @@ const ProductInfo = () => {
|
||||
visibleState={collapseState}
|
||||
updateVisibleState={updateCollapseState}
|
||||
/>
|
||||
<UserNotifierToggle
|
||||
type='product'
|
||||
objectData={objectFormState.objectData}
|
||||
disabled={objectFormState.loading}
|
||||
/>
|
||||
<DocumentPrintButton
|
||||
type='product'
|
||||
objectData={objectFormState.objectData}
|
||||
|
||||
@ -19,6 +19,7 @@ import ObjectActions from '../../common/ObjectActions.jsx'
|
||||
import ObjectTable from '../../common/ObjectTable.jsx'
|
||||
import InfoCollapsePlaceholder from '../../common/InfoCollapsePlaceholder.jsx'
|
||||
import DocumentPrintButton from '../../common/DocumentPrintButton.jsx'
|
||||
import UserNotifierToggle from '../../common/UserNotifierToggle.jsx'
|
||||
import ScrollBox from '../../common/ScrollBox.jsx'
|
||||
|
||||
const log = loglevel.getLogger('TaxRateInfo')
|
||||
@ -92,6 +93,11 @@ const TaxRateInfo = () => {
|
||||
visibleState={collapseState}
|
||||
updateVisibleState={updateCollapseState}
|
||||
/>
|
||||
<UserNotifierToggle
|
||||
type='taxRate'
|
||||
objectData={objectFormState.objectData}
|
||||
disabled={objectFormState.loading}
|
||||
/>
|
||||
<DocumentPrintButton
|
||||
type='taxRate'
|
||||
objectData={objectFormState.objectData}
|
||||
|
||||
@ -19,6 +19,7 @@ import ObjectActions from '../../common/ObjectActions.jsx'
|
||||
import ObjectTable from '../../common/ObjectTable.jsx'
|
||||
import InfoCollapsePlaceholder from '../../common/InfoCollapsePlaceholder.jsx'
|
||||
import DocumentPrintButton from '../../common/DocumentPrintButton.jsx'
|
||||
import UserNotifierToggle from '../../common/UserNotifierToggle.jsx'
|
||||
import ScrollBox from '../../common/ScrollBox.jsx'
|
||||
|
||||
const log = loglevel.getLogger('TaxRecordInfo')
|
||||
@ -95,6 +96,11 @@ const TaxRecordInfo = () => {
|
||||
visibleState={collapseState}
|
||||
updateVisibleState={updateCollapseState}
|
||||
/>
|
||||
<UserNotifierToggle
|
||||
type='taxRecord'
|
||||
objectData={objectFormState.objectData}
|
||||
disabled={objectFormState.loading}
|
||||
/>
|
||||
<DocumentPrintButton
|
||||
type='taxRecord'
|
||||
objectData={objectFormState.objectData}
|
||||
|
||||
@ -18,6 +18,7 @@ import ObjectActions from '../../common/ObjectActions.jsx'
|
||||
import ObjectTable from '../../common/ObjectTable.jsx'
|
||||
import InfoCollapsePlaceholder from '../../common/InfoCollapsePlaceholder.jsx'
|
||||
import DocumentPrintButton from '../../common/DocumentPrintButton.jsx'
|
||||
import UserNotifierToggle from '../../common/UserNotifierToggle.jsx'
|
||||
import ScrollBox from '../../common/ScrollBox.jsx'
|
||||
|
||||
const UserInfo = () => {
|
||||
@ -84,6 +85,11 @@ const UserInfo = () => {
|
||||
visibleState={collapseState}
|
||||
updateVisibleState={updateCollapseState}
|
||||
/>
|
||||
<UserNotifierToggle
|
||||
type='user'
|
||||
objectData={objectFormState.objectData}
|
||||
disabled={objectFormState.loading}
|
||||
/>
|
||||
<DocumentPrintButton
|
||||
type='user'
|
||||
objectData={objectFormState.objectData}
|
||||
|
||||
@ -19,6 +19,7 @@ import ObjectActions from '../../common/ObjectActions.jsx'
|
||||
import ObjectTable from '../../common/ObjectTable.jsx'
|
||||
import InfoCollapsePlaceholder from '../../common/InfoCollapsePlaceholder.jsx'
|
||||
import DocumentPrintButton from '../../common/DocumentPrintButton.jsx'
|
||||
import UserNotifierToggle from '../../common/UserNotifierToggle.jsx'
|
||||
import ScrollBox from '../../common/ScrollBox.jsx'
|
||||
|
||||
const log = loglevel.getLogger('VendorInfo')
|
||||
@ -92,6 +93,11 @@ const VendorInfo = () => {
|
||||
visibleState={collapseState}
|
||||
updateVisibleState={updateCollapseState}
|
||||
/>
|
||||
<UserNotifierToggle
|
||||
type='vendor'
|
||||
objectData={objectFormState.objectData}
|
||||
disabled={objectFormState.loading}
|
||||
/>
|
||||
<DocumentPrintButton
|
||||
type='vendor'
|
||||
objectData={objectFormState.objectData}
|
||||
|
||||
@ -22,6 +22,7 @@ import ObjectTable from '../../common/ObjectTable.jsx'
|
||||
import InfoCollapsePlaceholder from '../../common/InfoCollapsePlaceholder.jsx'
|
||||
import EyeIcon from '../../../Icons/EyeIcon.jsx'
|
||||
import DocumentPrintButton from '../../common/DocumentPrintButton.jsx'
|
||||
import UserNotifierToggle from '../../common/UserNotifierToggle.jsx'
|
||||
import MissingPlaceholder from '../../common/MissingPlaceholder.jsx'
|
||||
import FilePreview from '../../common/FilePreview.jsx'
|
||||
import ScrollBox from '../../common/ScrollBox.jsx'
|
||||
@ -106,6 +107,11 @@ const GCodeFileInfo = () => {
|
||||
visibleState={collapseState}
|
||||
updateVisibleState={updateCollapseState}
|
||||
/>
|
||||
<UserNotifierToggle
|
||||
type='gcodeFile'
|
||||
objectData={objectFormState.objectData}
|
||||
disabled={objectFormState.loading}
|
||||
/>
|
||||
<DocumentPrintButton
|
||||
type='gcodeFile'
|
||||
objectData={objectFormState.objectData}
|
||||
|
||||
@ -21,6 +21,7 @@ import ObjectTable from '../../common/ObjectTable.jsx'
|
||||
import InfoCollapsePlaceholder from '../../common/InfoCollapsePlaceholder.jsx'
|
||||
import JobIcon from '../../../Icons/JobIcon.jsx'
|
||||
import DocumentPrintButton from '../../common/DocumentPrintButton.jsx'
|
||||
import UserNotifierToggle from '../../common/UserNotifierToggle.jsx'
|
||||
import DeployJob from './DeployJob.jsx'
|
||||
import ScrollBox from '../../common/ScrollBox.jsx'
|
||||
|
||||
@ -105,6 +106,11 @@ const JobInfo = () => {
|
||||
visibleState={collapseState}
|
||||
updateVisibleState={updateCollapseState}
|
||||
/>
|
||||
<UserNotifierToggle
|
||||
type='job'
|
||||
objectData={objectFormState.objectData}
|
||||
disabled={objectFormState.loading}
|
||||
/>
|
||||
<DocumentPrintButton
|
||||
type='job'
|
||||
objectData={objectFormState.objectData}
|
||||
|
||||
@ -20,6 +20,7 @@ import ObjectActions from '../../common/ObjectActions.jsx'
|
||||
import ObjectTable from '../../common/ObjectTable.jsx'
|
||||
import InfoCollapsePlaceholder from '../../common/InfoCollapsePlaceholder.jsx'
|
||||
import DocumentPrintButton from '../../common/DocumentPrintButton.jsx'
|
||||
import UserNotifierToggle from '../../common/UserNotifierToggle.jsx'
|
||||
import ScrollBox from '../../common/ScrollBox.jsx'
|
||||
|
||||
const log = loglevel.getLogger('PrinterInfo')
|
||||
@ -94,6 +95,11 @@ const PrinterInfo = () => {
|
||||
visibleState={collapseState}
|
||||
updateVisibleState={updateCollapseState}
|
||||
/>
|
||||
<UserNotifierToggle
|
||||
type='printer'
|
||||
objectData={objectFormState.objectData}
|
||||
disabled={objectFormState.loading}
|
||||
/>
|
||||
<DocumentPrintButton
|
||||
type='printer'
|
||||
objectData={objectFormState.objectData}
|
||||
|
||||
@ -18,6 +18,7 @@ import ObjectActions from '../../common/ObjectActions.jsx'
|
||||
import ObjectTable from '../../common/ObjectTable.jsx'
|
||||
import InfoCollapsePlaceholder from '../../common/InfoCollapsePlaceholder.jsx'
|
||||
import DocumentPrintButton from '../../common/DocumentPrintButton.jsx'
|
||||
import UserNotifierToggle from '../../common/UserNotifierToggle.jsx'
|
||||
import ScrollBox from '../../common/ScrollBox.jsx'
|
||||
import StockEventIcon from '../../../Icons/StockEventIcon.jsx'
|
||||
|
||||
@ -85,6 +86,11 @@ const SubJobInfo = () => {
|
||||
visibleState={collapseState}
|
||||
updateVisibleState={updateCollapseState}
|
||||
/>
|
||||
<UserNotifierToggle
|
||||
type='subJob'
|
||||
objectData={objectFormState.objectData}
|
||||
disabled={objectFormState.loading}
|
||||
/>
|
||||
<DocumentPrintButton
|
||||
type='subJob'
|
||||
objectData={objectFormState.objectData}
|
||||
|
||||
@ -19,6 +19,7 @@ import ObjectActions from '../../common/ObjectActions.jsx'
|
||||
import ObjectTable from '../../common/ObjectTable.jsx'
|
||||
import InfoCollapsePlaceholder from '../../common/InfoCollapsePlaceholder.jsx'
|
||||
import DocumentPrintButton from '../../common/DocumentPrintButton.jsx'
|
||||
import UserNotifierToggle from '../../common/UserNotifierToggle.jsx'
|
||||
import ScrollBox from '../../common/ScrollBox.jsx'
|
||||
|
||||
const log = loglevel.getLogger('ClientInfo')
|
||||
@ -92,6 +93,11 @@ const ClientInfo = () => {
|
||||
visibleState={collapseState}
|
||||
updateVisibleState={updateCollapseState}
|
||||
/>
|
||||
<UserNotifierToggle
|
||||
type='client'
|
||||
objectData={objectFormState.objectData}
|
||||
disabled={objectFormState.loading}
|
||||
/>
|
||||
<DocumentPrintButton
|
||||
type='client'
|
||||
objectData={objectFormState.objectData}
|
||||
|
||||
@ -20,6 +20,7 @@ import ObjectActions from '../../common/ObjectActions.jsx'
|
||||
import ObjectTable from '../../common/ObjectTable.jsx'
|
||||
import InfoCollapsePlaceholder from '../../common/InfoCollapsePlaceholder.jsx'
|
||||
import DocumentPrintButton from '../../common/DocumentPrintButton.jsx'
|
||||
import UserNotifierToggle from '../../common/UserNotifierToggle.jsx'
|
||||
import ScrollBox from '../../common/ScrollBox.jsx'
|
||||
import OrderItemsIcon from '../../../Icons/OrderItemIcon.jsx'
|
||||
import NewOrderItem from '../../Inventory/OrderItems/NewOrderItem.jsx'
|
||||
@ -156,6 +157,11 @@ const SalesOrderInfo = () => {
|
||||
visibleState={collapseState}
|
||||
updateVisibleState={updateCollapseState}
|
||||
/>
|
||||
<UserNotifierToggle
|
||||
type='salesOrder'
|
||||
objectData={objectFormState.objectData}
|
||||
disabled={objectFormState.loading}
|
||||
/>
|
||||
<DocumentPrintButton
|
||||
type='salesOrder'
|
||||
objectData={objectFormState.objectData}
|
||||
|
||||
@ -20,6 +20,7 @@ import {
|
||||
import { AuthContext } from '../context/AuthContext'
|
||||
import { SpotlightContext } from '../context/SpotlightContext'
|
||||
import { ApiServerContext } from '../context/ApiServerContext'
|
||||
import { NotificationContext } from '../context/NotificationContext'
|
||||
import { useNavigate, useLocation } from 'react-router-dom'
|
||||
import { Header } from 'antd/es/layout/layout'
|
||||
import { useMediaQuery } from 'react-responsive'
|
||||
@ -46,8 +47,9 @@ const { Text } = Typography
|
||||
const DashboardNavigation = () => {
|
||||
const { logout, userProfile } = useContext(AuthContext)
|
||||
const { showSpotlight } = useContext(SpotlightContext)
|
||||
const { toggleNotificationCenter, unreadCount } = useContext(ApiServerContext)
|
||||
const { connecting, connected } = useContext(ApiServerContext)
|
||||
const { toggleNotificationCenter, unreadCount } =
|
||||
useContext(NotificationContext)
|
||||
const [apiServerState, setApiServerState] = useState('disconnected')
|
||||
const navigate = useNavigate()
|
||||
const location = useLocation()
|
||||
@ -257,7 +259,7 @@ const DashboardNavigation = () => {
|
||||
onClick={() => showSpotlight()}
|
||||
/>
|
||||
</KeyboardShortcut>
|
||||
<Badge count={unreadCount} size='small'>
|
||||
<Badge count={unreadCount} size='small' offset={[-4, 5]}>
|
||||
<KeyboardShortcut
|
||||
shortcut='alt+n'
|
||||
hint='ALT N'
|
||||
|
||||
@ -1,5 +1,6 @@
|
||||
import { useState } from 'react'
|
||||
import PropTypes from 'prop-types'
|
||||
import { Typography, Space, Tag, Badge, Flex, Card } from 'antd'
|
||||
import { Typography, Button, Tag, Badge, Flex, Card, Divider } from 'antd'
|
||||
import {
|
||||
BellOutlined,
|
||||
InfoCircleOutlined,
|
||||
@ -7,37 +8,131 @@ import {
|
||||
CheckCircleOutlined
|
||||
} from '@ant-design/icons'
|
||||
import TimeDisplay from './TimeDisplay'
|
||||
import EditIcon from '../../Icons/EditIcon'
|
||||
import PropertyChanges from './PropertyChanges'
|
||||
import XMarkIcon from '../../Icons/XMarkIcon'
|
||||
import ObjectDisplay from './ObjectDisplay'
|
||||
import BinIcon from '../../Icons/BinIcon'
|
||||
|
||||
const { Text, Paragraph } = Typography
|
||||
|
||||
const Notification = ({ notification, onMarkAsRead }) => {
|
||||
const Notification = ({
|
||||
notification,
|
||||
onMarkAsRead,
|
||||
onDelete,
|
||||
showCard = true,
|
||||
showDelete = true,
|
||||
showExtraInfo = true,
|
||||
inlineIcon = false
|
||||
}) => {
|
||||
const [deleting, setDeleting] = useState(false)
|
||||
|
||||
const getNotificationIcon = (type) => {
|
||||
switch (type) {
|
||||
case 'info':
|
||||
return <InfoCircleOutlined style={{ color: '#1890ff' }} />
|
||||
case 'warning':
|
||||
return <ExclamationCircleOutlined style={{ color: '#faad14' }} />
|
||||
return <InfoCircleOutlined />
|
||||
case 'editObject':
|
||||
return <EditIcon />
|
||||
case 'deleteObject':
|
||||
return <BinIcon />
|
||||
case 'error':
|
||||
return <ExclamationCircleOutlined style={{ color: '#ff4d4f' }} />
|
||||
return <ExclamationCircleOutlined />
|
||||
case 'success':
|
||||
return <CheckCircleOutlined style={{ color: '#52c41a' }} />
|
||||
return <CheckCircleOutlined />
|
||||
default:
|
||||
return <BellOutlined style={{ color: '#8c8c8c' }} />
|
||||
return <BellOutlined />
|
||||
}
|
||||
}
|
||||
|
||||
const getNotificationColor = (type) => {
|
||||
const getNotificationTag = (type) => {
|
||||
const icon = getNotificationIcon(type)
|
||||
switch (type) {
|
||||
case 'info':
|
||||
return 'blue'
|
||||
case 'warning':
|
||||
return 'orange'
|
||||
return (
|
||||
<Tag color='blue' icon={icon} style={{ margin: 0 }}>
|
||||
Info
|
||||
</Tag>
|
||||
)
|
||||
case 'editObject':
|
||||
return (
|
||||
<Tag color='blue' icon={icon} style={{ margin: 0 }}>
|
||||
Edit
|
||||
</Tag>
|
||||
)
|
||||
case 'deleteObject':
|
||||
return (
|
||||
<Tag color='error' icon={icon} style={{ margin: 0 }}>
|
||||
Delete
|
||||
</Tag>
|
||||
)
|
||||
case 'error':
|
||||
return 'red'
|
||||
return (
|
||||
<Tag color='red' icon={icon} style={{ margin: 0 }}>
|
||||
Error
|
||||
</Tag>
|
||||
)
|
||||
case 'success':
|
||||
return 'green'
|
||||
return (
|
||||
<Tag color='green' icon={icon} style={{ margin: 0 }}>
|
||||
Success
|
||||
</Tag>
|
||||
)
|
||||
default:
|
||||
return 'default'
|
||||
return (
|
||||
<Tag color='default' icon={icon} style={{ margin: 0 }}>
|
||||
Default
|
||||
</Tag>
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
const getMetadataDisplay = (metadata, type) => {
|
||||
if (metadata.old && metadata.new && type === 'editObject') {
|
||||
return (
|
||||
<PropertyChanges
|
||||
type={metadata?.objectType ?? 'unknown'}
|
||||
value={{
|
||||
old: metadata.old,
|
||||
new: metadata.new
|
||||
}}
|
||||
/>
|
||||
)
|
||||
}
|
||||
return null
|
||||
}
|
||||
|
||||
const getNotificationMessage = (metadata, type) => {
|
||||
const paragraph = {
|
||||
ellipsis: {
|
||||
rows: 2,
|
||||
expandable: true,
|
||||
symbol: 'Show more'
|
||||
},
|
||||
style: { margin: 0 }
|
||||
}
|
||||
switch (type) {
|
||||
case 'editObject': {
|
||||
return (
|
||||
<Paragraph {...paragraph}>
|
||||
Object:
|
||||
<ObjectDisplay
|
||||
object={metadata.object}
|
||||
objectType={metadata.objectType}
|
||||
showHyperlink={true}
|
||||
showSpotlight={true}
|
||||
/>
|
||||
User:
|
||||
<ObjectDisplay
|
||||
object={metadata.user}
|
||||
objectType='user'
|
||||
showHyperlink={true}
|
||||
showSpotlight={true}
|
||||
/>
|
||||
</Paragraph>
|
||||
)
|
||||
}
|
||||
default:
|
||||
return <Paragraph {...paragraph}>{notification.message}</Paragraph>
|
||||
}
|
||||
}
|
||||
|
||||
@ -47,52 +142,81 @@ const Notification = ({ notification, onMarkAsRead }) => {
|
||||
}
|
||||
}
|
||||
|
||||
return (
|
||||
const handleDelete = async (e) => {
|
||||
e.stopPropagation()
|
||||
if (onDelete) {
|
||||
setDeleting(true)
|
||||
try {
|
||||
await onDelete(notification._id)
|
||||
} finally {
|
||||
setDeleting(false)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const content = (
|
||||
<Flex align='start' gap='small'>
|
||||
<Flex vertical style={{ flex: 1 }} gap='small'>
|
||||
<Flex justify='space-between' align='center'>
|
||||
<Flex align='center' gap='middle'>
|
||||
{inlineIcon && getNotificationIcon(notification.type)}
|
||||
<Badge dot={!notification.read} offset={[2, 4]}>
|
||||
<Text strong={!notification.read}>{notification.title}</Text>
|
||||
</Badge>
|
||||
</Flex>
|
||||
{showDelete && (
|
||||
<Button
|
||||
type='text'
|
||||
size='small'
|
||||
loading={deleting}
|
||||
disabled={deleting}
|
||||
style={{
|
||||
width: 20,
|
||||
height: 20,
|
||||
position: 'relative',
|
||||
left: 2
|
||||
}}
|
||||
icon={
|
||||
<XMarkIcon
|
||||
style={{ fontSize: 10, marginBottom: 5, marginLeft: 0 }}
|
||||
/>
|
||||
}
|
||||
onClick={handleDelete}
|
||||
/>
|
||||
)}
|
||||
</Flex>
|
||||
|
||||
{getNotificationMessage(notification.metadata, notification.type)}
|
||||
|
||||
{notification.metadata &&
|
||||
getMetadataDisplay(notification.metadata, notification.type)}
|
||||
{showExtraInfo && (
|
||||
<>
|
||||
<Divider style={{ margin: 0 }} />
|
||||
<Flex justify='space-between' align='center'>
|
||||
{getNotificationTag(notification.type)}
|
||||
<TimeDisplay dateTime={notification.createdAt} showSince={true} />
|
||||
</Flex>
|
||||
</>
|
||||
)}
|
||||
</Flex>
|
||||
</Flex>
|
||||
)
|
||||
|
||||
return showCard ? (
|
||||
<Card
|
||||
size='small'
|
||||
style={{
|
||||
backgroundColor: notification.read ? '#fafafa' : '#ffffff',
|
||||
cursor: 'pointer',
|
||||
transition: 'background-color 0.2s',
|
||||
border: notification.read ? '1px solid #f0f0f0' : '1px solid #d9d9d9'
|
||||
transition: 'background-color 0.2s'
|
||||
}}
|
||||
onClick={handleMarkAsRead}
|
||||
bodyStyle={{ padding: '12px' }}
|
||||
styles={{ body: { padding: '10px 12px 12px 12px' } }}
|
||||
>
|
||||
<Flex align='start' gap='small'>
|
||||
<Badge dot={!notification.read} offset={[-5, 5]}>
|
||||
{getNotificationIcon(notification.type)}
|
||||
</Badge>
|
||||
<Flex vertical style={{ flex: 1 }} gap='small'>
|
||||
<Flex justify='space-between' align='center'>
|
||||
<Space>
|
||||
<Text strong={!notification.read}>{notification.title}</Text>
|
||||
<Tag color={getNotificationColor(notification.type)} size='small'>
|
||||
{notification.type}
|
||||
</Tag>
|
||||
</Space>
|
||||
<Text type='secondary' style={{ fontSize: '12px' }}>
|
||||
<TimeDisplay dateTime={notification.createdAt} showSince={true} />
|
||||
</Text>
|
||||
</Flex>
|
||||
<Paragraph
|
||||
ellipsis={{
|
||||
rows: 2,
|
||||
expandable: true,
|
||||
symbol: 'Show more'
|
||||
}}
|
||||
style={{ margin: 0 }}
|
||||
>
|
||||
{notification.message}
|
||||
</Paragraph>
|
||||
{notification.metadata && (
|
||||
<Text type='secondary' style={{ fontSize: '12px' }}>
|
||||
{JSON.stringify(notification.metadata)}
|
||||
</Text>
|
||||
)}
|
||||
</Flex>
|
||||
</Flex>
|
||||
{content}
|
||||
</Card>
|
||||
) : (
|
||||
<div style={{ marginTop: '-1px' }}>{content}</div>
|
||||
)
|
||||
}
|
||||
|
||||
@ -107,7 +231,12 @@ Notification.propTypes = {
|
||||
createdAt: PropTypes.string.isRequired,
|
||||
metadata: PropTypes.object
|
||||
}).isRequired,
|
||||
onMarkAsRead: PropTypes.func
|
||||
onMarkAsRead: PropTypes.func,
|
||||
onDelete: PropTypes.func,
|
||||
showCard: PropTypes.bool,
|
||||
showDelete: PropTypes.bool,
|
||||
showExtraInfo: PropTypes.bool,
|
||||
inlineIcon: PropTypes.bool
|
||||
}
|
||||
|
||||
export default Notification
|
||||
|
||||
@ -1,174 +1,62 @@
|
||||
import { useState, useEffect, useContext, useCallback } from 'react'
|
||||
import {
|
||||
Typography,
|
||||
Space,
|
||||
Button,
|
||||
Empty,
|
||||
Spin,
|
||||
Popconfirm,
|
||||
Flex,
|
||||
Badge,
|
||||
Dropdown
|
||||
} from 'antd'
|
||||
import { useMessageContext } from '../context/MessageContext'
|
||||
import {
|
||||
BellOutlined,
|
||||
DeleteOutlined,
|
||||
CheckOutlined,
|
||||
ReloadOutlined
|
||||
} from '@ant-design/icons'
|
||||
import axios from 'axios'
|
||||
import { useContext, useState } from 'react'
|
||||
import { Typography, Button, Empty, Spin, Flex, Dropdown } from 'antd'
|
||||
import { LoadingOutlined } from '@ant-design/icons'
|
||||
import PropTypes from 'prop-types'
|
||||
import { AuthContext } from '../context/AuthContext'
|
||||
import config from '../../../config'
|
||||
import { NotificationContext } from '../context/NotificationContext'
|
||||
import Notification from './Notification'
|
||||
import ScrollBox from './ScrollBox'
|
||||
import CheckIcon from '../../Icons/CheckIcon'
|
||||
import BinIcon from '../../Icons/BinIcon'
|
||||
|
||||
const { Text } = Typography
|
||||
|
||||
const NotificationCenter = ({ visible }) => {
|
||||
const [notifications, setNotifications] = useState([])
|
||||
const [loading, setLoading] = useState(false)
|
||||
const [showDeleteConfirm, setShowDeleteConfirm] = useState(false)
|
||||
const { showSuccess, showError } = useMessageContext()
|
||||
const {
|
||||
notifications,
|
||||
markNotificationAsRead,
|
||||
markAllNotificationsAsRead,
|
||||
deleteNotification,
|
||||
deleteAllNotifications,
|
||||
notificationsLoading
|
||||
} = useContext(NotificationContext)
|
||||
|
||||
const { authenticated } = useContext(AuthContext)
|
||||
const unreadCount = notifications.filter((n) => !n.read).length
|
||||
|
||||
const fetchNotifications = useCallback(async () => {
|
||||
if (!authenticated) return
|
||||
const [clearNotificationsLoading, setClearNotificationsLoading] =
|
||||
useState(false)
|
||||
|
||||
setLoading(true)
|
||||
try {
|
||||
const response = await axios.get(`${config.backendUrl}/notifications`, {
|
||||
headers: {
|
||||
Accept: 'application/json'
|
||||
},
|
||||
withCredentials: true
|
||||
})
|
||||
setNotifications(response.data)
|
||||
} catch (error) {
|
||||
console.error('Error fetching notifications:', error)
|
||||
showError('Failed to fetch notifications')
|
||||
} finally {
|
||||
setLoading(false)
|
||||
}
|
||||
}, [authenticated, showError])
|
||||
const handleMarkAsRead = async (notificationId) => {
|
||||
await markNotificationAsRead(notificationId)
|
||||
}
|
||||
|
||||
const markAsRead = useCallback(
|
||||
async (notificationId) => {
|
||||
try {
|
||||
await axios.put(
|
||||
`${config.backendUrl}/notifications/${notificationId}/read`,
|
||||
{},
|
||||
{
|
||||
headers: {
|
||||
Accept: 'application/json'
|
||||
},
|
||||
withCredentials: true
|
||||
}
|
||||
)
|
||||
|
||||
// Update local state
|
||||
setNotifications((prev) =>
|
||||
prev.map((notification) => {
|
||||
if (notification._id === notificationId) {
|
||||
return { ...notification, read: true }
|
||||
}
|
||||
return notification
|
||||
})
|
||||
)
|
||||
|
||||
showSuccess('Notification marked as read')
|
||||
} catch (error) {
|
||||
console.error('Error marking notification as read:', error)
|
||||
showError('Failed to mark notification as read')
|
||||
}
|
||||
},
|
||||
[showSuccess, showError]
|
||||
)
|
||||
|
||||
const markAllAsRead = useCallback(async () => {
|
||||
try {
|
||||
await axios.put(
|
||||
`${config.backendUrl}/notifications/read-all`,
|
||||
{},
|
||||
{
|
||||
headers: {
|
||||
Accept: 'application/json'
|
||||
},
|
||||
withCredentials: true
|
||||
}
|
||||
)
|
||||
|
||||
// Update local state
|
||||
setNotifications((prev) =>
|
||||
prev.map((notification) => ({ ...notification, read: true }))
|
||||
)
|
||||
|
||||
showSuccess('All notifications marked as read')
|
||||
} catch (error) {
|
||||
console.error('Error marking all notifications as read:', error)
|
||||
showError('Failed to mark all notifications as read')
|
||||
}
|
||||
}, [showSuccess, showError])
|
||||
|
||||
const deleteAllNotifications = useCallback(async () => {
|
||||
try {
|
||||
await axios.delete(`${config.backendUrl}/notifications`, {
|
||||
headers: {
|
||||
Accept: 'application/json'
|
||||
},
|
||||
withCredentials: true
|
||||
})
|
||||
|
||||
setNotifications([])
|
||||
showSuccess('All notifications deleted')
|
||||
} catch (error) {
|
||||
console.error('Error deleting all notifications:', error)
|
||||
showError('Failed to delete all notifications')
|
||||
} finally {
|
||||
setShowDeleteConfirm(false)
|
||||
}
|
||||
}, [showSuccess, showError])
|
||||
|
||||
useEffect(() => {
|
||||
if (visible && authenticated) {
|
||||
fetchNotifications()
|
||||
}
|
||||
}, [visible, authenticated, fetchNotifications])
|
||||
|
||||
const unreadCount = notifications.filter(
|
||||
(notification) => !notification.read
|
||||
).length
|
||||
const handleMarkAllAsRead = async () => {
|
||||
await markAllNotificationsAsRead()
|
||||
}
|
||||
|
||||
const actionItems = {
|
||||
items: [
|
||||
{
|
||||
label: 'Mark All Read',
|
||||
key: 'markAllRead',
|
||||
icon: <CheckOutlined />,
|
||||
icon: <CheckIcon />,
|
||||
disabled: unreadCount === 0
|
||||
},
|
||||
{
|
||||
label: 'Reload Notifications',
|
||||
key: 'reloadNotifications',
|
||||
icon: <ReloadOutlined />
|
||||
},
|
||||
{ type: 'divider' },
|
||||
{
|
||||
label: 'Delete All',
|
||||
key: 'deleteAll',
|
||||
icon: <DeleteOutlined />,
|
||||
label: 'Clear All',
|
||||
key: 'clearAll',
|
||||
icon: <BinIcon />,
|
||||
danger: true,
|
||||
|
||||
disabled: notifications.length === 0
|
||||
}
|
||||
],
|
||||
onClick: ({ key }) => {
|
||||
if (key === 'markAllRead') {
|
||||
markAllAsRead()
|
||||
} else if (key === 'reloadNotifications') {
|
||||
fetchNotifications()
|
||||
} else if (key === 'deleteAll') {
|
||||
setShowDeleteConfirm(true)
|
||||
handleMarkAllAsRead()
|
||||
} else if (key === 'clearAll') {
|
||||
setClearNotificationsLoading(true)
|
||||
deleteAllNotifications().finally(() => setClearNotificationsLoading(false))
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -178,58 +66,45 @@ const NotificationCenter = ({ visible }) => {
|
||||
}
|
||||
|
||||
return (
|
||||
<>
|
||||
<Flex justify='space-between' align='center'>
|
||||
<Space size='middle'>
|
||||
<Dropdown menu={actionItems}>
|
||||
<Button>Actions</Button>
|
||||
</Dropdown>
|
||||
<Badge count={unreadCount} size='small'>
|
||||
<BellOutlined style={{ fontSize: '18px' }} />
|
||||
</Badge>
|
||||
</Space>
|
||||
<Space>
|
||||
<Button icon={<DeleteOutlined />} danger />
|
||||
</Space>
|
||||
<Flex vertical style={{ height: '100%' }}>
|
||||
<Flex justify='space-between' align='center' style={{ marginBottom: 16 }}>
|
||||
<Dropdown menu={actionItems}>
|
||||
<Button loading={clearNotificationsLoading}>Actions</Button>
|
||||
</Dropdown>
|
||||
</Flex>
|
||||
|
||||
<div style={{ maxHeight: 500, overflow: 'auto' }}>
|
||||
{loading ? (
|
||||
<div style={{ padding: '40px', textAlign: 'center' }}>
|
||||
<Spin size='large' />
|
||||
<div style={{ marginTop: 16 }}>
|
||||
<Text type='secondary'>Loading notifications...</Text>
|
||||
</div>
|
||||
</div>
|
||||
<ScrollBox>
|
||||
{notificationsLoading ? (
|
||||
<Flex
|
||||
justify='center'
|
||||
align='center'
|
||||
style={{ padding: '40px' }}
|
||||
gap='small'
|
||||
>
|
||||
<Spin indicator={<LoadingOutlined />} />
|
||||
<Text type='secondary'>Loading notifications...</Text>
|
||||
</Flex>
|
||||
) : notifications.length === 0 ? (
|
||||
<Empty
|
||||
image={Empty.PRESENTED_IMAGE_SIMPLE}
|
||||
description='No notifications'
|
||||
style={{ padding: '40px 20px' }}
|
||||
/>
|
||||
<Flex justify='center' align='center' style={{ padding: '40px' }}>
|
||||
<Empty
|
||||
image={Empty.PRESENTED_IMAGE_SIMPLE}
|
||||
description='No notifications'
|
||||
/>
|
||||
</Flex>
|
||||
) : (
|
||||
<Flex vertical gap='small' style={{ padding: '16px' }}>
|
||||
<Flex vertical gap='small'>
|
||||
{notifications.map((notification) => (
|
||||
<Notification
|
||||
key={notification._id}
|
||||
notification={notification}
|
||||
onMarkAsRead={markAsRead}
|
||||
onMarkAsRead={handleMarkAsRead}
|
||||
onDelete={deleteNotification}
|
||||
/>
|
||||
))}
|
||||
</Flex>
|
||||
)}
|
||||
</div>
|
||||
|
||||
<Popconfirm
|
||||
title='Delete all notifications?'
|
||||
description='This action cannot be undone.'
|
||||
open={showDeleteConfirm}
|
||||
onConfirm={deleteAllNotifications}
|
||||
onCancel={() => setShowDeleteConfirm(false)}
|
||||
okText='Yes'
|
||||
cancelText='No'
|
||||
/>
|
||||
</>
|
||||
</ScrollBox>
|
||||
</Flex>
|
||||
)
|
||||
}
|
||||
|
||||
|
||||
@ -108,7 +108,7 @@ const TimeDisplay = ({
|
||||
|
||||
return (
|
||||
<Flex align={'center'} gap={'small'}>
|
||||
<Text type={type}>{formattedDate}</Text>
|
||||
{showDate || showTime ? <Text type={type}>{formattedDate}</Text> : null}
|
||||
{showSince ? <Tag style={{ margin: 0 }}>{timeAgo}</Tag> : null}
|
||||
</Flex>
|
||||
)
|
||||
|
||||
232
src/components/Dashboard/common/UserNotifierToggle.jsx
Normal file
232
src/components/Dashboard/common/UserNotifierToggle.jsx
Normal file
@ -0,0 +1,232 @@
|
||||
import PropTypes from 'prop-types'
|
||||
import { useState, useEffect, useContext } from 'react'
|
||||
import { Button, message, Popover, Typography, Space, Flex } from 'antd'
|
||||
import { UserOutlined } from '@ant-design/icons'
|
||||
import BellIcon from '../../Icons/BellIcon'
|
||||
import NewMailIcon from '../../Icons/NewMailIcon'
|
||||
import { ApiServerContext } from '../context/ApiServerContext'
|
||||
import { AuthContext } from '../context/AuthContext'
|
||||
import { LoadingOutlined } from '@ant-design/icons'
|
||||
import InfoCircleIcon from '../../Icons/InfoCircleIcon'
|
||||
|
||||
const { Text } = Typography
|
||||
|
||||
const UserNotifierToggle = ({
|
||||
type,
|
||||
objectData,
|
||||
disabled = false,
|
||||
...buttonProps
|
||||
}) => {
|
||||
const {
|
||||
toggleUserNotifier,
|
||||
editUserNotifier,
|
||||
fetchUserNotifiersForObject,
|
||||
fetchAllUserNotifiersForObject
|
||||
} = useContext(ApiServerContext)
|
||||
const { userProfile } = useContext(AuthContext)
|
||||
const [isNotifying, setIsNotifying] = useState(false)
|
||||
const [loading, setLoading] = useState(false)
|
||||
const [initialLoad, setInitialLoad] = useState(true)
|
||||
const [allNotifiers, setAllNotifiers] = useState([])
|
||||
const [popoverOpen, setPopoverOpen] = useState(false)
|
||||
const [popoverLoading, setPopoverLoading] = useState(false)
|
||||
const [emailTogglingId, setEmailTogglingId] = useState(null)
|
||||
|
||||
const objectId = objectData?._id
|
||||
|
||||
useEffect(() => {
|
||||
const loadNotifierState = async () => {
|
||||
if (!objectId || !type) return
|
||||
|
||||
setInitialLoad(true)
|
||||
try {
|
||||
const { data } = await fetchUserNotifiersForObject(objectId, type)
|
||||
setIsNotifying(data?.length > 0)
|
||||
} catch (error) {
|
||||
console.error('Error fetching user notifier state:', error)
|
||||
} finally {
|
||||
setInitialLoad(false)
|
||||
}
|
||||
}
|
||||
|
||||
loadNotifierState()
|
||||
}, [objectId, type, fetchUserNotifiersForObject])
|
||||
|
||||
useEffect(() => {
|
||||
const loadAllNotifiers = async () => {
|
||||
if (!objectId || !type || !popoverOpen) return
|
||||
|
||||
setPopoverLoading(true)
|
||||
try {
|
||||
const { data } = await fetchAllUserNotifiersForObject(objectId, type)
|
||||
setAllNotifiers(data || [])
|
||||
} catch (error) {
|
||||
console.error('Error fetching all user notifiers:', error)
|
||||
setAllNotifiers([])
|
||||
} finally {
|
||||
setPopoverLoading(false)
|
||||
}
|
||||
}
|
||||
|
||||
loadAllNotifiers()
|
||||
}, [objectId, type, popoverOpen, fetchAllUserNotifiersForObject])
|
||||
|
||||
const handleClick = async () => {
|
||||
if (!objectId || !type || loading) return
|
||||
|
||||
setLoading(true)
|
||||
try {
|
||||
const enabled = await toggleUserNotifier(objectId, type)
|
||||
setIsNotifying(enabled)
|
||||
if (popoverOpen) {
|
||||
const { data } = await fetchAllUserNotifiersForObject(objectId, type)
|
||||
setAllNotifiers(data || [])
|
||||
}
|
||||
message.success(
|
||||
enabled
|
||||
? 'Notifications enabled for this object'
|
||||
: 'Notifications disabled for this object'
|
||||
)
|
||||
} catch (error) {
|
||||
console.error('Error toggling user notifier:', error)
|
||||
message.error('Failed to update notifications')
|
||||
} finally {
|
||||
setLoading(false)
|
||||
}
|
||||
}
|
||||
|
||||
const getUserDisplayName = (user) => {
|
||||
if (!user) return 'Unknown'
|
||||
return (
|
||||
user.name ||
|
||||
user.username ||
|
||||
`${user.firstName || ''} ${user.lastName || ''}`.trim() ||
|
||||
user.email ||
|
||||
'Unknown'
|
||||
)
|
||||
}
|
||||
|
||||
const isCurrentUser = (user) => user?._id === userProfile?._id
|
||||
|
||||
const handleEmailToggle = async (item) => {
|
||||
if (!isCurrentUser(item.user) || emailTogglingId) return
|
||||
setEmailTogglingId(item._id)
|
||||
const newEmail = !item.email
|
||||
try {
|
||||
const result = await editUserNotifier(item._id, { email: newEmail })
|
||||
if (result) {
|
||||
setAllNotifiers((prev) =>
|
||||
prev.map((n) =>
|
||||
n._id === item._id ? { ...n, email: result.email ?? newEmail } : n
|
||||
)
|
||||
)
|
||||
message.success(
|
||||
(result.email ?? newEmail)
|
||||
? 'Email notifications enabled'
|
||||
: 'Email notifications disabled'
|
||||
)
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Error toggling email:', error)
|
||||
message.error('Failed to update email notifications')
|
||||
} finally {
|
||||
setEmailTogglingId(null)
|
||||
}
|
||||
}
|
||||
|
||||
const popoverContent = (
|
||||
<Flex
|
||||
vertical
|
||||
justify='center'
|
||||
style={{ minWidth: 240, minHeight: 25 }}
|
||||
gap={'4px'}
|
||||
>
|
||||
{popoverLoading ? (
|
||||
<Space size={'small'}>
|
||||
<LoadingOutlined />
|
||||
<Text style={{ margin: 0 }}>Loading, please wait...</Text>
|
||||
</Space>
|
||||
) : allNotifiers.length === 0 ? (
|
||||
<Space size={'small'}>
|
||||
<Text style={{ margin: 0 }} type='secondary'>
|
||||
<InfoCircleIcon />
|
||||
</Text>
|
||||
<Text style={{ margin: 0 }} type='secondary'>
|
||||
No users subscribed.
|
||||
</Text>
|
||||
</Space>
|
||||
) : (
|
||||
<>
|
||||
{[...allNotifiers]
|
||||
.sort(
|
||||
(a, b) =>
|
||||
(isCurrentUser(b.user) ? 1 : 0) -
|
||||
(isCurrentUser(a.user) ? 1 : 0)
|
||||
)
|
||||
.map((item) => (
|
||||
<Flex key={item._id} justify='space-between' align='center'>
|
||||
<Flex align='center' gap={'6px'}>
|
||||
<UserOutlined />
|
||||
<Text>{getUserDisplayName(item.user)}</Text>
|
||||
{isCurrentUser(item.user) && (
|
||||
<Text type='secondary'> (you)</Text>
|
||||
)}
|
||||
</Flex>
|
||||
<Space size={'small'}>
|
||||
<Button
|
||||
type='text'
|
||||
icon={
|
||||
<NewMailIcon
|
||||
style={{
|
||||
color: item.email ? 'var(--color-primary)' : undefined
|
||||
}}
|
||||
/>
|
||||
}
|
||||
size='small'
|
||||
disabled={!isCurrentUser(item.user)}
|
||||
loading={emailTogglingId === item._id}
|
||||
onClick={() => handleEmailToggle(item)}
|
||||
/>
|
||||
</Space>
|
||||
</Flex>
|
||||
))}
|
||||
</>
|
||||
)}
|
||||
</Flex>
|
||||
)
|
||||
|
||||
return (
|
||||
<Popover
|
||||
content={popoverContent}
|
||||
title={null}
|
||||
trigger='hover'
|
||||
placement='bottomLeft'
|
||||
arrow={false}
|
||||
open={popoverOpen}
|
||||
onOpenChange={setPopoverOpen}
|
||||
styles={{ body: { padding: '10px 15px' } }}
|
||||
>
|
||||
<Button
|
||||
{...buttonProps}
|
||||
icon={
|
||||
<BellIcon
|
||||
style={{
|
||||
color: isNotifying ? 'var(--color-warning)' : undefined
|
||||
}}
|
||||
/>
|
||||
}
|
||||
disabled={disabled || loading || initialLoad}
|
||||
loading={loading || !objectId || !type}
|
||||
onClick={handleClick}
|
||||
/>
|
||||
</Popover>
|
||||
)
|
||||
}
|
||||
|
||||
UserNotifierToggle.propTypes = {
|
||||
type: PropTypes.string.isRequired,
|
||||
objectData: PropTypes.object.isRequired,
|
||||
disabled: PropTypes.bool
|
||||
}
|
||||
|
||||
export default UserNotifierToggle
|
||||
@ -36,6 +36,7 @@ const ApiServerProvider = ({ children }) => {
|
||||
const [retryCallback, setRetryCallback] = useState(null)
|
||||
const subscribedCallbacksRef = useRef(new Map())
|
||||
const subscribedLockCallbacksRef = useRef(new Map())
|
||||
const notificationListenersRef = useRef(new Set())
|
||||
|
||||
const handleLockUpdate = useCallback(
|
||||
async (lockData) => {
|
||||
@ -72,6 +73,16 @@ const ApiServerProvider = ({ children }) => {
|
||||
const clearSubscriptions = useCallback(() => {
|
||||
subscribedCallbacksRef.current.clear()
|
||||
subscribedLockCallbacksRef.current.clear()
|
||||
notificationListenersRef.current.clear()
|
||||
}, [])
|
||||
|
||||
const registerNotificationListener = useCallback((callback) => {
|
||||
notificationListenersRef.current.add(callback)
|
||||
return () => notificationListenersRef.current.delete(callback)
|
||||
}, [])
|
||||
|
||||
const unregisterNotificationListener = useCallback((callback) => {
|
||||
notificationListenersRef.current.delete(callback)
|
||||
}, [])
|
||||
|
||||
const connectToServer = useCallback(() => {
|
||||
@ -101,6 +112,25 @@ const ApiServerProvider = ({ children }) => {
|
||||
newSocket.on('objectDelete', handleObjectDelete)
|
||||
newSocket.on('lockUpdate', handleLockUpdate)
|
||||
newSocket.on('modelStats', handleModelStats)
|
||||
newSocket.on('notification', (data) => {
|
||||
let notification
|
||||
try {
|
||||
notification =
|
||||
typeof data === 'string' ? JSON.parse(data) : data
|
||||
} catch (err) {
|
||||
logger.error('Failed to parse notification:', err)
|
||||
return
|
||||
}
|
||||
if (!notification) return
|
||||
logger.debug('Notification received:', notification)
|
||||
notificationListenersRef.current.forEach((cb) => {
|
||||
try {
|
||||
cb(notification)
|
||||
} catch (err) {
|
||||
logger.error('Error in notification callback:', err)
|
||||
}
|
||||
})
|
||||
})
|
||||
|
||||
newSocket.on('disconnect', () => {
|
||||
logger.debug('Api Server disconnected')
|
||||
@ -1212,6 +1242,130 @@ const ApiServerProvider = ({ children }) => {
|
||||
}
|
||||
}
|
||||
|
||||
const createUserNotifier = async (objectId, objectType) => {
|
||||
if (!userProfile?._id) return null
|
||||
return createObject('userNotifier', {
|
||||
user: userProfile._id,
|
||||
object: objectId,
|
||||
objectType
|
||||
})
|
||||
}
|
||||
|
||||
const deleteUserNotifier = async (userNotifierId) => {
|
||||
return deleteObject(userNotifierId, 'userNotifier')
|
||||
}
|
||||
|
||||
const fetchUserNotifiersForObject = async (objectId, objectType) => {
|
||||
if (!userProfile?._id) return { data: [] }
|
||||
const result = await fetchObjects('userNotifier', {
|
||||
filter: {
|
||||
user: userProfile._id,
|
||||
object: objectId,
|
||||
objectType
|
||||
},
|
||||
limit: 1
|
||||
})
|
||||
return result || { data: [] }
|
||||
}
|
||||
|
||||
const fetchAllUserNotifiersForObject = async (objectId, objectType) => {
|
||||
const result = await fetchObjects('userNotifier', {
|
||||
filter: {
|
||||
object: objectId,
|
||||
objectType
|
||||
},
|
||||
limit: 100
|
||||
})
|
||||
return result || { data: [] }
|
||||
}
|
||||
|
||||
const toggleUserNotifier = async (objectId, objectType) => {
|
||||
const { data } = await fetchUserNotifiersForObject(objectId, objectType)
|
||||
const existing = data?.[0]
|
||||
if (existing) {
|
||||
await deleteUserNotifier(existing._id)
|
||||
return false
|
||||
} else {
|
||||
await createUserNotifier(objectId, objectType)
|
||||
return true
|
||||
}
|
||||
}
|
||||
|
||||
const editUserNotifier = async (userNotifierId, updates) => {
|
||||
try {
|
||||
const result = await updateObject(userNotifierId, 'userNotifier', updates)
|
||||
return result
|
||||
} catch (err) {
|
||||
console.error(err)
|
||||
showError(err, () => editUserNotifier(userNotifierId, updates))
|
||||
return null
|
||||
}
|
||||
}
|
||||
|
||||
const fetchNotificationsApi = useCallback(async () => {
|
||||
const response = await axios.get(`${config.backendUrl}/notifications`, {
|
||||
params: { limit: 100, sort: 'createdAt', order: 'descend' },
|
||||
headers: {
|
||||
Accept: 'application/json',
|
||||
Authorization: `Bearer ${token}`
|
||||
}
|
||||
})
|
||||
return Array.isArray(response.data) ? response.data : []
|
||||
}, [token])
|
||||
|
||||
const markNotificationAsReadApi = useCallback(
|
||||
async (notificationId) => {
|
||||
await axios.put(
|
||||
`${config.backendUrl}/notifications/${notificationId}/read`,
|
||||
{},
|
||||
{
|
||||
headers: {
|
||||
Accept: 'application/json',
|
||||
Authorization: `Bearer ${token}`
|
||||
}
|
||||
}
|
||||
)
|
||||
},
|
||||
[token]
|
||||
)
|
||||
|
||||
const markAllNotificationsAsReadApi = useCallback(async () => {
|
||||
await axios.put(
|
||||
`${config.backendUrl}/notifications/read-all`,
|
||||
{},
|
||||
{
|
||||
headers: {
|
||||
Accept: 'application/json',
|
||||
Authorization: `Bearer ${token}`
|
||||
}
|
||||
}
|
||||
)
|
||||
}, [token])
|
||||
|
||||
const deleteNotificationApi = useCallback(
|
||||
async (notificationId) => {
|
||||
await axios.delete(
|
||||
`${config.backendUrl}/notifications/${notificationId}`,
|
||||
{
|
||||
headers: {
|
||||
Accept: 'application/json',
|
||||
Authorization: `Bearer ${token}`
|
||||
}
|
||||
}
|
||||
)
|
||||
},
|
||||
[token]
|
||||
)
|
||||
|
||||
const deleteAllNotificationsApi = useCallback(async () => {
|
||||
await axios.delete(`${config.backendUrl}/notifications`, {
|
||||
headers: {
|
||||
Accept: 'application/json',
|
||||
Authorization: `Bearer ${token}`
|
||||
}
|
||||
})
|
||||
}, [token])
|
||||
|
||||
const flushFile = async (id) => {
|
||||
logger.debug('Flushing file...')
|
||||
try {
|
||||
@ -1288,7 +1442,20 @@ const ApiServerProvider = ({ children }) => {
|
||||
sendObjectAction,
|
||||
uploadFile,
|
||||
flushFile,
|
||||
formatFileName
|
||||
formatFileName,
|
||||
createUserNotifier,
|
||||
deleteUserNotifier,
|
||||
fetchUserNotifiersForObject,
|
||||
fetchAllUserNotifiersForObject,
|
||||
toggleUserNotifier,
|
||||
editUserNotifier,
|
||||
fetchNotificationsApi,
|
||||
markNotificationAsReadApi,
|
||||
markAllNotificationsAsReadApi,
|
||||
deleteNotificationApi,
|
||||
deleteAllNotificationsApi,
|
||||
registerNotificationListener,
|
||||
unregisterNotificationListener
|
||||
}}
|
||||
>
|
||||
{contextHolder}
|
||||
|
||||
183
src/components/Dashboard/context/NotificationContext.jsx
Normal file
183
src/components/Dashboard/context/NotificationContext.jsx
Normal file
@ -0,0 +1,183 @@
|
||||
import {
|
||||
createContext,
|
||||
useState,
|
||||
useContext,
|
||||
useCallback,
|
||||
useEffect
|
||||
} from 'react'
|
||||
import { notification, Drawer } from 'antd'
|
||||
import PropTypes from 'prop-types'
|
||||
import { AuthContext } from './AuthContext'
|
||||
import { ApiServerContext } from './ApiServerContext'
|
||||
import NotificationCenter from '../common/NotificationCenter'
|
||||
import Notification from '../common/Notification'
|
||||
|
||||
const NotificationContext = createContext()
|
||||
|
||||
const NotificationProvider = ({ children }) => {
|
||||
const [api, contextHolder] = notification.useNotification()
|
||||
const { authenticated } = useContext(AuthContext)
|
||||
const {
|
||||
showError,
|
||||
fetchNotificationsApi,
|
||||
markNotificationAsReadApi,
|
||||
markAllNotificationsAsReadApi,
|
||||
deleteNotificationApi,
|
||||
deleteAllNotificationsApi,
|
||||
registerNotificationListener
|
||||
} = useContext(ApiServerContext)
|
||||
|
||||
const [notificationCenterVisible, setNotificationCenterVisible] =
|
||||
useState(false)
|
||||
const [notifications, setNotifications] = useState([])
|
||||
const [notificationsLoading, setNotificationsLoading] = useState(false)
|
||||
|
||||
const fetchNotifications = useCallback(async () => {
|
||||
if (!authenticated) return []
|
||||
setNotificationsLoading(true)
|
||||
try {
|
||||
const data = await fetchNotificationsApi()
|
||||
setNotifications(data)
|
||||
return data
|
||||
} catch (err) {
|
||||
console.error(err)
|
||||
showError(err, () => fetchNotifications())
|
||||
return []
|
||||
} finally {
|
||||
setNotificationsLoading(false)
|
||||
}
|
||||
}, [authenticated, fetchNotificationsApi, showError])
|
||||
|
||||
const markNotificationAsRead = useCallback(
|
||||
async (notificationId) => {
|
||||
try {
|
||||
await markNotificationAsReadApi(notificationId)
|
||||
setNotifications((prev) =>
|
||||
prev.map((n) => (n._id === notificationId ? { ...n, read: true } : n))
|
||||
)
|
||||
} catch (err) {
|
||||
console.error(err)
|
||||
showError(err, () => markNotificationAsRead(notificationId))
|
||||
}
|
||||
},
|
||||
[markNotificationAsReadApi, showError]
|
||||
)
|
||||
|
||||
const markAllNotificationsAsRead = useCallback(async () => {
|
||||
try {
|
||||
await markAllNotificationsAsReadApi()
|
||||
setNotifications((prev) => prev.map((n) => ({ ...n, read: true })))
|
||||
} catch (err) {
|
||||
console.error(err)
|
||||
showError(err, () => markAllNotificationsAsRead())
|
||||
}
|
||||
}, [markAllNotificationsAsReadApi, showError])
|
||||
|
||||
const deleteNotification = useCallback(
|
||||
async (notificationId) => {
|
||||
try {
|
||||
await deleteNotificationApi(notificationId)
|
||||
setNotifications((prev) => prev.filter((n) => n._id !== notificationId))
|
||||
} catch (err) {
|
||||
console.error(err)
|
||||
showError(err, () => deleteNotification(notificationId))
|
||||
}
|
||||
},
|
||||
[deleteNotificationApi, showError]
|
||||
)
|
||||
|
||||
const deleteAllNotifications = useCallback(async () => {
|
||||
try {
|
||||
await deleteAllNotificationsApi()
|
||||
setNotifications([])
|
||||
} catch (err) {
|
||||
console.error(err)
|
||||
showError(err, () => deleteAllNotifications())
|
||||
}
|
||||
}, [deleteAllNotificationsApi, showError])
|
||||
|
||||
const toggleNotificationCenter = useCallback(() => {
|
||||
setNotificationCenterVisible((prev) => !prev)
|
||||
}, [])
|
||||
|
||||
const unreadCount = notifications.filter((n) => !n.read).length
|
||||
|
||||
useEffect(() => {
|
||||
if (authenticated && notificationCenterVisible) {
|
||||
fetchNotifications()
|
||||
}
|
||||
}, [authenticated, notificationCenterVisible, fetchNotifications])
|
||||
|
||||
useEffect(() => {
|
||||
if (authenticated) {
|
||||
fetchNotifications()
|
||||
}
|
||||
}, [authenticated, fetchNotifications])
|
||||
|
||||
useEffect(() => {
|
||||
if (!authenticated || !registerNotificationListener) return
|
||||
const handleNotification = (notif) => {
|
||||
setNotifications((prev) => {
|
||||
const exists = prev.some((n) => n._id === notif._id)
|
||||
if (exists) return prev
|
||||
return [{ ...notif, read: false }, ...prev]
|
||||
})
|
||||
const show = api.open
|
||||
show({
|
||||
message: (
|
||||
<Notification
|
||||
notification={notif}
|
||||
onDelete={deleteNotification}
|
||||
showCard={false}
|
||||
showDelete={false}
|
||||
showExtraInfo={false}
|
||||
inlineIcon={true}
|
||||
/>
|
||||
),
|
||||
icon: null,
|
||||
duration: 3,
|
||||
key: notif._id
|
||||
})
|
||||
}
|
||||
const unregister = registerNotificationListener(handleNotification)
|
||||
return unregister
|
||||
}, [authenticated, registerNotificationListener, api])
|
||||
|
||||
return (
|
||||
<NotificationContext.Provider
|
||||
value={{
|
||||
notificationCenterVisible,
|
||||
toggleNotificationCenter,
|
||||
notifications,
|
||||
fetchNotifications,
|
||||
markNotificationAsRead,
|
||||
markAllNotificationsAsRead,
|
||||
deleteNotification,
|
||||
deleteAllNotifications,
|
||||
unreadCount,
|
||||
notificationsLoading
|
||||
}}
|
||||
>
|
||||
{contextHolder}
|
||||
{children}
|
||||
<Drawer
|
||||
title='Notifications'
|
||||
placement='right'
|
||||
width={400}
|
||||
onClose={() => setNotificationCenterVisible(false)}
|
||||
open={notificationCenterVisible}
|
||||
>
|
||||
<NotificationCenter
|
||||
visible={notificationCenterVisible}
|
||||
onClose={() => setNotificationCenterVisible(false)}
|
||||
/>
|
||||
</Drawer>
|
||||
</NotificationContext.Provider>
|
||||
)
|
||||
}
|
||||
|
||||
NotificationProvider.propTypes = {
|
||||
children: PropTypes.node.isRequired
|
||||
}
|
||||
|
||||
export { NotificationContext, NotificationProvider }
|
||||
@ -100,6 +100,23 @@ export const ThemeProvider = ({ children }) => {
|
||||
return colors
|
||||
}
|
||||
|
||||
// Set CSS custom properties for theme colors
|
||||
useEffect(() => {
|
||||
const root = document.documentElement
|
||||
root.style.setProperty('--color-primary', colors.colorPrimary)
|
||||
root.style.setProperty('--color-success', colors.colorSuccess)
|
||||
root.style.setProperty('--color-warning', colors.colorWarning)
|
||||
root.style.setProperty('--color-error', colors.colorError)
|
||||
root.style.setProperty('--color-info', colors.colorInfo)
|
||||
root.style.setProperty('--color-link', colors.colorLink)
|
||||
root.style.setProperty('--color-cyan', colors.colorCyan)
|
||||
root.style.setProperty('--color-pink', colors.colorPink)
|
||||
root.style.setProperty('--color-purple', colors.colorPurple)
|
||||
root.style.setProperty('--color-magenta', colors.colorMagenta)
|
||||
root.style.setProperty('--color-volcano', colors.colorVolcano)
|
||||
root.style.setProperty('--layout-header-bg', isDarkMode ? '#141414' : '#ffffff')
|
||||
}, [isDarkMode])
|
||||
|
||||
const themeConfig = {
|
||||
algorithm: getThemeAlgorithm(),
|
||||
token: {
|
||||
|
||||
@ -158,26 +158,19 @@ export const Payment = {
|
||||
showHyperlink: true
|
||||
},
|
||||
{
|
||||
name: 'useRemainingAmount',
|
||||
label: 'Use Remaining Amount',
|
||||
type: 'boolean',
|
||||
required: true
|
||||
},
|
||||
{
|
||||
name: 'vendor',
|
||||
label: 'Vendor',
|
||||
name: 'payTo',
|
||||
label: 'Pay To',
|
||||
type: 'object',
|
||||
objectType: 'vendor',
|
||||
showHyperlink: true,
|
||||
readOnly: true
|
||||
},
|
||||
{
|
||||
name: 'client',
|
||||
label: 'Client',
|
||||
type: 'object',
|
||||
objectType: 'client',
|
||||
showHyperlink: true,
|
||||
readOnly: true
|
||||
required: true,
|
||||
readOnly: true,
|
||||
disabled: (objectData) => {
|
||||
return objectData?.invoice?.orderType == 'purchaseOrder'
|
||||
},
|
||||
value: (objectData) => {
|
||||
return objectData?.invoice?.from
|
||||
}
|
||||
},
|
||||
{
|
||||
name: 'paymentDate',
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user