Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Websockets: Support for new notifications #2137

Merged
merged 12 commits into from
Oct 22, 2024
12 changes: 11 additions & 1 deletion src/common/queries/sockets.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,13 @@ import { useCurrentCompany } from '../hooks/useCurrentCompany';

// This file defines global events system for query invalidation.

export const events = ['App\\Events\\Invoice\\InvoiceWasPaid'] as const;
export const events = [
'App\\Events\\Invoice\\InvoiceWasPaid',
'App\\Events\\Invoice\\InvoiceWasViewed',
'App\\Events\\Payment\\PaymentWasUpdated',
'App\\Events\\Credit\\CreditWasCreated',
'App\\Events\\Credit\\CreditWasUpdated',
] as const;

export type Event = (typeof events)[number];

Expand All @@ -26,6 +32,10 @@ export function useGlobalSocketEvents() {

const callbacks: Callbacks = {
'App\\Events\\Invoice\\InvoiceWasPaid': () => {},
'App\\Events\\Invoice\\InvoiceWasViewed': () => {},
'App\\Events\\Payment\\PaymentWasUpdated': () => {},
'App\\Events\\Credit\\CreditWasCreated': () => {},
'App\\Events\\Credit\\CreditWasUpdated': () => {},
};

useEffect(() => {
Expand Down
105 changes: 103 additions & 2 deletions src/components/Notifications.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -18,9 +18,12 @@ import { useSocketEvent } from '$app/common/queries/sockets';
import { Invoice } from '$app/common/interfaces/invoice';
import { route } from '$app/common/helpers/route';
import { ClickableElement } from './cards';
import { date } from '$app/common/helpers';
import { date, trans } from '$app/common/helpers';
import { useCurrentCompanyDateFormats } from '$app/common/hooks/useCurrentCompanyDateFormats';
import { NonClickableElement } from './cards/NonClickableElement';
import { useCurrentCompanyUser } from '$app/common/hooks/useCurrentCompanyUser';
import { Credit } from '$app/common/interfaces/credit';
import { Payment } from '$app/common/interfaces/payment';
import classNames from 'classnames';

export interface Notification {
Expand All @@ -41,8 +44,16 @@ export function Notifications() {
const [isVisible, setIsVisible] = useState(false);
const [notifications, setNotifications] = useAtom(notificationsAtom);

const companyUser = useCurrentCompanyUser();

useSocketEvent({
on: ['App\\Events\\Invoice\\InvoiceWasPaid'],
on: [
'App\\Events\\Invoice\\InvoiceWasPaid',
'App\\Events\\Invoice\\InvoiceWasViewed',
'App\\Events\\Credit\\CreditWasCreated',
'App\\Events\\Credit\\CreditWasUpdated',
'App\\Events\\Payment\\PaymentWasUpdated',
],
callback: ({ event, data }) => {
if (event === 'App\\Events\\Invoice\\InvoiceWasPaid') {
const $invoice = data as Invoice;
Expand All @@ -63,6 +74,96 @@ export function Notifications() {

setNotifications((notifications) => [...notifications, notification]);
}

if (event === 'App\\Events\\Invoice\\InvoiceWasViewed') {
if (
!companyUser?.notifications.email.includes('invoice_viewed') ||
!companyUser?.notifications.email.includes('invoice_viewed_user')
) {
return;
}

const $invoice = data as Invoice;

const notification = {
label: trans('notification_invoice_viewed_subject', {
invoice: $invoice.number,
client: $invoice.client?.display_name,
}),
date: new Date().toString(),
link: route('/invoices/:id/edit', { id: $invoice.id }),
readAt: null,
};

if (
notifications.some((n) => n.label === notification.label) ||
notifications.some((n) => n.link === notification.link)
) {
return;
}

setNotifications((notifications) => [...notifications, notification]);
}

if (event === 'App\\Events\\Credit\\CreditWasCreated') {
const $credit = data as Credit;

const notification = {
label: `${t('credit_created')}: ${$credit.number}`,
date: new Date().toString(),
link: route('/credits/:id/edit', { id: $credit.id }),
readAt: null,
};

if (
notifications.some((n) => n.label === notification.label) ||
notifications.some((n) => n.link === notification.link)
) {
return;
}

setNotifications((notifications) => [...notifications, notification]);
}

if (event === 'App\\Events\\Credit\\CreditWasUpdated') {
const $credit = data as Credit;

const notification = {
label: `${t('credit_updated')}: ${$credit.number}`,
date: new Date().toString(),
link: route('/credits/:id/edit', { id: $credit.id }),
readAt: null,
};

if (
notifications.some((n) => n.label === notification.label) ||
notifications.some((n) => n.link === notification.link)
) {
return;
}

setNotifications((notifications) => [...notifications, notification]);
}

if (event === 'App\\Events\\Payment\\PaymentWasUpdated') {
const payment = data as Payment;

const notification = {
label: `${t('payment_updated')}: ${payment.number}`,
date: new Date().toString(),
link: route('/payments/:id/edit', { id: payment.id }),
readAt: null,
};

if (
notifications.some((n) => n.label === notification.label) ||
notifications.some((n) => n.link === notification.link)
) {
return;
}

setNotifications((notifications) => [...notifications, notification]);
}
},
});

Expand Down
43 changes: 41 additions & 2 deletions src/components/layouts/Default.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -23,11 +23,12 @@ import {
Briefcase,
Clock,
PieChart,
Info,
} from 'react-feather';
import CommonProps from '../../common/interfaces/common-props.interface';
import { useTranslation } from 'react-i18next';
import { useLocation } from 'react-router-dom';
import { Button } from '$app/components/forms';
import { Button, Link } from '$app/components/forms';
import { Breadcrumbs, Page } from '$app/components/Breadcrumbs';
import { DesktopSidebar, NavigationItem } from './components/DesktopSidebar';
import { MobileSidebar } from './components/MobileSidebar';
Expand All @@ -36,7 +37,7 @@ import { BiBuildings, BiWallet, BiFile } from 'react-icons/bi';
import { AiOutlineBank } from 'react-icons/ai';
import { ModuleBitmask } from '$app/pages/settings/account-management/component';
import { QuickCreatePopover } from '$app/components/QuickCreatePopover';
import { isDemo, isHosted, isSelfHosted } from '$app/common/helpers';
import { isDemo, isHosted, isSelfHosted, trans } from '$app/common/helpers';
import { useUnlockButtonForHosted } from '$app/common/hooks/useUnlockButtonForHosted';
import { useUnlockButtonForSelfHosted } from '$app/common/hooks/useUnlockButtonForSelfHosted';
import { useCurrentCompanyUser } from '$app/common/hooks/useCurrentCompanyUser';
Expand All @@ -57,6 +58,9 @@ import { useInjectUserChanges } from '$app/common/hooks/useInjectUserChanges';
import { useAtomValue } from 'jotai';
import { usePreventNavigation } from '$app/common/hooks/usePreventNavigation';
import { Notifications } from '../Notifications';
import { useSocketEvent } from '$app/common/queries/sockets';
import { Invoice } from '$app/common/interfaces/invoice';
import toast from 'react-hot-toast';

export interface SaveOption {
label: string;
Expand Down Expand Up @@ -365,6 +369,41 @@ export function Default(props: Props) {
const saveBtn = useAtomValue(saveBtnAtom);
const navigationTopRightElement = useNavigationTopRightElement();

useSocketEvent<Invoice>({
on: ['App\\Events\\Invoice\\InvoiceWasViewed'],
callback: ({ data }) => {
if (
!companyUser?.notifications.email.includes('invoice_viewed') ||
!companyUser?.notifications.email.includes('invoice_viewed_user')
) {
return;
}

toast(
<div className="flex flex-col gap-2">
<span className="flex items-center gap-1">
<Info size={18} />
<span>
{trans('notification_invoice_viewed_subject', {
invoice: data.number,
client: data.client?.display_name,
})}
.
</span>
</span>

<div className="flex justify-center">
<Link to={`/invoices/${data.id}/edit`}>{t('view_invoice')}</Link>
</div>
</div>,
{
duration: 8000,
position: 'top-center',
}
);
},
});

return (
<div>
<ActivateCompany />
Expand Down
13 changes: 13 additions & 0 deletions src/pages/clients/show/Client.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -92,6 +92,19 @@ export default function Client() {
callback: () => $refetch(['invoices']),
});

useSocketEvent({
on: 'App\\Events\\Payment\\PaymentWasUpdated',
callback: () => $refetch(['payments']),
});

useSocketEvent({
on: [
'App\\Events\\Credit\\CreditWasCreated',
'App\\Events\\Credit\\CreditWasUpdated',
],
callback: () => $refetch(['credits']),
});

return (
<Default
title={documentTitle}
Expand Down
18 changes: 18 additions & 0 deletions src/pages/credits/Credit.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,8 @@ import { creditAtom, invoiceSumAtom } from './common/atoms';
import { useActions, useCreditUtilities, useSave } from './common/hooks';
import { Tabs } from '$app/components/Tabs';
import { useTabs } from './common/hooks/useTabs';
import { Banner } from '$app/components/Banner';
import { socketId, useSocketEvent, WithSocketId } from '$app/common/queries/sockets';

export default function Credit() {
const { documentTitle } = useTitle('edit_credit');
Expand Down Expand Up @@ -93,6 +95,17 @@ export default function Credit() {
credit && calculateInvoiceSum(credit);
}, [credit]);

useSocketEvent<WithSocketId<ICredit>>({
on: ['App\\Events\\Credit\\CreditWasUpdated'],
callback: ({ data }) => {
if (socketId()?.toString() !== data['x-socket-id']) {
document
.getElementById('creditUpdateBanner')
?.classList.remove('hidden');
}
},
});

return (
<Default
title={documentTitle}
Expand All @@ -108,6 +121,11 @@ export default function Credit() {
/>
),
})}
aboveMainContainer={
<Banner id="creditUpdateBanner" className="hidden" variant="orange">
{t('credit_status_changed')}
</Banner>
}
>
{credit?.id === id ? (
<div className="space-y-4">
Expand Down
10 changes: 10 additions & 0 deletions src/pages/credits/index/Credits.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,8 @@ import {
} from '$app/pages/settings/invoice-design/pages/custom-designs/components/ChangeTemplate';
import { Credit } from '$app/common/interfaces/credit';
import { useDateRangeColumns } from '../common/hooks/useDateRangeColumns';
import { useSocketEvent } from '$app/common/queries/sockets';
import { $refetch } from '$app/common/hooks/useRefetch';

export default function Credits() {
useTitle('credits');
Expand All @@ -51,6 +53,14 @@ export default function Credits() {
changeTemplateResources,
} = useChangeTemplate();

useSocketEvent({
on: [
'App\\Events\\Credit\\CreditWasCreated',
'App\\Events\\Credit\\CreditWasUpdated',
],
callback: () => $refetch(['credits']),
});

return (
<Default title={t('credits')} breadcrumbs={pages} docsLink="en/credits/">
<DataTable
Expand Down
5 changes: 4 additions & 1 deletion src/pages/invoices/index/Invoices.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -92,7 +92,10 @@ export default function Invoices() {
} = useChangeTemplate();

useSocketEvent({
on: 'App\\Events\\Invoice\\InvoiceWasPaid',
on: [
'App\\Events\\Invoice\\InvoiceWasPaid',
'App\\Events\\Invoice\\InvoiceWasViewed',
],
callback: () => $refetch(['invoices']),
});

Expand Down
22 changes: 22 additions & 0 deletions src/pages/payments/Payment.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,12 @@ import {
ChangeTemplateModal,
useChangeTemplate,
} from '../settings/invoice-design/pages/custom-designs/components/ChangeTemplate';
import { Banner } from '$app/components/Banner';
import {
socketId,
useSocketEvent,
WithSocketId,
} from '$app/common/queries/sockets';

export default function Payment() {
const [t] = useTranslation();
Expand Down Expand Up @@ -70,6 +76,17 @@ export default function Payment() {
changeTemplateResources,
} = useChangeTemplate();

useSocketEvent<WithSocketId<PaymentEntity>>({
on: ['App\\Events\\Payment\\PaymentWasUpdated'],
callback: ({ data }) => {
if (socketId()?.toString() !== data['x-socket-id']) {
document
.getElementById('paymentUpdateBanner')
?.classList.remove('hidden');
}
},
});

return (
<Default
title={t('payment')}
Expand All @@ -87,6 +104,11 @@ export default function Payment() {
),
disableSaveButton: !paymentValue,
})}
aboveMainContainer={
<Banner id="paymentUpdateBanner" className="hidden" variant="orange">
{t('payment_status_changed')}
</Banner>
}
>
<Container breadcrumbs={[]}>
<Tabs tabs={tabs} disableBackupNavigation />
Expand Down
7 changes: 7 additions & 0 deletions src/pages/payments/index/Payments.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,8 @@ import {
} from '$app/pages/settings/invoice-design/pages/custom-designs/components/ChangeTemplate';
import { EntityState } from '$app/common/enums/entity-state';
import { getEntityState } from '$app/common/helpers';
import { useSocketEvent } from '$app/common/queries/sockets';
import { $refetch } from '$app/common/hooks/useRefetch';

export default function Payments() {
useTitle('payments');
Expand Down Expand Up @@ -84,6 +86,11 @@ export default function Payments() {
changeTemplateResources,
} = useChangeTemplate();

useSocketEvent({
on: 'App\\Events\\Payment\\PaymentWasUpdated',
callback: () => $refetch(['payments']),
});

return (
<Default title={t('payments')} breadcrumbs={pages} docsLink="en/payments/">
<DataTable
Expand Down
Loading