Implemented notifications.
Some checks failed
farmcontrol/farmcontrol-ui/pipeline/head There was a failure building this commit

This commit is contained in:
Tom Butcher 2026-03-01 01:42:27 +00:00
parent 1e0f125f21
commit 2622fae555
42 changed files with 1059 additions and 269 deletions

View File

@ -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';
}

View File

@ -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>

View File

@ -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}

View File

@ -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}

View File

@ -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}

View File

@ -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>

View File

@ -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}

View File

@ -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}

View File

@ -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}

View File

@ -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}

View File

@ -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}

View File

@ -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}

View File

@ -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}

View File

@ -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}

View File

@ -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}

View File

@ -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}

View File

@ -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}

View File

@ -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}

View File

@ -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}

View File

@ -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}

View File

@ -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}

View File

@ -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}

View File

@ -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}

View File

@ -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}

View File

@ -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}

View File

@ -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}

View File

@ -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}

View File

@ -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}

View File

@ -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}

View File

@ -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}

View File

@ -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}

View File

@ -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}

View File

@ -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}

View File

@ -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'

View File

@ -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

View File

@ -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>
)
}

View File

@ -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>
)

View 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

View File

@ -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}

View 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 }

View File

@ -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: {

View File

@ -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',