From a27c48547f4be94f7f28303da0255457476abf60 Mon Sep 17 00:00:00 2001 From: Civolilah Date: Mon, 22 Jul 2024 15:57:55 +0200 Subject: [PATCH 01/36] Implementing cards on the dashboard --- src/common/interfaces/company-user.ts | 28 ++ src/components/DropdownDateRangePicker.tsx | 4 +- .../components/DashboardCardSelector.tsx | 292 ++++++++++++++++++ src/pages/dashboard/components/Totals.tsx | 23 +- 4 files changed, 334 insertions(+), 13 deletions(-) create mode 100644 src/pages/dashboard/components/DashboardCardSelector.tsx diff --git a/src/common/interfaces/company-user.ts b/src/common/interfaces/company-user.ts index 39833652fc..e89ec91f59 100644 --- a/src/common/interfaces/company-user.ts +++ b/src/common/interfaces/company-user.ts @@ -32,10 +32,38 @@ export interface CompanyUser { react_settings: ReactSettings; } +type Format = 'time' | 'money'; +export type Period = 'current' | 'previous' | 'total'; +export type Calculate = 'sum' | 'avg' | 'count'; +export type Field = + | 'active_invoices' + | 'outstanding_invoices' + | 'completed_payments' + | 'refunded_payments' + | 'active_quotes' + | 'unapproved_quotes' + | 'logged_tasks' + | 'invoiced_tasks' + | 'paid_tasks' + | 'logged_expenses' + | 'pending_expenses' + | 'invoiced_expenses' + | 'invoice_paid_expenses'; + +export interface DashboardField { + calculate: Calculate; + field: Field; + format: Format; + period: Period; +} + export interface Settings { accent_color: string; table_columns?: Record; react_table_columns?: Record; + dashboard_fields?: DashboardField[]; + dashboard_fields_per_row_desktop?: number; + dashboard_fields_per_row_mobile?: number; } export interface Notifications { diff --git a/src/components/DropdownDateRangePicker.tsx b/src/components/DropdownDateRangePicker.tsx index 5c3c2277a7..50f8b438aa 100644 --- a/src/components/DropdownDateRangePicker.tsx +++ b/src/components/DropdownDateRangePicker.tsx @@ -71,8 +71,8 @@ export function DropdownDateRangePicker(props: Props) { const accentColor = useAccentColor(); return ( -
- +
+ ([]); + + const [currentField, setCurrentField] = useState({ + field: '' as Field, + period: 'current', + calculate: 'sum', + format: 'time', + }); + + const [isCardsModalOpen, setIsCardsModalOpen] = useState(false); + const [isFieldsModalOpen, setIsFieldsModalOpen] = useState(false); + + const handleCardsModalClose = () => { + setIsCardsModalOpen(false); + }; + + const handleFieldsModalClose = () => { + setIsFieldsModalOpen(false); + + setCurrentField({ + field: '' as Field, + period: 'current', + calculate: 'sum', + format: 'time', + }); + }; + + const onDragEnd = (result: DropResult) => { + const sorted = arrayMoveImmutable( + currentFields, + result.source.index, + result.destination?.index as unknown as number + ); + + setCurrentFields(sorted); + }; + + const handleDelete = (fieldKey: Field) => { + const updatedCurrentColumns = currentFields.filter( + (field) => field.field !== fieldKey + ); + + setCurrentFields(updatedCurrentColumns); + }; + + return ( + <> +
setIsCardsModalOpen(true)} + > + +
+ + +
+ + { + const dashboardField = currentFields[rubric.source.index]; + + return ( +
+
+ + +
+

{t(dashboardField.field)}

+ +
+ {t(dashboardField.period)} + · + {t(dashboardField.calculate)} +
+
+
+ +
+ +
+
+ ); + }} + > + {(provided) => ( +
+ {currentFields.map((field, index) => ( + + {(provided) => ( +
+
+ handleDelete(field.field)} + /> + +
+

{t(field.field)}

+ +
+ {t(field.period)} + · + {t(field.calculate)} +
+
+
+ +
+ +
+
+ )} +
+ ))} + + {provided.placeholder} +
+ )} +
+
+ + + + + + + + + + + + + + + +
+
+ + +
+ + setCurrentField((currentField) => ({ + ...currentField, + field: value as Field, + })) + } + withBlank + > + {FIELDS.map((field) => ( + + ))} + + + + setCurrentField((currentField) => ({ + ...currentField, + period: value as Period, + })) + } + > + + + + + + + setCurrentField((currentField) => ({ + ...currentField, + calculate: value as Calculate, + })) + } + > + + + + + + +
+
+ + ); +} diff --git a/src/pages/dashboard/components/Totals.tsx b/src/pages/dashboard/components/Totals.tsx index 7e24d4b842..527f5de347 100644 --- a/src/pages/dashboard/components/Totals.tsx +++ b/src/pages/dashboard/components/Totals.tsx @@ -30,6 +30,7 @@ import collect from 'collect.js'; import { useColorScheme } from '$app/common/colors'; import { CurrencySelector } from '$app/components/CurrencySelector'; import { useQuery } from 'react-query'; +import { DashboardCardSelector } from './DashboardCardSelector'; interface TotalsRecord { revenue: { paid_to_date: string; code: string }; @@ -252,17 +253,17 @@ export function Totals() {
-
- - update('preferences.dashboard_charts.range', value) - } - value={body.date_range} - /> -
+ + update('preferences.dashboard_charts.range', value) + } + value={body.date_range} + /> + + Date: Mon, 22 Jul 2024 21:24:24 +0200 Subject: [PATCH 02/36] Implemented saving dashboard fields --- .../components/DashboardCardSelector.tsx | 81 +++++++++++++++---- 1 file changed, 66 insertions(+), 15 deletions(-) diff --git a/src/pages/dashboard/components/DashboardCardSelector.tsx b/src/pages/dashboard/components/DashboardCardSelector.tsx index f0adbc8832..90b0ec075f 100644 --- a/src/pages/dashboard/components/DashboardCardSelector.tsx +++ b/src/pages/dashboard/components/DashboardCardSelector.tsx @@ -1,4 +1,3 @@ -/* eslint-disable @typescript-eslint/no-unused-vars */ /** * Invoice Ninja (https://invoiceninja.com). * @@ -9,14 +8,20 @@ * @license https://www.elastic.co/licensing/elastic-license */ +import { endpoint } from '$app/common/helpers'; +import { request } from '$app/common/helpers/request'; +import { toast } from '$app/common/helpers/toast/toast'; import { useCurrentUser } from '$app/common/hooks/useCurrentUser'; -import { useInjectUserChanges } from '$app/common/hooks/useInjectUserChanges'; +import { $refetch } from '$app/common/hooks/useRefetch'; import { Calculate, + CompanyUser, DashboardField, Field, Period, } from '$app/common/interfaces/company-user'; +import { GenericSingleResourceResponse } from '$app/common/interfaces/generic-api-response'; +import { User } from '$app/common/interfaces/user'; import { Button, SelectField } from '$app/components/forms'; import { Icon } from '$app/components/icons/Icon'; import { Modal } from '$app/components/Modal'; @@ -27,10 +32,13 @@ import { DropResult, } from '@hello-pangea/dnd'; import { arrayMoveImmutable } from 'array-move'; -import { useState } from 'react'; +import { cloneDeep, set } from 'lodash'; +import { useEffect, useState } from 'react'; import { useTranslation } from 'react-i18next'; import { CgOptions } from 'react-icons/cg'; import { MdClose, MdDragHandle } from 'react-icons/md'; +import { useDispatch } from 'react-redux'; +import { updateUser } from '$app/common/stores/slices/user'; const FIELDS = [ 'active_invoices', @@ -50,13 +58,11 @@ const FIELDS = [ export function DashboardCardSelector() { const [t] = useTranslation(); + const dispatch = useDispatch(); const currentUser = useCurrentUser(); - const userChanges = useInjectUserChanges(); - const [currentFields, setCurrentFields] = useState([]); - const [currentField, setCurrentField] = useState({ field: '' as Field, period: 'current', @@ -64,6 +70,7 @@ export function DashboardCardSelector() { format: 'time', }); + const [isFormBusy, setIsFormBusy] = useState(false); const [isCardsModalOpen, setIsCardsModalOpen] = useState(false); const [isFieldsModalOpen, setIsFieldsModalOpen] = useState(false); @@ -100,6 +107,43 @@ export function DashboardCardSelector() { setCurrentFields(updatedCurrentColumns); }; + const handleSaveCards = () => { + const updatedUser = cloneDeep(currentUser) as User; + + if (updatedUser && !isFormBusy) { + toast.processing(); + setIsFormBusy(true); + + set(updatedUser, 'company_user.settings.dashboard_fields', currentFields); + + request( + 'PUT', + endpoint('/api/v1/company_users/:id', { id: updatedUser.id }), + updatedUser + ) + .then((response: GenericSingleResourceResponse) => { + toast.success('updated_settings'); + + set(updatedUser, 'company_user', response.data.data); + + $refetch(['company_users']); + + dispatch(updateUser(updatedUser)); + + handleCardsModalClose(); + }) + .finally(() => setIsFormBusy(false)); + } + }; + + useEffect(() => { + if (currentUser && Object.keys(currentUser).length && isCardsModalOpen) { + setCurrentFields( + currentUser.company_user?.settings.dashboard_fields ?? [] + ); + } + }, [currentUser, isCardsModalOpen]); + return ( <>
- +

{t(dashboardField.field)}

-
+
{t(dashboardField.period)} · {t(dashboardField.calculate)} @@ -143,7 +187,7 @@ export function DashboardCardSelector() {
- +
); @@ -167,14 +211,14 @@ export function DashboardCardSelector() { handleDelete(field.field)} />

{t(field.field)}

-
+
{t(field.period)} · {t(field.calculate)} @@ -183,7 +227,7 @@ export function DashboardCardSelector() {
- +
)} @@ -204,7 +248,7 @@ export function DashboardCardSelector() { {t('add_field')} - + {/* @@ -213,9 +257,16 @@ export function DashboardCardSelector() { - + */} - +
From f60902cf38dfda8868b6c492c3f1c4af66cb52cb Mon Sep 17 00:00:00 2001 From: Civolilah Date: Sun, 28 Jul 2024 20:05:06 +0200 Subject: [PATCH 03/36] Implementing cards on the dashboard --- .../components/DashboardCardSelector.tsx | 30 ++++- .../dashboard/components/DashboardCards.tsx | 113 ++++++++++++++++++ src/pages/dashboard/components/Totals.tsx | 7 ++ 3 files changed, 146 insertions(+), 4 deletions(-) create mode 100644 src/pages/dashboard/components/DashboardCards.tsx diff --git a/src/pages/dashboard/components/DashboardCardSelector.tsx b/src/pages/dashboard/components/DashboardCardSelector.tsx index 90b0ec075f..3eff9abc1d 100644 --- a/src/pages/dashboard/components/DashboardCardSelector.tsx +++ b/src/pages/dashboard/components/DashboardCardSelector.tsx @@ -56,6 +56,22 @@ const FIELDS = [ 'invoice_paid_expenses', ]; +export const FIELDS_LABELS = { + active_invoices: 'total_active_invoices', + outstanding_invoices: 'total_outstanding_invoices', + completed_payments: 'total_completed_payments', + refunded_payments: 'total_refunded_payments', + active_quotes: 'total_active_quotes', + unapproved_quotes: 'total_unapproved_quotes', + logged_tasks: 'total_logged_tasks', + invoiced_tasks: 'total_invoiced_tasks', + paid_tasks: 'total_paid_tasks', + logged_expenses: 'total_logged_expenses', + pending_expenses: 'total_pending_expenses', + invoiced_expenses: 'total_invoiced_expenses', + invoice_paid_expenses: 'total_invoice_paid_expenses', +}; + export function DashboardCardSelector() { const [t] = useTranslation(); const dispatch = useDispatch(); @@ -67,7 +83,7 @@ export function DashboardCardSelector() { field: '' as Field, period: 'current', calculate: 'sum', - format: 'time', + format: 'money', }); const [isFormBusy, setIsFormBusy] = useState(false); @@ -160,6 +176,12 @@ export function DashboardCardSelector() { disableClosing={isFieldsModalOpen} >
+ {!currentFields.length && ( + + {t('no_records_found')} + + )} +
-

{t(dashboardField.field)}

+

{t(FIELDS_LABELS[dashboardField.field])}

{t(dashboardField.period)} @@ -216,7 +238,7 @@ export function DashboardCardSelector() { />
-

{t(field.field)}

+

{t(FIELDS_LABELS[field.field])}

{t(field.period)} @@ -290,7 +312,7 @@ export function DashboardCardSelector() { > {FIELDS.map((field) => ( ))} diff --git a/src/pages/dashboard/components/DashboardCards.tsx b/src/pages/dashboard/components/DashboardCards.tsx new file mode 100644 index 0000000000..97c8ba14ff --- /dev/null +++ b/src/pages/dashboard/components/DashboardCards.tsx @@ -0,0 +1,113 @@ +/* eslint-disable @typescript-eslint/no-unused-vars */ +/** + * Invoice Ninja (https://invoiceninja.com). + * + * @link https://github.com/invoiceninja/invoiceninja source repository + * + * @copyright Copyright (c) 2022. Invoice Ninja LLC (https://invoiceninja.com) + * + * @license https://www.elastic.co/licensing/elastic-license + */ + +import { useFormatMoney } from '$app/common/hooks/money/useFormatMoney'; +import { useCurrentUser } from '$app/common/hooks/useCurrentUser'; +import { DashboardField } from '$app/common/interfaces/company-user'; +import { useEffect, useState } from 'react'; +import { useTranslation } from 'react-i18next'; +import { FIELDS_LABELS } from './DashboardCardSelector'; +import { Card as CardElement } from '$app/components/cards'; +import { useQueryClient } from 'react-query'; +import { request } from '$app/common/helpers/request'; +import { endpoint } from '$app/common/helpers'; +import { Spinner } from '$app/components/Spinner'; + +interface DashboardCardsProps { + dateRange: string; + startDate: string; + endDate: string; +} + +interface CardProps extends DashboardCardsProps { + field: DashboardField; +} + +function Card(props: CardProps) { + const [t] = useTranslation(); + + const { dateRange, startDate, endDate, field } = props; + + const queryClient = useQueryClient(); + const formatMoney = useFormatMoney(); + + const [isFormBusy, setIsFormBusy] = useState(false); + + useEffect(() => { + setIsFormBusy(true); + + queryClient.fetchQuery([`/api/v1/charts/calculated_fields`], () => + request('POST', endpoint('/api/v1/charts/calculated_fields'), { + date_range: dateRange, + start_date: startDate, + end_date: endDate, + field: field.field, + calculation: field.calculate, + period: field.period, + format: field.format, + }) + .then((response) => { + console.log(response); + }) + .finally(() => setIsFormBusy(false)) + ); + }, [field]); + + return ( + + {isFormBusy && ( +
+ +
+ )} + + {!isFormBusy && ( +
+ {t(FIELDS_LABELS[field.field])} + + {/* {field.format === 'money' && ( + {formatMoney(field.value)} + )} */} +
+ )} +
+ ); +} + +export function DashboardCards(props: DashboardCardsProps) { + const { dateRange, startDate, endDate } = props; + + const currentUser = useCurrentUser(); + + const [currentFields, setCurrentFields] = useState([]); + + useEffect(() => { + if (currentUser && Object.keys(currentUser).length) { + setCurrentFields( + currentUser.company_user?.settings.dashboard_fields ?? [] + ); + } + }, [currentUser]); + + return ( +
+ {currentFields.map((field, index) => ( + + ))} +
+ ); +} diff --git a/src/pages/dashboard/components/Totals.tsx b/src/pages/dashboard/components/Totals.tsx index 527f5de347..feb491d112 100644 --- a/src/pages/dashboard/components/Totals.tsx +++ b/src/pages/dashboard/components/Totals.tsx @@ -31,6 +31,7 @@ import { useColorScheme } from '$app/common/colors'; import { CurrencySelector } from '$app/components/CurrencySelector'; import { useQuery } from 'react-query'; import { DashboardCardSelector } from './DashboardCardSelector'; +import { DashboardCards } from './DashboardCards'; interface TotalsRecord { revenue: { paid_to_date: string; code: string }; @@ -310,6 +311,12 @@ export function Totals() {
+ +
{company && ( Date: Mon, 29 Jul 2024 01:35:42 +0200 Subject: [PATCH 04/36] Implemented basic displaying card on the dashboard --- src/common/hooks/useRefetch.tsx | 3 +- .../components/DashboardCardSelector.tsx | 33 ++++++++++++++++--- .../dashboard/components/DashboardCards.tsx | 28 +++++++++++----- 3 files changed, 50 insertions(+), 14 deletions(-) diff --git a/src/common/hooks/useRefetch.tsx b/src/common/hooks/useRefetch.tsx index f221ee0d40..c1106461e7 100644 --- a/src/common/hooks/useRefetch.tsx +++ b/src/common/hooks/useRefetch.tsx @@ -142,11 +142,12 @@ export const keys = { '/api/v1/payments', '/api/v1/expenses', '/api/v1/tasks', + '/api/v1/charts/calculated_fields', ], }, company_users: { path: '/api/v1/company_users', - dependencies: [], + dependencies: ['/api/v1/charts/calculated_fields'], }, clients: { path: '/api/v1/clients', diff --git a/src/pages/dashboard/components/DashboardCardSelector.tsx b/src/pages/dashboard/components/DashboardCardSelector.tsx index 3eff9abc1d..a2ee76c6e8 100644 --- a/src/pages/dashboard/components/DashboardCardSelector.tsx +++ b/src/pages/dashboard/components/DashboardCardSelector.tsx @@ -39,6 +39,7 @@ import { CgOptions } from 'react-icons/cg'; import { MdClose, MdDragHandle } from 'react-icons/md'; import { useDispatch } from 'react-redux'; import { updateUser } from '$app/common/stores/slices/user'; +import { PERIOD_LABELS } from './DashboardCards'; const FIELDS = [ 'active_invoices', @@ -201,9 +202,21 @@ export function DashboardCardSelector() {

{t(FIELDS_LABELS[dashboardField.field])}

- {t(dashboardField.period)} + + {t( + PERIOD_LABELS[ + dashboardField.period as keyof typeof PERIOD_LABELS + ] ?? dashboardField.period + )} + · - {t(dashboardField.calculate)} + + {t( + dashboardField.calculate === 'avg' + ? 'average' + : dashboardField.calculate + )} +
@@ -241,9 +254,21 @@ export function DashboardCardSelector() {

{t(FIELDS_LABELS[field.field])}

- {t(field.period)} + + {t( + PERIOD_LABELS[ + field.period as keyof typeof PERIOD_LABELS + ] ?? field.period + )} + · - {t(field.calculate)} + + {t( + field.calculate === 'avg' + ? 'average' + : field.calculate + )} +
diff --git a/src/pages/dashboard/components/DashboardCards.tsx b/src/pages/dashboard/components/DashboardCards.tsx index 97c8ba14ff..c3bb8bf1b3 100644 --- a/src/pages/dashboard/components/DashboardCards.tsx +++ b/src/pages/dashboard/components/DashboardCards.tsx @@ -1,4 +1,3 @@ -/* eslint-disable @typescript-eslint/no-unused-vars */ /** * Invoice Ninja (https://invoiceninja.com). * @@ -31,6 +30,11 @@ interface CardProps extends DashboardCardsProps { field: DashboardField; } +export const PERIOD_LABELS = { + current: 'current_period', + previous: 'previous_period', +}; + function Card(props: CardProps) { const [t] = useTranslation(); @@ -40,11 +44,12 @@ function Card(props: CardProps) { const formatMoney = useFormatMoney(); const [isFormBusy, setIsFormBusy] = useState(false); + const [responseData, setResponseData] = useState(0); useEffect(() => { setIsFormBusy(true); - queryClient.fetchQuery([`/api/v1/charts/calculated_fields`], () => + queryClient.fetchQuery(['/api/v1/charts/calculated_fields'], () => request('POST', endpoint('/api/v1/charts/calculated_fields'), { date_range: dateRange, start_date: startDate, @@ -54,9 +59,7 @@ function Card(props: CardProps) { period: field.period, format: field.format, }) - .then((response) => { - console.log(response); - }) + .then((response) => setResponseData(response.data)) .finally(() => setIsFormBusy(false)) ); }, [field]); @@ -70,12 +73,19 @@ function Card(props: CardProps) { )} {!isFormBusy && ( -
+
{t(FIELDS_LABELS[field.field])} - {/* {field.format === 'money' && ( - {formatMoney(field.value)} - )} */} + {field.format === 'money' && ( + {formatMoney(responseData, '', '')} + )} + + + {t( + PERIOD_LABELS[field.period as keyof typeof PERIOD_LABELS] ?? + field.period + )} +
)} From 9351879cf8723201d2abe08d005a394b4ea6081f Mon Sep 17 00:00:00 2001 From: Civolilah Date: Tue, 6 Aug 2024 18:49:56 +0200 Subject: [PATCH 05/36] Added currency_id in the payload for cards calculation endpoint --- src/pages/dashboard/components/DashboardCards.tsx | 7 +++++-- src/pages/dashboard/components/Totals.tsx | 1 + 2 files changed, 6 insertions(+), 2 deletions(-) diff --git a/src/pages/dashboard/components/DashboardCards.tsx b/src/pages/dashboard/components/DashboardCards.tsx index c3bb8bf1b3..daf7b2a8e6 100644 --- a/src/pages/dashboard/components/DashboardCards.tsx +++ b/src/pages/dashboard/components/DashboardCards.tsx @@ -24,6 +24,7 @@ interface DashboardCardsProps { dateRange: string; startDate: string; endDate: string; + currencyId: string; } interface CardProps extends DashboardCardsProps { @@ -38,7 +39,7 @@ export const PERIOD_LABELS = { function Card(props: CardProps) { const [t] = useTranslation(); - const { dateRange, startDate, endDate, field } = props; + const { dateRange, startDate, endDate, field, currencyId } = props; const queryClient = useQueryClient(); const formatMoney = useFormatMoney(); @@ -58,6 +59,7 @@ function Card(props: CardProps) { calculation: field.calculate, period: field.period, format: field.format, + currency_id: currencyId, }) .then((response) => setResponseData(response.data)) .finally(() => setIsFormBusy(false)) @@ -93,7 +95,7 @@ function Card(props: CardProps) { } export function DashboardCards(props: DashboardCardsProps) { - const { dateRange, startDate, endDate } = props; + const { dateRange, startDate, endDate, currencyId } = props; const currentUser = useCurrentUser(); @@ -116,6 +118,7 @@ export function DashboardCards(props: DashboardCardsProps) { dateRange={dateRange} startDate={startDate} endDate={endDate} + currencyId={currencyId} /> ))}
diff --git a/src/pages/dashboard/components/Totals.tsx b/src/pages/dashboard/components/Totals.tsx index feb491d112..f5a65973ac 100644 --- a/src/pages/dashboard/components/Totals.tsx +++ b/src/pages/dashboard/components/Totals.tsx @@ -315,6 +315,7 @@ export function Totals() { dateRange={dateRange} startDate={dates.start_date} endDate={dates.end_date} + currencyId={currency.toString()} />
From 58e7ef3e6ef149c88983ba2e61cd165d62c00467 Mon Sep 17 00:00:00 2001 From: Civolilah Date: Fri, 9 Aug 2024 10:01:04 +0200 Subject: [PATCH 06/36] Implementing grid resize system on the dashboard --- package-lock.json | 81 ++++++++ package.json | 2 + src/pages/dashboard/Dashboard.tsx | 59 ++---- src/pages/dashboard/components/Activity.tsx | 7 +- .../dashboard/components/DashboardCards.tsx | 29 +-- .../dashboard/components/ExpiredQuotes.tsx | 3 +- .../dashboard/components/PastDueInvoices.tsx | 2 +- .../dashboard/components/RecentPayments.tsx | 2 +- .../dashboard/components/ResizableContent.tsx | 178 ++++++++++++++++++ src/pages/dashboard/components/Totals.tsx | 5 +- .../dashboard/components/UpcomingInvoices.tsx | 2 +- .../dashboard/components/UpcomingQuotes.tsx | 3 +- .../components/UpcomingRecurringInvoices.tsx | 9 +- src/resources/css/gridLayout.css | 4 + 14 files changed, 314 insertions(+), 72 deletions(-) create mode 100644 src/pages/dashboard/components/ResizableContent.tsx create mode 100644 src/resources/css/gridLayout.css diff --git a/package-lock.json b/package-lock.json index 4f2dfb8da7..eaccd100e2 100644 --- a/package-lock.json +++ b/package-lock.json @@ -47,6 +47,7 @@ "react-dom": "^18.2.0", "react-dropzone": "^14.2.3", "react-feather": "^2.0.10", + "react-grid-layout": "^1.4.4", "react-hot-toast": "^2.4.0", "react-i18next": "^12.1.1", "react-icons": "^4.7.1", @@ -83,6 +84,7 @@ "@types/react-date-range": "^1.4.4", "@types/react-datepicker": "^4.8.0", "@types/react-dom": "^18.0.9", + "@types/react-grid-layout": "^1.3.5", "@types/uuid": "^9.0.0", "@typescript-eslint/eslint-plugin": "^5.45.1", "@typescript-eslint/parser": "^5.45.1", @@ -2959,6 +2961,16 @@ "@types/react": "*" } }, + "node_modules/@types/react-grid-layout": { + "version": "1.3.5", + "resolved": "https://registry.npmjs.org/@types/react-grid-layout/-/react-grid-layout-1.3.5.tgz", + "integrity": "sha512-WH/po1gcEcoR6y857yAnPGug+ZhkF4PaTUxgAbwfeSH/QOgVSakKHBXoPGad/sEznmkiaK3pqHk+etdWisoeBQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/react": "*" + } + }, "node_modules/@types/react-transition-group": { "version": "4.4.8", "resolved": "https://registry.npmjs.org/@types/react-transition-group/-/react-transition-group-4.4.8.tgz", @@ -4133,6 +4145,15 @@ "node": ">=12" } }, + "node_modules/clsx": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/clsx/-/clsx-2.1.1.tgz", + "integrity": "sha512-eYm0QWBtUrBWZWG0d386OGAw16Z995PiOVo2B7bjWSbHedGl5e0ZWaq65kOGgUSNesEIDkB9ISbTg/JK9dhCZA==", + "license": "MIT", + "engines": { + "node": ">=6" + } + }, "node_modules/co": { "version": "4.6.0", "resolved": "https://registry.npmjs.org/co/-/co-4.6.0.tgz", @@ -10068,6 +10089,29 @@ "react": "^18.2.0" } }, + "node_modules/react-draggable": { + "version": "4.4.6", + "resolved": "https://registry.npmjs.org/react-draggable/-/react-draggable-4.4.6.tgz", + "integrity": "sha512-LtY5Xw1zTPqHkVmtM3X8MUOxNDOUhv/khTgBgrUvwaS064bwVvxT+q5El0uUFNx5IEPKXuRejr7UqLwBIg5pdw==", + "license": "MIT", + "dependencies": { + "clsx": "^1.1.1", + "prop-types": "^15.8.1" + }, + "peerDependencies": { + "react": ">= 16.3.0", + "react-dom": ">= 16.3.0" + } + }, + "node_modules/react-draggable/node_modules/clsx": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/clsx/-/clsx-1.2.1.tgz", + "integrity": "sha512-EcR6r5a8bj6pu3ycsa/E/cKVGuTgZJZdsyUYHOksG/UHIiKfjxzRxYJpyVBwYaQeOvghal9fcc4PidlgzugAQg==", + "license": "MIT", + "engines": { + "node": ">=6" + } + }, "node_modules/react-dropzone": { "version": "14.2.3", "resolved": "https://registry.npmjs.org/react-dropzone/-/react-dropzone-14.2.3.tgz", @@ -10100,6 +10144,30 @@ "react": ">=16.8.6" } }, + "node_modules/react-grid-layout": { + "version": "1.4.4", + "resolved": "https://registry.npmjs.org/react-grid-layout/-/react-grid-layout-1.4.4.tgz", + "integrity": "sha512-7+Lg8E8O8HfOH5FrY80GCIR1SHTn2QnAYKh27/5spoz+OHhMmEhU/14gIkRzJOtympDPaXcVRX/nT1FjmeOUmQ==", + "license": "MIT", + "dependencies": { + "clsx": "^2.0.0", + "fast-equals": "^4.0.3", + "prop-types": "^15.8.1", + "react-draggable": "^4.4.5", + "react-resizable": "^3.0.5", + "resize-observer-polyfill": "^1.5.1" + }, + "peerDependencies": { + "react": ">= 16.3.0", + "react-dom": ">= 16.3.0" + } + }, + "node_modules/react-grid-layout/node_modules/fast-equals": { + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/fast-equals/-/fast-equals-4.0.3.tgz", + "integrity": "sha512-G3BSX9cfKttjr+2o1O22tYMLq0DPluZnYtq1rXumE1SpL/F/SLIfHx08WYQoWSIpeMYf8sRbJ8++71+v6Pnxfg==", + "license": "MIT" + }, "node_modules/react-hot-toast": { "version": "2.4.1", "resolved": "https://registry.npmjs.org/react-hot-toast/-/react-hot-toast-2.4.1.tgz", @@ -10322,6 +10390,19 @@ "node": ">=0.10.0" } }, + "node_modules/react-resizable": { + "version": "3.0.5", + "resolved": "https://registry.npmjs.org/react-resizable/-/react-resizable-3.0.5.tgz", + "integrity": "sha512-vKpeHhI5OZvYn82kXOs1bC8aOXktGU5AmKAgaZS4F5JPburCtbmDPqE7Pzp+1kN4+Wb81LlF33VpGwWwtXem+w==", + "license": "MIT", + "dependencies": { + "prop-types": "15.x", + "react-draggable": "^4.0.3" + }, + "peerDependencies": { + "react": ">= 16.3" + } + }, "node_modules/react-resizable-panels": { "version": "2.0.12", "resolved": "https://registry.npmjs.org/react-resizable-panels/-/react-resizable-panels-2.0.12.tgz", diff --git a/package.json b/package.json index 9c22fcdfcc..9b2c61fd6b 100644 --- a/package.json +++ b/package.json @@ -42,6 +42,7 @@ "react-dom": "^18.2.0", "react-dropzone": "^14.2.3", "react-feather": "^2.0.10", + "react-grid-layout": "^1.4.4", "react-hot-toast": "^2.4.0", "react-i18next": "^12.1.1", "react-icons": "^4.7.1", @@ -100,6 +101,7 @@ "@types/react-date-range": "^1.4.4", "@types/react-datepicker": "^4.8.0", "@types/react-dom": "^18.0.9", + "@types/react-grid-layout": "^1.3.5", "@types/uuid": "^9.0.0", "@typescript-eslint/eslint-plugin": "^5.45.1", "@typescript-eslint/parser": "^5.45.1", diff --git a/src/pages/dashboard/Dashboard.tsx b/src/pages/dashboard/Dashboard.tsx index 4a5222d948..bc029dd682 100644 --- a/src/pages/dashboard/Dashboard.tsx +++ b/src/pages/dashboard/Dashboard.tsx @@ -1,3 +1,4 @@ +/* eslint-disable @typescript-eslint/no-unused-vars */ /** * Invoice Ninja (https://invoiceninja.com). * @@ -9,18 +10,20 @@ */ import { useTitle } from '$app/common/hooks/useTitle'; -import { Activity } from '$app/pages/dashboard/components/Activity'; -import { PastDueInvoices } from '$app/pages/dashboard/components/PastDueInvoices'; -import { RecentPayments } from '$app/pages/dashboard/components/RecentPayments'; import { Totals } from '$app/pages/dashboard/components/Totals'; -import { UpcomingInvoices } from '$app/pages/dashboard/components/UpcomingInvoices'; import { useTranslation } from 'react-i18next'; import { Default } from '../../components/layouts/Default'; -import { ExpiredQuotes } from './components/ExpiredQuotes'; -import { UpcomingQuotes } from './components/UpcomingQuotes'; import { useEnabled } from '$app/common/guards/guards/enabled'; -import { ModuleBitmask } from '../settings'; -import { UpcomingRecurringInvoices } from './components/UpcomingRecurringInvoices'; +import GridLayoutComponent from './components/ResizableContent'; + +interface GridItem { + i: string; + x: number; + y: number; + w: number; + h: number; + content: string; +} export default function Dashboard() { const [t] = useTranslation(); @@ -32,45 +35,7 @@ export default function Dashboard() { -
-
- -
- -
- -
- - {enabled(ModuleBitmask.Invoices) && ( -
- -
- )} - - {enabled(ModuleBitmask.Invoices) && ( -
- -
- )} - - {enabled(ModuleBitmask.Quotes) && ( -
- -
- )} - - {enabled(ModuleBitmask.Quotes) && ( -
- -
- )} - - {enabled(ModuleBitmask.RecurringInvoices) && ( -
- -
- )} -
+
); } diff --git a/src/pages/dashboard/components/Activity.tsx b/src/pages/dashboard/components/Activity.tsx index be285cb467..48c0cde02f 100644 --- a/src/pages/dashboard/components/Activity.tsx +++ b/src/pages/dashboard/components/Activity.tsx @@ -33,7 +33,8 @@ export function Activity() { return ( {isLoading && ( @@ -46,10 +47,10 @@ export function Activity() { {t('error_refresh_page')} )} -
+
{data?.data.data && data.data.data.map((record: ActivityRecord, index: number) => ( diff --git a/src/pages/dashboard/components/DashboardCards.tsx b/src/pages/dashboard/components/DashboardCards.tsx index daf7b2a8e6..c0ac072ab2 100644 --- a/src/pages/dashboard/components/DashboardCards.tsx +++ b/src/pages/dashboard/components/DashboardCards.tsx @@ -20,6 +20,9 @@ import { request } from '$app/common/helpers/request'; import { endpoint } from '$app/common/helpers'; import { Spinner } from '$app/components/Spinner'; +import 'react-grid-layout/css/styles.css'; +import 'react-resizable/css/styles.css'; + interface DashboardCardsProps { dateRange: string; startDate: string; @@ -110,17 +113,19 @@ export function DashboardCards(props: DashboardCardsProps) { }, [currentUser]); return ( -
- {currentFields.map((field, index) => ( - - ))} -
+ <> +
+ {currentFields.map((field, index) => ( + + ))} +
+ ); } diff --git a/src/pages/dashboard/components/ExpiredQuotes.tsx b/src/pages/dashboard/components/ExpiredQuotes.tsx index 94878d9203..f38d21bc58 100644 --- a/src/pages/dashboard/components/ExpiredQuotes.tsx +++ b/src/pages/dashboard/components/ExpiredQuotes.tsx @@ -73,9 +73,10 @@ export function ExpiredQuotes() { return (
diff --git a/src/pages/dashboard/components/RecentPayments.tsx b/src/pages/dashboard/components/RecentPayments.tsx index 054b23b3b2..bbfe736a47 100644 --- a/src/pages/dashboard/components/RecentPayments.tsx +++ b/src/pages/dashboard/components/RecentPayments.tsx @@ -94,7 +94,7 @@ export function RecentPayments() { return (
diff --git a/src/pages/dashboard/components/ResizableContent.tsx b/src/pages/dashboard/components/ResizableContent.tsx new file mode 100644 index 0000000000..fcfe78e016 --- /dev/null +++ b/src/pages/dashboard/components/ResizableContent.tsx @@ -0,0 +1,178 @@ +import React, { useState, useEffect, useRef } from 'react'; +import GridLayout from 'react-grid-layout'; +import 'react-grid-layout/css/styles.css'; +import 'react-resizable/css/styles.css'; +import '$app/resources/css/gridLayout.css'; + +import { Activity } from '$app/pages/dashboard/components/Activity'; +import { PastDueInvoices } from '$app/pages/dashboard/components/PastDueInvoices'; +import { RecentPayments } from '$app/pages/dashboard/components/RecentPayments'; +import { UpcomingInvoices } from '$app/pages/dashboard/components/UpcomingInvoices'; +import { ModuleBitmask } from '$app/pages/settings'; +import { ExpiredQuotes } from './ExpiredQuotes'; +import { UpcomingQuotes } from './UpcomingQuotes'; +import { UpcomingRecurringInvoices } from './UpcomingRecurringInvoices'; +import { useEnabled } from '$app/common/guards/guards/enabled'; + +const GridLayoutComponent = () => { + const enabled = useEnabled(); + const [width, setWidth] = useState(1000); + const [columns, setColumns] = useState(4); + const containerRef = useRef(null); + + useEffect(() => { + const resizeObserver = new ResizeObserver((entries) => { + if (entries[0]) { + setWidth(entries[0].contentRect.width); + } + }); + + if (containerRef.current) { + resizeObserver.observe(containerRef.current); + } + + return () => { + if (containerRef.current) { + resizeObserver.unobserve(containerRef.current); + } + }; + }, []); + + const handleColumnsChange = (event: React.ChangeEvent) => { + setColumns(parseInt(event.target.value)); + }; + + return ( +
+
+ + +
+ + +
+ +
+ +
+ +
+ + {enabled(ModuleBitmask.Invoices) && ( +
+ +
+ )} + + {enabled(ModuleBitmask.Invoices) && ( +
+ +
+ )} + + {enabled(ModuleBitmask.Quotes) && ( +
+ +
+ )} + + {enabled(ModuleBitmask.Quotes) && ( +
+ +
+ )} + + {enabled(ModuleBitmask.RecurringInvoices) && ( +
+ +
+ )} +
+
+ ); +}; + +export default GridLayoutComponent; diff --git a/src/pages/dashboard/components/Totals.tsx b/src/pages/dashboard/components/Totals.tsx index f5a65973ac..4e8ea910fa 100644 --- a/src/pages/dashboard/components/Totals.tsx +++ b/src/pages/dashboard/components/Totals.tsx @@ -31,7 +31,6 @@ import { useColorScheme } from '$app/common/colors'; import { CurrencySelector } from '$app/components/CurrencySelector'; import { useQuery } from 'react-query'; import { DashboardCardSelector } from './DashboardCardSelector'; -import { DashboardCards } from './DashboardCards'; interface TotalsRecord { revenue: { paid_to_date: string; code: string }; @@ -311,12 +310,12 @@ export function Totals() {
- + /> */}
{company && ( diff --git a/src/pages/dashboard/components/UpcomingInvoices.tsx b/src/pages/dashboard/components/UpcomingInvoices.tsx index dd8740cc21..fdd9fc0ede 100644 --- a/src/pages/dashboard/components/UpcomingInvoices.tsx +++ b/src/pages/dashboard/components/UpcomingInvoices.tsx @@ -84,7 +84,7 @@ export function UpcomingInvoices() { return (
diff --git a/src/pages/dashboard/components/UpcomingQuotes.tsx b/src/pages/dashboard/components/UpcomingQuotes.tsx index be75063863..7be7bd1518 100644 --- a/src/pages/dashboard/components/UpcomingQuotes.tsx +++ b/src/pages/dashboard/components/UpcomingQuotes.tsx @@ -73,9 +73,10 @@ export function UpcomingQuotes() { return (
Date: Mon, 19 Aug 2024 19:15:31 +0200 Subject: [PATCH 07/36] Implemented logic for resizing and dragging cards on the dashboard --- src/pages/dashboard/Dashboard.tsx | 26 +- src/pages/dashboard/components/Chart.tsx | 2 +- .../dashboard/components/ResizableContent.tsx | 49 +- .../components/ResizableDashboardCards.tsx | 697 ++++++++++++++++++ src/pages/dashboard/components/Totals.tsx | 437 ----------- 5 files changed, 719 insertions(+), 492 deletions(-) create mode 100644 src/pages/dashboard/components/ResizableDashboardCards.tsx delete mode 100644 src/pages/dashboard/components/Totals.tsx diff --git a/src/pages/dashboard/Dashboard.tsx b/src/pages/dashboard/Dashboard.tsx index bc029dd682..1dba817509 100644 --- a/src/pages/dashboard/Dashboard.tsx +++ b/src/pages/dashboard/Dashboard.tsx @@ -1,4 +1,3 @@ -/* eslint-disable @typescript-eslint/no-unused-vars */ /** * Invoice Ninja (https://invoiceninja.com). * @@ -10,32 +9,15 @@ */ import { useTitle } from '$app/common/hooks/useTitle'; -import { Totals } from '$app/pages/dashboard/components/Totals'; -import { useTranslation } from 'react-i18next'; import { Default } from '../../components/layouts/Default'; -import { useEnabled } from '$app/common/guards/guards/enabled'; -import GridLayoutComponent from './components/ResizableContent'; - -interface GridItem { - i: string; - x: number; - y: number; - w: number; - h: number; - content: string; -} +import { ResizableDashboardCards } from './components/ResizableDashboardCards'; export default function Dashboard() { - const [t] = useTranslation(); - useTitle('dashboard'); - - const enabled = useEnabled(); + const { documentTitle } = useTitle('dashboard'); return ( - - - - + + ); } diff --git a/src/pages/dashboard/components/Chart.tsx b/src/pages/dashboard/components/Chart.tsx index 927afee7cc..0a98ed3af4 100644 --- a/src/pages/dashboard/components/Chart.tsx +++ b/src/pages/dashboard/components/Chart.tsx @@ -12,7 +12,7 @@ import { useCurrentCompanyDateFormats } from '$app/common/hooks/useCurrentCompan import { useEffect, useMemo, useState } from 'react'; import { useTranslation } from 'react-i18next'; import { date as formatDate, useParseDayjs } from '$app/common/helpers'; -import { ChartData, TotalColors } from './Totals'; +import { ChartData, TotalColors } from './ResizableDashboardCards'; import { Line, CartesianGrid, diff --git a/src/pages/dashboard/components/ResizableContent.tsx b/src/pages/dashboard/components/ResizableContent.tsx index fcfe78e016..52beb8b654 100644 --- a/src/pages/dashboard/components/ResizableContent.tsx +++ b/src/pages/dashboard/components/ResizableContent.tsx @@ -16,8 +16,8 @@ import { useEnabled } from '$app/common/guards/guards/enabled'; const GridLayoutComponent = () => { const enabled = useEnabled(); + const [width, setWidth] = useState(1000); - const [columns, setColumns] = useState(4); const containerRef = useRef(null); useEffect(() => { @@ -38,36 +38,16 @@ const GridLayoutComponent = () => { }; }, []); - const handleColumnsChange = (event: React.ChangeEvent) => { - setColumns(parseInt(event.target.value)); - }; - return (
-
- - -
- - +
{ key="2" className="drag-handle" data-grid={{ - x: 6, + x: 12, y: 0, - w: 6, + w: 12, h: 2.2, isResizable: true, resizeHandles: ['s', 'w', 'e', 'n', 'sw', 'nw', 'se', 'ne'], @@ -98,9 +78,10 @@ const GridLayoutComponent = () => { data-grid={{ x: 0, y: 1, - w: 6, + w: 12, h: 2.2, isResizable: true, + resizeHandles: ['s', 'w', 'e', 'n', 'sw', 'nw', 'se', 'ne'], }} > @@ -112,11 +93,12 @@ const GridLayoutComponent = () => { key="4" className="drag-handle" data-grid={{ - x: 6, + x: 12, y: 1, - w: 6, + w: 12, h: 2.2, isResizable: true, + resizeHandles: ['s', 'w', 'e', 'n', 'sw', 'nw', 'se', 'ne'], }} > @@ -130,9 +112,10 @@ const GridLayoutComponent = () => { data-grid={{ x: 0, y: 2, - w: 6, + w: 12, h: 2.2, isResizable: true, + resizeHandles: ['s', 'w', 'e', 'n', 'sw', 'nw', 'se', 'ne'], }} > @@ -144,11 +127,12 @@ const GridLayoutComponent = () => { key="6" className="drag-handle" data-grid={{ - x: 6, + x: 12, y: 2, - w: 6, + w: 12, h: 2.2, isResizable: true, + resizeHandles: ['s', 'w', 'e', 'n', 'sw', 'nw', 'se', 'ne'], }} > @@ -162,9 +146,10 @@ const GridLayoutComponent = () => { data-grid={{ x: 0, y: 3, - w: 6, + w: 12, h: 2.2, isResizable: true, + resizeHandles: ['s', 'w', 'e', 'n', 'sw', 'nw', 'se', 'ne'], }} > diff --git a/src/pages/dashboard/components/ResizableDashboardCards.tsx b/src/pages/dashboard/components/ResizableDashboardCards.tsx new file mode 100644 index 0000000000..f905036b4b --- /dev/null +++ b/src/pages/dashboard/components/ResizableDashboardCards.tsx @@ -0,0 +1,697 @@ +/* eslint-disable @typescript-eslint/no-unused-vars */ +/** + * Invoice Ninja (https://invoiceninja.com). + * + * @link https://github.com/invoiceninja/invoiceninja source repository + * + * @copyright Copyright (c) 2022. Invoice Ninja LLC (https://invoiceninja.com) + * + * @license https://www.elastic.co/licensing/elastic-license + */ + +import 'react-grid-layout/css/styles.css'; +import 'react-resizable/css/styles.css'; +import '$app/resources/css/gridLayout.css'; +import { Button, SelectField } from '$app/components/forms'; +import { endpoint } from '$app/common/helpers'; +import { Chart } from '$app/pages/dashboard/components/Chart'; +import { useEffect, useRef, useState } from 'react'; +import { Spinner } from '$app/components/Spinner'; +import { DropdownDateRangePicker } from '../../../components/DropdownDateRangePicker'; +import { Card } from '$app/components/cards'; +import { useTranslation } from 'react-i18next'; +import { request } from '$app/common/helpers/request'; +import { useFormatMoney } from '$app/common/hooks/money/useFormatMoney'; +import { useCurrentCompany } from '$app/common/hooks/useCurrentCompany'; +import { useCurrentUser } from '$app/common/hooks/useCurrentUser'; +import { Badge } from '$app/components/Badge'; +import { + ChartsDefaultView, + useReactSettings, +} from '$app/common/hooks/useReactSettings'; +import { usePreferences } from '$app/common/hooks/usePreferences'; +import collect from 'collect.js'; +import { useColorScheme } from '$app/common/colors'; +import { CurrencySelector } from '$app/components/CurrencySelector'; +import { useQuery } from 'react-query'; +import { DashboardCardSelector } from './DashboardCardSelector'; +import GridLayout from 'react-grid-layout'; +import { Icon } from '$app/components/icons/Icon'; +import { BiMove } from 'react-icons/bi'; +import classNames from 'classnames'; +import { ModuleBitmask } from '$app/pages/settings'; +import { UpcomingQuotes } from './UpcomingQuotes'; +import { UpcomingRecurringInvoices } from './UpcomingRecurringInvoices'; +import { ExpiredQuotes } from './ExpiredQuotes'; +import { PastDueInvoices } from './PastDueInvoices'; +import { UpcomingInvoices } from './UpcomingInvoices'; +import { Activity } from './Activity'; +import { RecentPayments } from './RecentPayments'; +import { useEnabled } from '$app/common/guards/guards/enabled'; + +interface TotalsRecord { + revenue: { paid_to_date: string; code: string }; + expenses: { amount: string; code: string }; + invoices: { invoiced_amount: string; code: string; date: string }; + outstanding: { outstanding_count: number; amount: string; code: string }; +} + +interface Currency { + value: string; + label: string; +} + +export interface ChartData { + invoices: { + total: string; + date: string; + currency: string; + }[]; + payments: { + total: string; + date: string; + currency: string; + }[]; + outstanding: { + total: string; + date: string; + currency: string; + }[]; + expenses: { + total: string; + date: string; + currency: string; + }[]; +} + +interface GridItem { + i: string; + x: number; + y: number; + w: number; + h: number; + content: string; +} + +export enum TotalColors { + Green = '#54B434', + Blue = '#2596BE', + Red = '#BE4D25', + Gray = '#242930', +} + +export function ResizableDashboardCards() { + const [t] = useTranslation(); + + const { Preferences, update } = usePreferences(); + + const enabled = useEnabled(); + const formatMoney = useFormatMoney(); + + const containerRef = useRef(null); + + const user = useCurrentUser(); + const colors = useColorScheme(); + const company = useCurrentCompany(); + const settings = useReactSettings(); + + const [width, setWidth] = useState(1000); + const [chartData, setChartData] = useState([]); + const [currencies, setCurrencies] = useState([]); + const [totalsData, setTotalsData] = useState([]); + + const [isEditMode, setIsEditMode] = useState(false); + + const chartScale = + settings?.preferences?.dashboard_charts?.default_view || 'month'; + const currency = settings?.preferences?.dashboard_charts?.currency || 1; + const dateRange = + settings?.preferences?.dashboard_charts?.range || 'this_month'; + + const [dates, setDates] = useState<{ start_date: string; end_date: string }>({ + start_date: new Date(new Date().getFullYear(), new Date().getMonth(), 1) + .toISOString() + .split('T')[0], + end_date: new Date().toISOString().split('T')[0], + }); + + const [body, setBody] = useState<{ + start_date: string; + end_date: string; + date_range: string; + }>({ + start_date: '', + end_date: '', + date_range: dateRange, + }); + + const handleDateChange = (DateSet: string) => { + const [startDate, endDate] = DateSet.split(','); + if (new Date(startDate) > new Date(endDate)) { + setBody({ + start_date: endDate, + end_date: startDate, + date_range: 'custom', + }); + } else { + setBody({ + start_date: startDate, + end_date: endDate, + date_range: 'custom', + }); + } + }; + + const totals = useQuery({ + queryKey: ['/api/v1/charts/totals_v2', body], + queryFn: () => + request('POST', endpoint('/api/v1/charts/totals_v2'), body).then( + (response) => response.data + ), + staleTime: Infinity, + }); + + const chart = useQuery({ + queryKey: ['/api/v1/charts/chart_summary_v2', body], + queryFn: () => + request('POST', endpoint('/api/v1/charts/chart_summary_v2'), body).then( + (response) => response.data + ), + staleTime: Infinity, + }); + + useEffect(() => { + setBody((current) => ({ + ...current, + date_range: dateRange, + })); + }, [settings?.preferences?.dashboard_charts?.range]); + + useEffect(() => { + if (totals.data) { + setTotalsData(totals.data); + + const currencies: Currency[] = []; + + Object.entries(totals.data.currencies).map(([id, name]) => { + currencies.push({ value: id, label: name as unknown as string }); + }); + + const $currencies = collect(currencies) + .pluck('value') + .map((value) => parseInt(value as string)) + .toArray() as number[]; + + if (!$currencies.includes(currency)) { + update('preferences.dashboard_charts.currency', $currencies[0]); + } + + setCurrencies(currencies); + } + }, [totals.data]); + + useEffect(() => { + if (chart.data) { + setDates({ + start_date: chart.data.start_date, + end_date: chart.data.end_date, + }); + + setChartData(chart.data); + } + }, [chart.data]); + + useEffect(() => { + const resizeObserver = new ResizeObserver((entries) => { + if (entries[0]) { + setWidth(entries[0].contentRect.width); + } + }); + + if (containerRef.current) { + resizeObserver.observe(containerRef.current); + } + + return () => { + if (containerRef.current) { + resizeObserver.unobserve(containerRef.current); + } + }; + }, []); + + return ( +
+ {!totals.isLoading ? ( + + {totals.isLoading && ( +
+ +
+ )} + + {/* Quick date, currency & date picker. */} +
+
+ {currencies && ( + + update( + 'preferences.dashboard_charts.currency', + parseInt(value) + ) + } + > + + + {currencies.map((currency, index) => ( + + ))} + + )} + +
+ + + + + +
+ + + update('preferences.dashboard_charts.range', value) + } + value={body.date_range} + /> + + + + + + update('preferences.dashboard_charts.currency', parseInt(v)) + } + /> + + + update( + 'preferences.dashboard_charts.default_view', + value as ChartsDefaultView + ) + } + > + + + + + + + update('preferences.dashboard_charts.range', value) + } + > + + + + + + + + + + + + +
setIsEditMode((current) => !current)} + > + +
+
+
+ + {/* */} + + {company && ( +
+ +
+
+ {`${user?.first_name} ${user?.last_name}`} + + {t('recent_transactions')} +
+ +
+
+ {t('invoices')} + + + + {formatMoney( + totalsData[currency]?.invoices?.invoiced_amount || + 0, + company.settings.country_id, + currency.toString(), + 2 + )} + + +
+ +
+ {t('payments')} + + + {formatMoney( + totalsData[currency]?.revenue?.paid_to_date || 0, + company.settings.country_id, + currency.toString(), + 2 + )} + + +
+ +
+ {t('expenses')} + + + {formatMoney( + totalsData[currency]?.expenses?.amount || 0, + company.settings.country_id, + currency.toString(), + 2 + )} + + +
+ +
+ {t('outstanding')} + + + {formatMoney( + totalsData[currency]?.outstanding?.amount || 0, + company.settings.country_id, + currency.toString(), + 2 + )} + + +
+ +
+ {t('total_invoices_outstanding')} + + + + {totalsData[currency]?.outstanding + ?.outstanding_count || 0} + + +
+
+
+
+
+ )} + + {chartData && ( +
+ + + +
+ )} + +
+ +
+ +
+ +
+ + {enabled(ModuleBitmask.Invoices) && ( +
+ +
+ )} + + {enabled(ModuleBitmask.Invoices) && ( +
+ +
+ )} + + {enabled(ModuleBitmask.Quotes) && ( +
+ +
+ )} + + {enabled(ModuleBitmask.Quotes) && ( +
+ +
+ )} + + {enabled(ModuleBitmask.RecurringInvoices) && ( +
+ +
+ )} +
+ ) : ( +
+ +
+ )} +
+ ); +} diff --git a/src/pages/dashboard/components/Totals.tsx b/src/pages/dashboard/components/Totals.tsx deleted file mode 100644 index 4e8ea910fa..0000000000 --- a/src/pages/dashboard/components/Totals.tsx +++ /dev/null @@ -1,437 +0,0 @@ -/** - * Invoice Ninja (https://invoiceninja.com). - * - * @link https://github.com/invoiceninja/invoiceninja source repository - * - * @copyright Copyright (c) 2022. Invoice Ninja LLC (https://invoiceninja.com) - * - * @license https://www.elastic.co/licensing/elastic-license - */ - -import { Button, SelectField } from '$app/components/forms'; -import { endpoint } from '$app/common/helpers'; -import { Chart } from '$app/pages/dashboard/components/Chart'; -import { useEffect, useState } from 'react'; -import { Spinner } from '$app/components/Spinner'; -import { DropdownDateRangePicker } from '../../../components/DropdownDateRangePicker'; -import { Card } from '$app/components/cards'; -import { useTranslation } from 'react-i18next'; -import { request } from '$app/common/helpers/request'; -import { useFormatMoney } from '$app/common/hooks/money/useFormatMoney'; -import { useCurrentCompany } from '$app/common/hooks/useCurrentCompany'; -import { useCurrentUser } from '$app/common/hooks/useCurrentUser'; -import { Badge } from '$app/components/Badge'; -import { - ChartsDefaultView, - useReactSettings, -} from '$app/common/hooks/useReactSettings'; -import { usePreferences } from '$app/common/hooks/usePreferences'; -import collect from 'collect.js'; -import { useColorScheme } from '$app/common/colors'; -import { CurrencySelector } from '$app/components/CurrencySelector'; -import { useQuery } from 'react-query'; -import { DashboardCardSelector } from './DashboardCardSelector'; - -interface TotalsRecord { - revenue: { paid_to_date: string; code: string }; - expenses: { amount: string; code: string }; - invoices: { invoiced_amount: string; code: string; date: string }; - outstanding: { outstanding_count: number; amount: string; code: string }; -} - -interface Currency { - value: string; - label: string; -} - -export interface ChartData { - invoices: { - total: string; - date: string; - currency: string; - }[]; - payments: { - total: string; - date: string; - currency: string; - }[]; - outstanding: { - total: string; - date: string; - currency: string; - }[]; - expenses: { - total: string; - date: string; - currency: string; - }[]; -} - -export enum TotalColors { - Green = '#54B434', - Blue = '#2596BE', - Red = '#BE4D25', - Gray = '#242930', -} - -export function Totals() { - const [t] = useTranslation(); - - const settings = useReactSettings(); - - const { Preferences, update } = usePreferences(); - - const formatMoney = useFormatMoney(); - const company = useCurrentCompany(); - - const user = useCurrentUser(); - - const [totalsData, setTotalsData] = useState([]); - - const [currencies, setCurrencies] = useState([]); - - const [chartData, setChartData] = useState([]); - - const chartScale = - settings?.preferences?.dashboard_charts?.default_view || 'month'; - const currency = settings?.preferences?.dashboard_charts?.currency || 1; - const dateRange = - settings?.preferences?.dashboard_charts?.range || 'this_month'; - - const [dates, setDates] = useState<{ start_date: string; end_date: string }>({ - start_date: new Date(new Date().getFullYear(), new Date().getMonth(), 1) - .toISOString() - .split('T')[0], - end_date: new Date().toISOString().split('T')[0], - }); - - const [body, setBody] = useState<{ - start_date: string; - end_date: string; - date_range: string; - }>({ - start_date: '', - end_date: '', - date_range: dateRange, - }); - - useEffect(() => { - setBody((current) => ({ - ...current, - date_range: dateRange, - })); - }, [settings?.preferences?.dashboard_charts?.range]); - - const handleDateChange = (DateSet: string) => { - const [startDate, endDate] = DateSet.split(','); - if (new Date(startDate) > new Date(endDate)) { - setBody({ - start_date: endDate, - end_date: startDate, - date_range: 'custom', - }); - } else { - setBody({ - start_date: startDate, - end_date: endDate, - date_range: 'custom', - }); - } - }; - - const totals = useQuery({ - queryKey: ['/api/v1/charts/totals_v2', body], - queryFn: () => - request('POST', endpoint('/api/v1/charts/totals_v2'), body).then( - (response) => response.data - ), - staleTime: Infinity, - }); - - const chart = useQuery({ - queryKey: ['/api/v1/charts/chart_summary_v2', body], - queryFn: () => - request('POST', endpoint('/api/v1/charts/chart_summary_v2'), body).then( - (response) => response.data - ), - staleTime: Infinity, - }); - - useEffect(() => { - if (totals.data) { - setTotalsData(totals.data); - - const currencies: Currency[] = []; - - Object.entries(totals.data.currencies).map(([id, name]) => { - currencies.push({ value: id, label: name as unknown as string }); - }); - - const $currencies = collect(currencies) - .pluck('value') - .map((value) => parseInt(value as string)) - .toArray() as number[]; - - if (!$currencies.includes(currency)) { - update('preferences.dashboard_charts.currency', $currencies[0]); - } - - setCurrencies(currencies); - } - }, [totals.data]); - - useEffect(() => { - if (chart.data) { - setDates({ - start_date: chart.data.start_date, - end_date: chart.data.end_date, - }); - - setChartData(chart.data); - } - }, [chart.data]); - - const colors = useColorScheme(); - - return ( - <> - {totals.isLoading && ( -
- -
- )} - - {/* Quick date, currency & date picker. */} -
-
- {currencies && ( - - update('preferences.dashboard_charts.currency', parseInt(value)) - } - > - - - {currencies.map((currency, index) => ( - - ))} - - )} - -
- - - - - -
- - - update('preferences.dashboard_charts.range', value) - } - value={body.date_range} - /> - - - - - - update('preferences.dashboard_charts.currency', parseInt(v)) - } - /> - - - update( - 'preferences.dashboard_charts.default_view', - value as ChartsDefaultView - ) - } - > - - - - - - - update('preferences.dashboard_charts.range', value) - } - > - - - - - - - - - - - -
-
- - {/* */} - -
- {company && ( - -
-
- {`${user?.first_name} ${user?.last_name}`} - - {t('recent_transactions')} -
- -
-
- {t('invoices')} - - - - {formatMoney( - totalsData[currency]?.invoices?.invoiced_amount || 0, - company.settings.country_id, - currency.toString(), - 2 - )} - - -
- -
- {t('payments')} - - - {formatMoney( - totalsData[currency]?.revenue?.paid_to_date || 0, - company.settings.country_id, - currency.toString(), - 2 - )} - - -
- -
- {t('expenses')} - - - {formatMoney( - totalsData[currency]?.expenses?.amount || 0, - company.settings.country_id, - currency.toString(), - 2 - )} - - -
- -
- {t('outstanding')} - - - {formatMoney( - totalsData[currency]?.outstanding?.amount || 0, - company.settings.country_id, - currency.toString(), - 2 - )} - - -
- -
- {t('total_invoices_outstanding')} - - - - {totalsData[currency]?.outstanding?.outstanding_count || - 0} - - -
-
-
-
- )} - - {chartData && ( - - - - )} -
- - ); -} From eb6efe01709b28ea2af6f33183c505b852b9bfde Mon Sep 17 00:00:00 2001 From: Civolilah Date: Sat, 24 Aug 2024 18:25:37 +0200 Subject: [PATCH 08/36] Made change from charts issue fixes --- .../components/ResizableDashboardCards.tsx | 53 ++++++++++++++++--- 1 file changed, 47 insertions(+), 6 deletions(-) diff --git a/src/pages/dashboard/components/ResizableDashboardCards.tsx b/src/pages/dashboard/components/ResizableDashboardCards.tsx index f905036b4b..303c63d5c6 100644 --- a/src/pages/dashboard/components/ResizableDashboardCards.tsx +++ b/src/pages/dashboard/components/ResizableDashboardCards.tsx @@ -48,6 +48,7 @@ import { UpcomingInvoices } from './UpcomingInvoices'; import { Activity } from './Activity'; import { RecentPayments } from './RecentPayments'; import { useEnabled } from '$app/common/guards/guards/enabled'; +import dayjs from 'dayjs'; interface TotalsRecord { revenue: { paid_to_date: string; code: string }; @@ -100,6 +101,48 @@ export enum TotalColors { Gray = '#242930', } +const GLOBAL_DATE_RANGES: Record = { + last7_days: { + start: dayjs().subtract(7, 'days').format('YYYY-MM-DD'), + end: dayjs().format('YYYY-MM-DD'), + }, + last30_days: { + start: dayjs().subtract(1, 'month').format('YYYY-MM-DD'), + end: dayjs().format('YYYY-MM-DD'), + }, + last365_days: { + start: dayjs().subtract(365, 'days').format('YYYY-MM-DD'), + end: dayjs().format('YYYY-MM-DD'), + }, + this_month: { + start: dayjs().startOf('month').format('YYYY-MM-DD'), + end: dayjs().endOf('month').format('YYYY-MM-DD'), + }, + last_month: { + start: dayjs().startOf('month').subtract(1, 'month').format('YYYY-MM-DD'), + end: dayjs().subtract(1, 'month').endOf('month').format('YYYY-MM-DD'), + }, + this_quarter: { + start: dayjs().startOf('quarter').format('YYYY-MM-DD'), + end: dayjs().endOf('quarter').format('YYYY-MM-DD'), + }, + last_quarter: { + start: dayjs() + .subtract(1, 'quarter') + .startOf('quarter') + .format('YYYY-MM-DD'), + end: dayjs().subtract(1, 'quarter').endOf('quarter').format('YYYY-MM-DD'), + }, + this_year: { + start: dayjs().startOf('year').format('YYYY-MM-DD'), + end: dayjs().format('YYYY-MM-DD'), + }, + last_year: { + start: dayjs().subtract(1, 'year').startOf('year').format('YYYY-MM-DD'), + end: dayjs().subtract(1, 'year').endOf('year').format('YYYY-MM-DD'), + }, +}; + export function ResizableDashboardCards() { const [t] = useTranslation(); @@ -129,10 +172,8 @@ export function ResizableDashboardCards() { settings?.preferences?.dashboard_charts?.range || 'this_month'; const [dates, setDates] = useState<{ start_date: string; end_date: string }>({ - start_date: new Date(new Date().getFullYear(), new Date().getMonth(), 1) - .toISOString() - .split('T')[0], - end_date: new Date().toISOString().split('T')[0], + start_date: GLOBAL_DATE_RANGES[dateRange]?.start || '', + end_date: GLOBAL_DATE_RANGES[dateRange]?.end || '', }); const [body, setBody] = useState<{ @@ -140,8 +181,8 @@ export function ResizableDashboardCards() { end_date: string; date_range: string; }>({ - start_date: '', - end_date: '', + start_date: GLOBAL_DATE_RANGES[dateRange]?.start || '', + end_date: GLOBAL_DATE_RANGES[dateRange]?.end || '', date_range: dateRange, }); From 263f37dc6e9f1e8fc234ddf05c1a072e5ee562ab Mon Sep 17 00:00:00 2001 From: Civolilah Date: Sat, 24 Aug 2024 19:15:41 +0200 Subject: [PATCH 09/36] Fixed issues with dashboard UI --- src/components/cards/Card.tsx | 1 - src/pages/dashboard/components/Activity.tsx | 8 ++- .../components/ResizableDashboardCards.tsx | 52 +++++++++++++------ 3 files changed, 39 insertions(+), 22 deletions(-) diff --git a/src/components/cards/Card.tsx b/src/components/cards/Card.tsx index 556c142cc4..cb6c3fecae 100644 --- a/src/components/cards/Card.tsx +++ b/src/components/cards/Card.tsx @@ -141,7 +141,6 @@ export function Card(props: Props) { 'py-0': props.withoutBodyPadding, 'py-4': padding === 'regular' && !props.withoutBodyPadding, 'py-2': padding === 'small' && !props.withoutBodyPadding, - 'h-full': height === 'full', })} > {props.isLoading && } />} diff --git a/src/pages/dashboard/components/Activity.tsx b/src/pages/dashboard/components/Activity.tsx index 48c0cde02f..8a0f494eb3 100644 --- a/src/pages/dashboard/components/Activity.tsx +++ b/src/pages/dashboard/components/Activity.tsx @@ -36,6 +36,7 @@ export function Activity() { className="relative" height="full" withoutBodyPadding + withScrollableBody > {isLoading && ( @@ -47,11 +48,8 @@ export function Activity() { {t('error_refresh_page')} )} -
-
+
+
{data?.data.data && data.data.data.map((record: ActivityRecord, index: number) => ( diff --git a/src/pages/dashboard/components/ResizableDashboardCards.tsx b/src/pages/dashboard/components/ResizableDashboardCards.tsx index 303c63d5c6..46d05d5c6c 100644 --- a/src/pages/dashboard/components/ResizableDashboardCards.tsx +++ b/src/pages/dashboard/components/ResizableDashboardCards.tsx @@ -288,7 +288,7 @@ export function ResizableDashboardCards() { > {!totals.isLoading ? (
@@ -565,10 +568,12 @@ export function ResizableDashboardCards() { 'cursor-grab': isEditMode, })} data-grid={{ - x: 11, + x: 40, // Prilagođeno u odnosu na 100 kolona y: 1, - w: 14.2, + w: 60, // Prilagođeno u odnosu na 100 kolona h: 3.2, + minH: 1.62, + minW: 18, // Prilagođeno u odnosu na 100 kolona resizeHandles: ['s', 'w', 'e', 'n', 'sw', 'nw', 'se', 'ne'], isResizable: isEditMode, isDraggable: isEditMode, @@ -578,6 +583,7 @@ export function ResizableDashboardCards() { title={t('overview')} className="col-span-12 xl:col-span-8 pr-4" height="full" + withScrollableBody > Date: Mon, 2 Sep 2024 21:01:25 +0200 Subject: [PATCH 10/36] Implemented responsive system --- package-lock.json | 7 + package.json | 1 + .../components/ResizableDashboardCards.tsx | 261 +++++++++--------- 3 files changed, 145 insertions(+), 124 deletions(-) diff --git a/package-lock.json b/package-lock.json index f30d75ab13..7a36da8b1d 100644 --- a/package-lock.json +++ b/package-lock.json @@ -27,6 +27,7 @@ "collect.js": "^4.34.3", "currency.js": "^2.0.4", "dayjs": "^1.11.7", + "deep-object-diff": "^1.1.9", "dompurify": "^3.1.3", "dotenv": "^16.0.3", "formik": "^2.2.9", @@ -4640,6 +4641,12 @@ "integrity": "sha512-oIPzksmTg4/MriiaYGO+okXDT7ztn/w3Eptv/+gSIdMdKsJo0u4CfYNFJPy+4SKMuCqGw2wxnA+URMg3t8a/bQ==", "dev": true }, + "node_modules/deep-object-diff": { + "version": "1.1.9", + "resolved": "https://registry.npmjs.org/deep-object-diff/-/deep-object-diff-1.1.9.tgz", + "integrity": "sha512-Rn+RuwkmkDwCi2/oXOFS9Gsr5lJZu/yTGpK7wAaAIE75CC+LCGEZHpY6VQJa/RoJcrmaA/docWJZvYohlNkWPA==", + "license": "MIT" + }, "node_modules/deepmerge": { "version": "2.2.1", "resolved": "https://registry.npmjs.org/deepmerge/-/deepmerge-2.2.1.tgz", diff --git a/package.json b/package.json index c89b56675b..a71d6dcca2 100644 --- a/package.json +++ b/package.json @@ -22,6 +22,7 @@ "collect.js": "^4.34.3", "currency.js": "^2.0.4", "dayjs": "^1.11.7", + "deep-object-diff": "^1.1.9", "dompurify": "^3.1.3", "dotenv": "^16.0.3", "formik": "^2.2.9", diff --git a/src/pages/dashboard/components/ResizableDashboardCards.tsx b/src/pages/dashboard/components/ResizableDashboardCards.tsx index 46d05d5c6c..673cc7d081 100644 --- a/src/pages/dashboard/components/ResizableDashboardCards.tsx +++ b/src/pages/dashboard/components/ResizableDashboardCards.tsx @@ -35,7 +35,7 @@ import { useColorScheme } from '$app/common/colors'; import { CurrencySelector } from '$app/components/CurrencySelector'; import { useQuery } from 'react-query'; import { DashboardCardSelector } from './DashboardCardSelector'; -import GridLayout from 'react-grid-layout'; +import GridLayout, { Responsive, WidthProvider } from 'react-grid-layout'; import { Icon } from '$app/components/icons/Icon'; import { BiMove } from 'react-icons/bi'; import classNames from 'classnames'; @@ -50,6 +50,8 @@ import { RecentPayments } from './RecentPayments'; import { useEnabled } from '$app/common/guards/guards/enabled'; import dayjs from 'dayjs'; +const ResponsiveGridLayout = WidthProvider(Responsive); + interface TotalsRecord { revenue: { paid_to_date: string; code: string }; expenses: { amount: string; code: string }; @@ -143,6 +145,93 @@ const GLOBAL_DATE_RANGES: Record = { }, }; +const initialLayouts = { + lg: [ + { i: '1', x: 80, y: 0, w: 100, h: 2.8, isResizable: false, static: true }, + { + i: '2', + x: 0, + y: 1, + w: 33, + h: 25.4, + minH: 18.144, + minW: 18, + }, + { + i: '3', + x: 40, + y: 1, + w: 66, + h: 25.4, + minH: 18.144, + minW: 18, + }, + { + i: '4', + x: 0, + y: 2, + w: 49.6, + h: 20, + minH: 18.144, + minW: 18, + }, + { + i: '5', + x: 51, + y: 2, + w: 49.6, + h: 20, + minH: 18.144, + minW: 18, + }, + { + i: '6', + x: 0, + y: 3, + w: 49.6, + h: 20, + minH: 18.144, + minW: 18, + }, + { + i: '7', + x: 51, + y: 3, + w: 49.6, + h: 20, + minH: 18.144, + minW: 18, + }, + { + i: '8', + x: 0, + y: 4, + w: 49.6, + h: 20, + minH: 18.144, + minW: 18, + }, + { + i: '9', + x: 51, + y: 4, + w: 49.6, + h: 20, + minH: 18.144, + minW: 18, + }, + { + i: '10', + x: 0, + y: 5, + w: 49.6, + h: 20, + minH: 18.144, + minW: 18, + }, + ], +}; + export function ResizableDashboardCards() { const [t] = useTranslation(); @@ -162,6 +251,8 @@ export function ResizableDashboardCards() { const [chartData, setChartData] = useState([]); const [currencies, setCurrencies] = useState([]); const [totalsData, setTotalsData] = useState([]); + const [layoutBreakpoint, setLayoutBreakpoint] = useState('lg'); + const [layouts, setLayouts] = useState(initialLayouts); const [isEditMode, setIsEditMode] = useState(false); @@ -221,6 +312,24 @@ export function ResizableDashboardCards() { staleTime: Infinity, }); + const onLayoutChange = (newLayout: GridLayout.Layout[]) => { + console.log(newLayout); + + setLayouts((current) => ({ ...current, [layoutBreakpoint]: newLayout })); + }; + + // const onResizeStop = ( + // layout: GridLayout.Layout[], + // oldItem: GridLayout.Layout, + // newItem: GridLayout.Layout + // ) => { + // setLayouts( + // layout.map((item) => + // item.i === newItem.i ? { ...item, h: newItem.h } : item + // ) + // ); + // }; + useEffect(() => { setBody((current) => ({ ...current, @@ -287,13 +396,27 @@ export function ResizableDashboardCards() { style={{ width: '100%' }} > {!totals.isLoading ? ( - setLayoutBreakpoint(breakPoint)} + resizeHandles={['s', 'w', 'e', 'n', 'sw', 'nw', 'se', 'ne']} > {totals.isLoading && (
@@ -302,18 +425,7 @@ export function ResizableDashboardCards() { )} {/* Quick date, currency & date picker. */} -
+
{currencies && (
@@ -641,20 +709,9 @@ export function ResizableDashboardCards() { {enabled(ModuleBitmask.Invoices) && (
@@ -663,20 +720,9 @@ export function ResizableDashboardCards() { {enabled(ModuleBitmask.Invoices) && (
@@ -685,20 +731,9 @@ export function ResizableDashboardCards() { {enabled(ModuleBitmask.Quotes) && (
@@ -707,20 +742,9 @@ export function ResizableDashboardCards() { {enabled(ModuleBitmask.Quotes) && (
@@ -729,25 +753,14 @@ export function ResizableDashboardCards() { {enabled(ModuleBitmask.RecurringInvoices) && (
)} - + ) : (
From edf8b084697217d2d8a2638384f3106455d4be79 Mon Sep 17 00:00:00 2001 From: Civolilah Date: Wed, 4 Sep 2024 01:24:01 +0200 Subject: [PATCH 11/36] Implemented saving customized dashboard settings --- src/common/hooks/useReactSettings.ts | 1 + .../dashboard/components/PastDueInvoices.tsx | 1 + .../components/ResizableDashboardCards.tsx | 496 ++++++++++++++++-- 3 files changed, 440 insertions(+), 58 deletions(-) diff --git a/src/common/hooks/useReactSettings.ts b/src/common/hooks/useReactSettings.ts index 31133b25f5..a510c42c2b 100644 --- a/src/common/hooks/useReactSettings.ts +++ b/src/common/hooks/useReactSettings.ts @@ -65,6 +65,7 @@ export interface ReactSettings { show_table_footer?: boolean; dark_mode?: boolean; color_theme?: ColorTheme; + dashboard_cards_configuration?: any; } export type ReactTableColumns = diff --git a/src/pages/dashboard/components/PastDueInvoices.tsx b/src/pages/dashboard/components/PastDueInvoices.tsx index b94f5ec15a..a9701202d5 100644 --- a/src/pages/dashboard/components/PastDueInvoices.tsx +++ b/src/pages/dashboard/components/PastDueInvoices.tsx @@ -110,6 +110,7 @@ export function PastDueInvoices() { height: '19.9rem', }} withoutSortQueryParameter + withResourcefulActions />
diff --git a/src/pages/dashboard/components/ResizableDashboardCards.tsx b/src/pages/dashboard/components/ResizableDashboardCards.tsx index 673cc7d081..4e0ea5b6bb 100644 --- a/src/pages/dashboard/components/ResizableDashboardCards.tsx +++ b/src/pages/dashboard/components/ResizableDashboardCards.tsx @@ -1,4 +1,3 @@ -/* eslint-disable @typescript-eslint/no-unused-vars */ /** * Invoice Ninja (https://invoiceninja.com). * @@ -15,7 +14,7 @@ import '$app/resources/css/gridLayout.css'; import { Button, SelectField } from '$app/components/forms'; import { endpoint } from '$app/common/helpers'; import { Chart } from '$app/pages/dashboard/components/Chart'; -import { useEffect, useRef, useState } from 'react'; +import { useEffect, useState } from 'react'; import { Spinner } from '$app/components/Spinner'; import { DropdownDateRangePicker } from '../../../components/DropdownDateRangePicker'; import { Card } from '$app/components/cards'; @@ -49,6 +48,15 @@ import { Activity } from './Activity'; import { RecentPayments } from './RecentPayments'; import { useEnabled } from '$app/common/guards/guards/enabled'; import dayjs from 'dayjs'; +import { useDebounce } from 'react-use'; +import { diff } from 'deep-object-diff'; +import { User } from '$app/common/interfaces/user'; +import { cloneDeep, set } from 'lodash'; +import { GenericSingleResourceResponse } from '$app/common/interfaces/generic-api-response'; +import { CompanyUser } from '$app/common/interfaces/company-user'; +import { $refetch } from '$app/common/hooks/useRefetch'; +import { updateUser } from '$app/common/stores/slices/user'; +import { useDispatch } from 'react-redux'; const ResponsiveGridLayout = WidthProvider(Responsive); @@ -64,6 +72,8 @@ interface Currency { label: string; } +export type DashboardGridLayouts = GridLayout.Layouts; + export interface ChartData { invoices: { total: string; @@ -87,15 +97,6 @@ export interface ChartData { }[]; } -interface GridItem { - i: string; - x: number; - y: number; - w: number; - h: number; - content: string; -} - export enum TotalColors { Green = '#54B434', Blue = '#2596BE', @@ -170,7 +171,7 @@ const initialLayouts = { i: '4', x: 0, y: 2, - w: 49.6, + w: 49.5, h: 20, minH: 18.144, minW: 18, @@ -179,7 +180,7 @@ const initialLayouts = { i: '5', x: 51, y: 2, - w: 49.6, + w: 49.5, h: 20, minH: 18.144, minW: 18, @@ -188,7 +189,7 @@ const initialLayouts = { i: '6', x: 0, y: 3, - w: 49.6, + w: 49.5, h: 20, minH: 18.144, minW: 18, @@ -197,7 +198,7 @@ const initialLayouts = { i: '7', x: 51, y: 3, - w: 49.6, + w: 49.5, h: 20, minH: 18.144, minW: 18, @@ -206,7 +207,7 @@ const initialLayouts = { i: '8', x: 0, y: 4, - w: 49.6, + w: 49.5, h: 20, minH: 18.144, minW: 18, @@ -215,7 +216,259 @@ const initialLayouts = { i: '9', x: 51, y: 4, - w: 49.6, + w: 49.5, + h: 20, + minH: 18.144, + minW: 18, + }, + { + i: '10', + x: 0, + y: 5, + w: 49.5, + h: 20, + minH: 18.144, + minW: 18, + }, + ], + md: [ + { i: '1', x: 80, y: 0, w: 100, h: 2.8, isResizable: false, static: true }, + { + i: '2', + x: 0, + y: 1, + w: 100, + h: 25.4, + minH: 18.144, + minW: 18, + }, + { + i: '3', + x: 0, + y: 2, + w: 100, + h: 25.4, + minH: 18.144, + minW: 18, + }, + { + i: '4', + x: 0, + y: 3, + w: 100, + h: 20, + minH: 18.144, + minW: 18, + }, + { + i: '5', + x: 0, + y: 4, + w: 100, + h: 20, + minH: 18.144, + minW: 18, + }, + { + i: '6', + x: 0, + y: 5, + w: 100, + h: 20, + minH: 18.144, + minW: 18, + }, + { + i: '7', + x: 0, + y: 6, + w: 100, + h: 20, + minH: 18.144, + minW: 18, + }, + { + i: '8', + x: 0, + y: 7, + w: 100, + h: 20, + minH: 18.144, + minW: 18, + }, + { + i: '9', + x: 0, + y: 8, + w: 100, + h: 20, + minH: 18.144, + minW: 18, + }, + { + i: '10', + x: 0, + y: 9, + w: 100, + h: 20, + minH: 18.144, + minW: 18, + }, + ], + sm: [ + { i: '1', x: 80, y: 0, w: 100, h: 2.8, isResizable: false, static: true }, + { + i: '2', + x: 0, + y: 1, + w: 100, + h: 25.4, + minH: 18.144, + minW: 18, + }, + { + i: '3', + x: 0, + y: 2, + w: 100, + h: 25.4, + minH: 18.144, + minW: 18, + }, + { + i: '4', + x: 0, + y: 3, + w: 100, + h: 20, + minH: 18.144, + minW: 18, + }, + { + i: '5', + x: 0, + y: 4, + w: 100, + h: 20, + minH: 18.144, + minW: 18, + }, + { + i: '6', + x: 0, + y: 5, + w: 100, + h: 20, + minH: 18.144, + minW: 18, + }, + { + i: '7', + x: 0, + y: 6, + w: 100, + h: 20, + minH: 18.144, + minW: 18, + }, + { + i: '8', + x: 0, + y: 7, + w: 100, + h: 20, + minH: 18.144, + minW: 18, + }, + { + i: '9', + x: 0, + y: 8, + w: 100, + h: 20, + minH: 18.144, + minW: 18, + }, + { + i: '10', + x: 0, + y: 9, + w: 100, + h: 20, + minH: 18.144, + minW: 18, + }, + ], + xs: [ + { i: '1', x: 80, y: 0, w: 100, h: 2.8, isResizable: false, static: true }, + { + i: '2', + x: 0, + y: 1, + w: 100, + h: 25.4, + minH: 18.144, + minW: 18, + }, + { + i: '3', + x: 0, + y: 2, + w: 100, + h: 25.4, + minH: 18.144, + minW: 18, + }, + { + i: '4', + x: 0, + y: 3, + w: 100, + h: 20, + minH: 18.144, + minW: 18, + }, + { + i: '5', + x: 0, + y: 4, + w: 100, + h: 20, + minH: 18.144, + minW: 18, + }, + { + i: '6', + x: 0, + y: 5, + w: 100, + h: 20, + minH: 18.144, + minW: 18, + }, + { + i: '7', + x: 0, + y: 6, + w: 100, + h: 20, + minH: 18.144, + minW: 18, + }, + { + i: '8', + x: 0, + y: 7, + w: 100, + h: 20, + minH: 18.144, + minW: 18, + }, + { + i: '9', + x: 0, + y: 8, + w: 100, h: 20, minH: 18.144, minW: 18, @@ -223,8 +476,92 @@ const initialLayouts = { { i: '10', x: 0, + y: 9, + w: 100, + h: 20, + minH: 18.144, + minW: 18, + }, + ], + xxs: [ + { i: '1', x: 80, y: 0, w: 100, h: 2.8, isResizable: false, static: true }, + { + i: '2', + x: 0, + y: 1, + w: 100, + h: 25.4, + minH: 18.144, + minW: 18, + }, + { + i: '3', + x: 0, + y: 2, + w: 100, + h: 25.4, + minH: 18.144, + minW: 18, + }, + { + i: '4', + x: 0, + y: 3, + w: 100, + h: 20, + minH: 18.144, + minW: 18, + }, + { + i: '5', + x: 0, + y: 4, + w: 100, + h: 20, + minH: 18.144, + minW: 18, + }, + { + i: '6', + x: 0, y: 5, - w: 49.6, + w: 100, + h: 20, + minH: 18.144, + minW: 18, + }, + { + i: '7', + x: 0, + y: 6, + w: 100, + h: 20, + minH: 18.144, + minW: 18, + }, + { + i: '8', + x: 0, + y: 7, + w: 100, + h: 20, + minH: 18.144, + minW: 18, + }, + { + i: '9', + x: 0, + y: 8, + w: 100, + h: 20, + minH: 18.144, + minW: 18, + }, + { + i: '10', + x: 0, + y: 9, + w: 100, h: 20, minH: 18.144, minW: 18, @@ -238,22 +575,23 @@ export function ResizableDashboardCards() { const { Preferences, update } = usePreferences(); const enabled = useEnabled(); + const dispatch = useDispatch(); const formatMoney = useFormatMoney(); - const containerRef = useRef(null); - const user = useCurrentUser(); const colors = useColorScheme(); const company = useCurrentCompany(); const settings = useReactSettings(); - const [width, setWidth] = useState(1000); const [chartData, setChartData] = useState([]); const [currencies, setCurrencies] = useState([]); const [totalsData, setTotalsData] = useState([]); - const [layoutBreakpoint, setLayoutBreakpoint] = useState('lg'); - const [layouts, setLayouts] = useState(initialLayouts); + const [layoutBreakpoint, setLayoutBreakpoint] = useState(); + const [layouts, setLayouts] = useState(initialLayouts); + + const [isLayoutsInitialized, setIsLayoutsInitialized] = + useState(false); const [isEditMode, setIsEditMode] = useState(false); const chartScale = @@ -312,23 +650,51 @@ export function ResizableDashboardCards() { staleTime: Infinity, }); - const onLayoutChange = (newLayout: GridLayout.Layout[]) => { - console.log(newLayout); + const onResizeStop = ( + layout: GridLayout.Layout[], + oldItem: GridLayout.Layout, + newItem: GridLayout.Layout + ) => { + if (layoutBreakpoint) { + setLayouts((current) => ({ + ...current, + [layoutBreakpoint]: layout.map((item) => ({ + ...item, + h: item.y === newItem.y ? newItem.h : item.h, + })), + })); + } + }; - setLayouts((current) => ({ ...current, [layoutBreakpoint]: newLayout })); + const onDragStop = (layout: GridLayout.Layout[]) => { + layoutBreakpoint && + setLayouts((current) => ({ + ...current, + [layoutBreakpoint]: layout, + })); }; - // const onResizeStop = ( - // layout: GridLayout.Layout[], - // oldItem: GridLayout.Layout, - // newItem: GridLayout.Layout - // ) => { - // setLayouts( - // layout.map((item) => - // item.i === newItem.i ? { ...item, h: newItem.h } : item - // ) - // ); - // }; + const handleUpdateUserPreferences = () => { + const updatedUser = cloneDeep(user) as User; + + set( + updatedUser, + 'company_user.react_settings.dashboard_cards_configuration', + cloneDeep(layouts) + ); + + request( + 'PUT', + endpoint('/api/v1/company_users/:id', { id: updatedUser.id }), + updatedUser + ).then((response: GenericSingleResourceResponse) => { + set(updatedUser, 'company_user', response.data.data); + + $refetch(['company_users']); + + dispatch(updateUser(updatedUser)); + }); + }; useEffect(() => { setBody((current) => ({ @@ -372,29 +738,40 @@ export function ResizableDashboardCards() { }, [chart.data]); useEffect(() => { - const resizeObserver = new ResizeObserver((entries) => { - if (entries[0]) { - setWidth(entries[0].contentRect.width); + if (layoutBreakpoint) { + if (settings?.dashboard_cards_configuration && !isLayoutsInitialized) { + setLayouts(cloneDeep(settings?.dashboard_cards_configuration)); } - }); - if (containerRef.current) { - resizeObserver.observe(containerRef.current); + setIsLayoutsInitialized(true); } + }, [layoutBreakpoint]); + + useDebounce( + () => { + if ( + settings && + !settings.dashboard_cards_configuration && + Object.keys(diff(initialLayouts, layouts)).length + ) { + handleUpdateUserPreferences(); + } - return () => { - if (containerRef.current) { - resizeObserver.unobserve(containerRef.current); + if ( + settings && + settings.dashboard_cards_configuration && + Object.keys(diff(settings.dashboard_cards_configuration, layouts)) + .length + ) { + handleUpdateUserPreferences(); } - }; - }, []); + }, + 1500, + [layouts] + ); return ( -
+
{!totals.isLoading ? ( setLayoutBreakpoint(breakPoint)} + onBreakpointChange={(currentBreakPoint) => + setLayoutBreakpoint(currentBreakPoint) + } + onResizeStop={onResizeStop} + onDragStop={onDragStop} resizeHandles={['s', 'w', 'e', 'n', 'sw', 'nw', 'se', 'ne']} > - {totals.isLoading && ( + {(totals.isLoading || !isLayoutsInitialized) && (
)} {/* Quick date, currency & date picker. */} +
{currencies && ( From a28c916cdca90de036833d2a22cc74b7b1c75db9 Mon Sep 17 00:00:00 2001 From: Civolilah Date: Thu, 5 Sep 2024 19:55:43 +0200 Subject: [PATCH 12/36] Implemented adding cards into responsive grid layout system --- src/common/hooks/useRefetch.tsx | 10 +- src/common/interfaces/company-user.ts | 2 +- .../{DashboardCards.tsx => DashboardCard.tsx} | 98 +++++------ .../components/DashboardCardSelector.tsx | 40 +++-- .../dashboard/components/PastDueInvoices.tsx | 1 - .../components/ResizableDashboardCards.tsx | 161 +++++++++++++++--- 6 files changed, 214 insertions(+), 98 deletions(-) rename src/pages/dashboard/components/{DashboardCards.tsx => DashboardCard.tsx} (51%) diff --git a/src/common/hooks/useRefetch.tsx b/src/common/hooks/useRefetch.tsx index f4495ce3bb..56a08bad31 100644 --- a/src/common/hooks/useRefetch.tsx +++ b/src/common/hooks/useRefetch.tsx @@ -20,6 +20,7 @@ export const keys = { '/api/v1/activities/entity', '/api/v1/activities', '/api/v1/documents', + '/api/v1/charts/calculated_fields', ], }, designs: { @@ -64,6 +65,7 @@ export const keys = { '/api/v1/charts/totals_v2', '/api/v1/charts/chart_summary_v2', '/api/v1/documents', + '/api/v1/charts/calculated_fields', ], }, group_settings: { @@ -80,6 +82,7 @@ export const keys = { '/api/v1/charts/chart_summary_v2', '/api/v1/activities', '/api/v1/documents', + '/api/v1/charts/calculated_fields', ], }, purchase_orders: { @@ -96,7 +99,11 @@ export const keys = { }, tasks: { path: '/api/v1/tasks', - dependencies: ['/api/v1/projects', '/api/v1/documents'], + dependencies: [ + '/api/v1/projects', + '/api/v1/documents', + '/api/v1/charts/calculated_fields', + ], }, tax_rates: { path: '/api/v1/tax_rates', @@ -181,6 +188,7 @@ export const keys = { '/api/v1/clients', '/api/v1/activities', '/api/v1/documents', + '/api/v1/charts/calculated_fields', ], }, recurring_invoices: { diff --git a/src/common/interfaces/company-user.ts b/src/common/interfaces/company-user.ts index e89ec91f59..a5d3a4f6f7 100644 --- a/src/common/interfaces/company-user.ts +++ b/src/common/interfaces/company-user.ts @@ -32,7 +32,7 @@ export interface CompanyUser { react_settings: ReactSettings; } -type Format = 'time' | 'money'; +export type Format = 'time' | 'money'; export type Period = 'current' | 'previous' | 'total'; export type Calculate = 'sum' | 'avg' | 'count'; export type Field = diff --git a/src/pages/dashboard/components/DashboardCards.tsx b/src/pages/dashboard/components/DashboardCard.tsx similarity index 51% rename from src/pages/dashboard/components/DashboardCards.tsx rename to src/pages/dashboard/components/DashboardCard.tsx index c0ac072ab2..fffd955a9d 100644 --- a/src/pages/dashboard/components/DashboardCards.tsx +++ b/src/pages/dashboard/components/DashboardCard.tsx @@ -9,7 +9,6 @@ */ import { useFormatMoney } from '$app/common/hooks/money/useFormatMoney'; -import { useCurrentUser } from '$app/common/hooks/useCurrentUser'; import { DashboardField } from '$app/common/interfaces/company-user'; import { useEffect, useState } from 'react'; import { useTranslation } from 'react-i18next'; @@ -20,9 +19,6 @@ import { request } from '$app/common/helpers/request'; import { endpoint } from '$app/common/helpers'; import { Spinner } from '$app/components/Spinner'; -import 'react-grid-layout/css/styles.css'; -import 'react-resizable/css/styles.css'; - interface DashboardCardsProps { dateRange: string; startDate: string; @@ -39,7 +35,7 @@ export const PERIOD_LABELS = { previous: 'previous_period', }; -function Card(props: CardProps) { +export function DashboardCard(props: CardProps) { const [t] = useTranslation(); const { dateRange, startDate, endDate, field, currencyId } = props; @@ -48,31 +44,46 @@ function Card(props: CardProps) { const formatMoney = useFormatMoney(); const [isFormBusy, setIsFormBusy] = useState(false); - const [responseData, setResponseData] = useState(0); + const [responseData, setResponseData] = useState(); useEffect(() => { - setIsFormBusy(true); + (async () => { + typeof responseData === 'undefined' && setIsFormBusy(true); + + const response = await queryClient.fetchQuery( + [ + '/api/v1/charts/calculated_fields', + dateRange, + startDate, + endDate, + field.field, + field.calculate, + field.period, + currencyId, + ], + () => + request('POST', endpoint('/api/v1/charts/calculated_fields'), { + date_range: dateRange, + start_date: startDate, + end_date: endDate, + field: field.field, + calculation: field.calculate, + period: field.period, + format: field.format, + currency_id: currencyId, + }).then((response) => response.data), + { staleTime: Infinity } + ); - queryClient.fetchQuery(['/api/v1/charts/calculated_fields'], () => - request('POST', endpoint('/api/v1/charts/calculated_fields'), { - date_range: dateRange, - start_date: startDate, - end_date: endDate, - field: field.field, - calculation: field.calculate, - period: field.period, - format: field.format, - currency_id: currencyId, - }) - .then((response) => setResponseData(response.data)) - .finally(() => setIsFormBusy(false)) - ); + setResponseData(response); + typeof responseData === 'undefined' && setIsFormBusy(false); + })(); }, [field]); return ( - + {isFormBusy && ( -
+
)} @@ -81,9 +92,11 @@ function Card(props: CardProps) {
{t(FIELDS_LABELS[field.field])} - {field.format === 'money' && ( - {formatMoney(responseData, '', '')} - )} + + {field.format === 'money' && field.calculate !== 'count' + ? formatMoney(responseData ?? 0, '', '') + : responseData} + {t( @@ -96,36 +109,3 @@ function Card(props: CardProps) { ); } - -export function DashboardCards(props: DashboardCardsProps) { - const { dateRange, startDate, endDate, currencyId } = props; - - const currentUser = useCurrentUser(); - - const [currentFields, setCurrentFields] = useState([]); - - useEffect(() => { - if (currentUser && Object.keys(currentUser).length) { - setCurrentFields( - currentUser.company_user?.settings.dashboard_fields ?? [] - ); - } - }, [currentUser]); - - return ( - <> -
- {currentFields.map((field, index) => ( - - ))} -
- - ); -} diff --git a/src/pages/dashboard/components/DashboardCardSelector.tsx b/src/pages/dashboard/components/DashboardCardSelector.tsx index a2ee76c6e8..c81793ed2d 100644 --- a/src/pages/dashboard/components/DashboardCardSelector.tsx +++ b/src/pages/dashboard/components/DashboardCardSelector.tsx @@ -18,6 +18,7 @@ import { CompanyUser, DashboardField, Field, + Format, Period, } from '$app/common/interfaces/company-user'; import { GenericSingleResourceResponse } from '$app/common/interfaces/generic-api-response'; @@ -32,14 +33,14 @@ import { DropResult, } from '@hello-pangea/dnd'; import { arrayMoveImmutable } from 'array-move'; -import { cloneDeep, set } from 'lodash'; +import { cloneDeep, isEqual, set } from 'lodash'; import { useEffect, useState } from 'react'; import { useTranslation } from 'react-i18next'; import { CgOptions } from 'react-icons/cg'; import { MdClose, MdDragHandle } from 'react-icons/md'; import { useDispatch } from 'react-redux'; import { updateUser } from '$app/common/stores/slices/user'; -import { PERIOD_LABELS } from './DashboardCards'; +import { PERIOD_LABELS } from './DashboardCard'; const FIELDS = [ 'active_invoices', @@ -102,7 +103,7 @@ export function DashboardCardSelector() { field: '' as Field, period: 'current', calculate: 'sum', - format: 'time', + format: 'money', }); }; @@ -116,9 +117,9 @@ export function DashboardCardSelector() { setCurrentFields(sorted); }; - const handleDelete = (fieldKey: Field) => { + const handleDelete = (field: DashboardField) => { const updatedCurrentColumns = currentFields.filter( - (field) => field.field !== fieldKey + (currentField) => !isEqual(currentField, field) ); setCurrentFields(updatedCurrentColumns); @@ -247,7 +248,7 @@ export function DashboardCardSelector() { className="cursor-pointer" element={MdClose} size={24} - onClick={() => handleDelete(field.field)} + onClick={() => handleDelete(field)} />
@@ -295,17 +296,6 @@ export function DashboardCardSelector() { {t('add_field')} - {/* - - - - - - - - - */} -
diff --git a/src/pages/dashboard/components/ResizableDashboardCards.tsx b/src/pages/dashboard/components/ResizableDashboardCards.tsx index 4e0ea5b6bb..f6d6180b99 100644 --- a/src/pages/dashboard/components/ResizableDashboardCards.tsx +++ b/src/pages/dashboard/components/ResizableDashboardCards.tsx @@ -57,6 +57,8 @@ import { CompanyUser } from '$app/common/interfaces/company-user'; import { $refetch } from '$app/common/hooks/useRefetch'; import { updateUser } from '$app/common/stores/slices/user'; import { useDispatch } from 'react-redux'; +import { DashboardCard } from './DashboardCard'; +import { MdRefresh } from 'react-icons/md'; const ResponsiveGridLayout = WidthProvider(Responsive); @@ -148,11 +150,21 @@ const GLOBAL_DATE_RANGES: Record = { const initialLayouts = { lg: [ - { i: '1', x: 80, y: 0, w: 100, h: 2.8, isResizable: false, static: true }, + { i: '0', x: 80, y: 0, w: 100, h: 2.8, isResizable: false, static: true }, { - i: '2', + i: '1', x: 0, y: 1, + w: 100, + h: 6.3, + minH: 6.3, + minW: 100, + isResizable: false, + }, + { + i: '2', + x: 0, + y: 2, w: 33, h: 25.4, minH: 18.144, @@ -161,7 +173,7 @@ const initialLayouts = { { i: '3', x: 40, - y: 1, + y: 2, w: 66, h: 25.4, minH: 18.144, @@ -170,7 +182,7 @@ const initialLayouts = { { i: '4', x: 0, - y: 2, + y: 3, w: 49.5, h: 20, minH: 18.144, @@ -179,7 +191,7 @@ const initialLayouts = { { i: '5', x: 51, - y: 2, + y: 3, w: 49.5, h: 20, minH: 18.144, @@ -188,7 +200,7 @@ const initialLayouts = { { i: '6', x: 0, - y: 3, + y: 4, w: 49.5, h: 20, minH: 18.144, @@ -197,7 +209,7 @@ const initialLayouts = { { i: '7', x: 51, - y: 3, + y: 4, w: 49.5, h: 20, minH: 18.144, @@ -206,7 +218,7 @@ const initialLayouts = { { i: '8', x: 0, - y: 4, + y: 5, w: 49.5, h: 20, minH: 18.144, @@ -215,7 +227,7 @@ const initialLayouts = { { i: '9', x: 51, - y: 4, + y: 5, w: 49.5, h: 20, minH: 18.144, @@ -224,7 +236,7 @@ const initialLayouts = { { i: '10', x: 0, - y: 5, + y: 6, w: 49.5, h: 20, minH: 18.144, @@ -314,6 +326,15 @@ const initialLayouts = { minH: 18.144, minW: 18, }, + { + i: '11', + x: 0, + y: 1, + w: 40, + h: 30, + minH: 10, + minW: 10, + }, ], sm: [ { i: '1', x: 80, y: 0, w: 100, h: 2.8, isResizable: false, static: true }, @@ -398,6 +419,15 @@ const initialLayouts = { minH: 18.144, minW: 18, }, + { + i: '11', + x: 0, + y: 1, + w: 40, + h: 30, + minH: 10, + minW: 10, + }, ], xs: [ { i: '1', x: 80, y: 0, w: 100, h: 2.8, isResizable: false, static: true }, @@ -482,6 +512,15 @@ const initialLayouts = { minH: 18.144, minW: 18, }, + { + i: '11', + x: 0, + y: 1, + w: 40, + h: 30, + minH: 10, + minW: 10, + }, ], xxs: [ { i: '1', x: 80, y: 0, w: 100, h: 2.8, isResizable: false, static: true }, @@ -566,6 +605,15 @@ const initialLayouts = { minH: 18.144, minW: 18, }, + { + i: '11', + x: 0, + y: 1, + w: 40, + h: 30, + minH: 10, + minW: 10, + }, ], }; @@ -674,6 +722,36 @@ export function ResizableDashboardCards() { })); }; + const updateLayoutHeight = () => { + const currentHeight = + document.getElementById('cardsContainer')?.clientHeight; + + setLayouts((currentLayouts) => { + const updatedLayouts = cloneDeep(currentLayouts); + + const cardsNumbers = + user?.company_user?.settings.dashboard_fields?.length; + + Object.keys(updatedLayouts).forEach((breakpoint) => { + updatedLayouts[breakpoint] = updatedLayouts[breakpoint].map((item) => + item.i === '1' + ? { + ...item, + h: + currentHeight && cardsNumbers + ? (currentHeight + 80) / 30 + : cardsNumbers + ? 6.3 + : 0, + } + : item + ); + }); + + return updatedLayouts; + }); + }; + const handleUpdateUserPreferences = () => { const updatedUser = cloneDeep(user) as User; @@ -683,6 +761,9 @@ export function ResizableDashboardCards() { cloneDeep(layouts) ); + // delete updatedUser.company_user.react_settings + // .dashboard_cards_configuration; + request( 'PUT', endpoint('/api/v1/company_users/:id', { id: updatedUser.id }), @@ -740,13 +821,22 @@ export function ResizableDashboardCards() { useEffect(() => { if (layoutBreakpoint) { if (settings?.dashboard_cards_configuration && !isLayoutsInitialized) { - setLayouts(cloneDeep(settings?.dashboard_cards_configuration)); + //setLayouts(cloneDeep(settings?.dashboard_cards_configuration)); } setIsLayoutsInitialized(true); } }, [layoutBreakpoint]); + useEffect(() => { + if (isLayoutsInitialized) { + updateLayoutHeight(); + } + }, [ + user?.company_user?.settings.dashboard_fields?.length, + isLayoutsInitialized, + ]); + useDebounce( () => { if ( @@ -805,7 +895,7 @@ export function ResizableDashboardCards() { {/* Quick date, currency & date picker. */} -
+
{currencies && (
+ + {isEditMode && ( +
+ layoutBreakpoint && + setLayouts((currentLayouts) => ({ + ...currentLayouts, + [layoutBreakpoint]: + initialLayouts[ + layoutBreakpoint as keyof typeof initialLayouts + ], + })) + } + > + +
+ )}
- {/* */} + {Boolean(user?.company_user?.settings.dashboard_fields?.length) && ( +
+ {user?.company_user?.settings.dashboard_fields?.map( + (field, index) => ( + + ) + )} +
+ )} {company && (
From 2b65910f88252e33d00a7dd75fabf15f2b130c52 Mon Sep 17 00:00:00 2001 From: Civolilah Date: Wed, 2 Oct 2024 18:17:22 +0200 Subject: [PATCH 13/36] Merge branch 'develop' into feature/1605-dashboard-charts --- .../helpers/dates/date-format-resolver.ts | 25 +++++ src/common/hooks/useGetCurrencySeparators.ts | 106 +++++++++++------- src/common/hooks/useNumericFormatter.ts | 57 ++++++++++ src/common/hooks/useUserNumberPrecision.ts | 21 ++++ src/components/forms/NumberInputField.tsx | 96 ++++++++-------- .../useResolveDateAndTimeClientFormat.ts | 50 +++++++++ .../create/components/AdditionalInfo.tsx | 2 +- .../common/hooks/useInvoiceProducts.ts | 10 +- .../common/hooks/usePurchaseOrderProducts.ts | 6 +- .../common/hooks/useInvoiceProject.tsx | 77 ++++++++++--- .../components/AdditionalInfo.tsx | 6 +- .../common/hooks/useBankAccountColumns.tsx | 37 +++--- .../tasks/common/components/TaskSlider.tsx | 5 +- .../common/hooks/useAddTasksOnInvoice.ts | 74 +++++++++--- .../tasks/common/hooks/useInvoiceTask.ts | 72 +++++++++--- .../components/TransactionForm.tsx | 2 +- 16 files changed, 485 insertions(+), 161 deletions(-) create mode 100644 src/common/helpers/dates/date-format-resolver.ts create mode 100644 src/common/hooks/useNumericFormatter.ts create mode 100644 src/common/hooks/useUserNumberPrecision.ts create mode 100644 src/pages/clients/common/hooks/useResolveDateAndTimeClientFormat.ts diff --git a/src/common/helpers/dates/date-format-resolver.ts b/src/common/helpers/dates/date-format-resolver.ts new file mode 100644 index 0000000000..3135b217f7 --- /dev/null +++ b/src/common/helpers/dates/date-format-resolver.ts @@ -0,0 +1,25 @@ +/** + * Invoice Ninja (https://invoiceninja.com). + * + * @link https://github.com/invoiceninja/invoiceninja source repository + * + * @copyright Copyright (c) 2022. Invoice Ninja LLC (https://invoiceninja.com) + * + * @license https://www.elastic.co/licensing/elastic-license + */ + +import { useStaticsQuery } from '../../queries/statics'; + +export function useResolveDateFormat() { + const statics = useStaticsQuery(); + + return (id: string) => { + if (statics) { + return statics.data?.date_formats.find( + (dateFormat) => dateFormat.id === id + ); + } + + return undefined; + }; +} diff --git a/src/common/hooks/useGetCurrencySeparators.ts b/src/common/hooks/useGetCurrencySeparators.ts index 10ba41c5a1..c84335f44b 100644 --- a/src/common/hooks/useGetCurrencySeparators.ts +++ b/src/common/hooks/useGetCurrencySeparators.ts @@ -21,7 +21,7 @@ import { useResolveCountry } from './useResolveCountry'; import { useVendorResolver } from './vendors/useVendorResolver'; export function useGetCurrencySeparators( - setInputCurrencySeparators: React.Dispatch< + setInputCurrencySeparators?: React.Dispatch< React.SetStateAction > ) { @@ -33,61 +33,87 @@ export function useGetCurrencySeparators( const currencyResolver = useCurrencyResolver(); const resolveCountry = useResolveCountry(); - return (relationId: string, relationType: RelationType) => { + return async (relationId: string, relationType: RelationType) => { + let separators: DecimalInputSeparators | undefined; + if (relationId.length >= 1 && relationType === 'client_id') { - clientResolver.find(relationId).then((client: Client) => - currencyResolver + await clientResolver.find(relationId).then(async (client: Client) => { + await currencyResolver .find(client.settings.currency_id || company.settings?.currency_id) .then((currency: Currency | undefined) => { const companyCountry = resolveCountry(company.settings.country_id); - currency && - setInputCurrencySeparators({ - thousandSeparator: - companyCountry?.thousand_separator || - currency.thousand_separator, - decimalSeparator: - companyCountry?.decimal_separator || - currency.decimal_separator, - precision: currency.precision, - }); - }) - ); + const currentSeparators = { + thousandSeparator: + companyCountry?.thousand_separator || + currency?.thousand_separator || + ',', + decimalSeparator: + companyCountry?.decimal_separator || + currency?.decimal_separator || + '.', + precision: currency?.precision || 2, + }; + + if (setInputCurrencySeparators) { + setInputCurrencySeparators(currentSeparators); + } else { + separators = currentSeparators; + } + }); + }); } else if (relationId.length >= 1 && relationType === 'vendor_id') { - vendorResolver.find(relationId).then((vendor: Vendor) => - currencyResolver + await vendorResolver.find(relationId).then(async (vendor: Vendor) => { + await currencyResolver .find(vendor.currency_id || company.settings?.currency_id) .then((currency: Currency | undefined) => { const companyCountry = resolveCountry(company.settings.country_id); - currency && - setInputCurrencySeparators({ - thousandSeparator: - companyCountry?.thousand_separator || - currency.thousand_separator, - decimalSeparator: - companyCountry?.decimal_separator || - currency.decimal_separator, - precision: currency.precision, - }); - }) - ); + const currentSeparators = { + thousandSeparator: + companyCountry?.thousand_separator || + currency?.thousand_separator || + ',', + decimalSeparator: + companyCountry?.decimal_separator || + currency?.decimal_separator || + '.', + precision: currency?.precision || 2, + }; + + if (setInputCurrencySeparators) { + setInputCurrencySeparators(currentSeparators); + } else { + separators = currentSeparators; + } + }); + }); } else { - currencyResolver + await currencyResolver .find(company.settings?.currency_id) .then((currency: Currency | undefined) => { const companyCountry = resolveCountry(company.settings.country_id); - currency && - setInputCurrencySeparators({ - thousandSeparator: - companyCountry?.thousand_separator || - currency.thousand_separator, - decimalSeparator: - companyCountry?.decimal_separator || currency.decimal_separator, - precision: currency.precision, - }); + const currentSeparators = { + thousandSeparator: + companyCountry?.thousand_separator || + currency?.thousand_separator || + ',', + decimalSeparator: + companyCountry?.decimal_separator || + currency?.decimal_separator || + '.', + precision: currency?.precision || 2, + }; + + if (setInputCurrencySeparators) { + setInputCurrencySeparators(currentSeparators); + } else { + separators = currentSeparators; + } }); } + + return separators; }; } diff --git a/src/common/hooks/useNumericFormatter.ts b/src/common/hooks/useNumericFormatter.ts new file mode 100644 index 0000000000..c383b36355 --- /dev/null +++ b/src/common/hooks/useNumericFormatter.ts @@ -0,0 +1,57 @@ +/** + * Invoice Ninja (https://invoiceninja.com). + * + * @link https://github.com/invoiceninja/invoiceninja source repository + * + * @copyright Copyright (c) 2022. Invoice Ninja LLC (https://invoiceninja.com) + * + * @license https://www.elastic.co/licensing/elastic-license + */ + +import { numericFormatter } from 'react-number-format'; +import { useCurrentCompany } from './useCurrentCompany'; +import { useUserNumberPrecision } from './useUserNumberPrecision'; + +export function useNumericFormatter() { + const company = useCurrentCompany(); + const userNumberPrecision = useUserNumberPrecision(); + + const getThousandSeparator = ( + currentThousandSeparator: string | undefined + ) => { + if (currentThousandSeparator) { + return currentThousandSeparator; + } + + if (company?.use_comma_as_decimal_place) { + return '.'; + } + + return ','; + }; + + const getDecimalSeparator = (currentDecimalSeparator: string | undefined) => { + if (currentDecimalSeparator) { + return currentDecimalSeparator; + } + + if (company?.use_comma_as_decimal_place) { + return ','; + } + + return '.'; + }; + + return ( + numStr: string, + thousandSeparator?: string, + decimalSeparator?: string, + precision?: number + ) => { + return numericFormatter(numStr, { + thousandSeparator: getThousandSeparator(thousandSeparator), + decimalSeparator: getDecimalSeparator(decimalSeparator), + decimalScale: precision || userNumberPrecision, + }); + }; +} diff --git a/src/common/hooks/useUserNumberPrecision.ts b/src/common/hooks/useUserNumberPrecision.ts new file mode 100644 index 0000000000..8bc47455e2 --- /dev/null +++ b/src/common/hooks/useUserNumberPrecision.ts @@ -0,0 +1,21 @@ +/** + * Invoice Ninja (https://invoiceninja.com). + * + * @link https://github.com/invoiceninja/invoiceninja source repository + * + * @copyright Copyright (c) 2022. Invoice Ninja LLC (https://invoiceninja.com) + * + * @license https://www.elastic.co/licensing/elastic-license + */ + +import { useReactSettings } from './useReactSettings'; + +export function useUserNumberPrecision() { + const reactSettings = useReactSettings(); + + return reactSettings?.number_precision && + reactSettings?.number_precision > 0 && + reactSettings?.number_precision <= 100 + ? reactSettings.number_precision + : 2; +} diff --git a/src/components/forms/NumberInputField.tsx b/src/components/forms/NumberInputField.tsx index fb7cf9458a..241df30d6d 100644 --- a/src/components/forms/NumberInputField.tsx +++ b/src/components/forms/NumberInputField.tsx @@ -49,10 +49,36 @@ export function NumberInputField(props: Props) { : undefined ); + const getNumberPrecision = () => { + if (typeof props.precision === 'number') { + return props.precision; + } + + if ( + reactSettings?.number_precision && + reactSettings?.number_precision > 0 && + reactSettings?.number_precision <= 100 + ) { + return reactSettings.number_precision; + } + + return 2; + }; + + const getDecimalSeparator = () => { + return company?.use_comma_as_decimal_place ? ',' : '.'; + }; + + const getThousandSeparator = () => { + return company?.use_comma_as_decimal_place ? '.' : ','; + }; + useDebounce( () => { - if (props.onValueChange && currentValue) { - props.onValueChange(String(currentValue)); + if (props.onValueChange) { + props.onValueChange( + typeof currentValue === 'number' ? String(currentValue) : '' + ); } }, 500, @@ -60,9 +86,7 @@ export function NumberInputField(props: Props) { ); useEffect(() => { - if (props.value) { - setCurrentValue(parseFloat(String(props.value))); - } + setCurrentValue(props.value ? parseFloat(String(props.value)) : undefined); }, [props.value]); if (import.meta.env.VITE_RETURN_NUMBER_FIELD === 'true') { @@ -110,23 +134,18 @@ export function NumberInputField(props: Props) { 'border border-gray-300': props.border !== false, } )} - value={currentValue} + value={currentValue || ''} placeholder={props.placeholder ?? undefined} onChange={(event) => { if (props.onValueChange && props.changeOverride) { - const formattedValue = currency(event.target.value, { - separator: company?.use_comma_as_decimal_place ? '.' : ',', - decimal: company?.use_comma_as_decimal_place ? ',' : '.', - symbol: '', - precision: - typeof props.precision === 'number' - ? props.precision - : reactSettings?.number_precision && - reactSettings?.number_precision > 0 && - reactSettings?.number_precision <= 100 - ? reactSettings.number_precision - : 2, - }).value; + const formattedValue = event.target.value + ? currency(event.target.value, { + separator: getThousandSeparator(), + decimal: getDecimalSeparator(), + symbol: '', + precision: getNumberPrecision(), + }).value + : undefined; setCurrentValue(formattedValue); } @@ -134,35 +153,22 @@ export function NumberInputField(props: Props) { onBlur={(event) => { if (props.onValueChange && !props.changeOverride) { props.onValueChange( - String( - currency(event.target.value, { - separator: company?.use_comma_as_decimal_place ? '.' : ',', - decimal: company?.use_comma_as_decimal_place ? ',' : '.', - symbol: '', - precision: - typeof props.precision === 'number' - ? props.precision - : reactSettings?.number_precision && - reactSettings?.number_precision > 0 && - reactSettings?.number_precision <= 100 - ? reactSettings.number_precision - : 2, - }).value - ) + event.target.value + ? String( + currency(event.target.value, { + separator: getThousandSeparator(), + decimal: getDecimalSeparator(), + symbol: '', + precision: getNumberPrecision(), + }).value + ) + : '' ); } }} - thousandSeparator={company?.use_comma_as_decimal_place ? '.' : ','} - decimalSeparator={company?.use_comma_as_decimal_place ? ',' : '.'} - decimalScale={ - typeof props.precision === 'number' - ? props.precision - : reactSettings?.number_precision && - reactSettings?.number_precision > 0 && - reactSettings?.number_precision <= 100 - ? reactSettings.number_precision - : 2 - } + thousandSeparator={getThousandSeparator()} + decimalSeparator={getDecimalSeparator()} + decimalScale={getNumberPrecision()} allowNegative style={{ backgroundColor: colors.$1, diff --git a/src/pages/clients/common/hooks/useResolveDateAndTimeClientFormat.ts b/src/pages/clients/common/hooks/useResolveDateAndTimeClientFormat.ts new file mode 100644 index 0000000000..c9dcd1e638 --- /dev/null +++ b/src/pages/clients/common/hooks/useResolveDateAndTimeClientFormat.ts @@ -0,0 +1,50 @@ +/** + * Invoice Ninja (https://invoiceninja.com). + * + * @link https://github.com/invoiceninja/invoiceninja source repository + * + * @copyright Copyright (c) 2022. Invoice Ninja LLC (https://invoiceninja.com) + * + * @license https://www.elastic.co/licensing/elastic-license + */ + +import { useResolveDateFormat } from '$app/common/helpers/dates/date-format-resolver'; +import { useClientResolver } from '$app/common/hooks/clients/useClientResolver'; +import { useCurrentCompany } from '$app/common/hooks/useCurrentCompany'; +import { Client } from '$app/common/interfaces/client'; + +export function useResolveDateAndTimeClientFormat() { + const company = useCurrentCompany(); + const clientResolver = useClientResolver(); + + const resolveDateFormat = useResolveDateFormat(); + + const getTimeFormat = (militaryTime: boolean) => { + return militaryTime ? 'HH:mm:ss' : 'hh:mm:ss A'; + }; + + return async (relationId: string) => { + const dateFormat = resolveDateFormat(company?.settings.date_format_id); + + const dateTimeFormats = { + dateFormat, + timeFormat: getTimeFormat(Boolean(company?.settings.military_time)), + }; + + if (relationId.length >= 1) { + await clientResolver.find(relationId).then((client: Client) => { + if (client.settings.date_format_id) { + dateTimeFormats.dateFormat = resolveDateFormat( + client.settings.date_format_id + ); + } + + dateTimeFormats.timeFormat = getTimeFormat( + Boolean(client.settings.military_time) + ); + }); + } + + return dateTimeFormats; + }; +} diff --git a/src/pages/expenses/create/components/AdditionalInfo.tsx b/src/pages/expenses/create/components/AdditionalInfo.tsx index 7e71189402..9a85e1227c 100644 --- a/src/pages/expenses/create/components/AdditionalInfo.tsx +++ b/src/pages/expenses/create/components/AdditionalInfo.tsx @@ -250,7 +250,7 @@ export function AdditionalInfo(props: ExpenseCardProps) { } className="auto" value={(expense.foreign_amount || 0).toString()} - onChange={(value: string) => + onValueChange={(value: string) => onConvertedAmountChange(parseFloat(value)) } errorMessage={errors?.errors.foreign_amount} diff --git a/src/pages/products/common/hooks/useInvoiceProducts.ts b/src/pages/products/common/hooks/useInvoiceProducts.ts index df9ee21116..28d1e62689 100644 --- a/src/pages/products/common/hooks/useInvoiceProducts.ts +++ b/src/pages/products/common/hooks/useInvoiceProducts.ts @@ -10,6 +10,7 @@ import { blankLineItem } from '$app/common/constants/blank-line-item'; import { useCurrentCompany } from '$app/common/hooks/useCurrentCompany'; +import { useUserNumberPrecision } from '$app/common/hooks/useUserNumberPrecision'; import { InvoiceItemType } from '$app/common/interfaces/invoice-item'; import { Product } from '$app/common/interfaces/product'; import { useBlankInvoiceQuery } from '$app/common/queries/invoices'; @@ -22,11 +23,12 @@ interface Params { } export const useInvoiceProducts = (params?: Params) => { - const navigate = useNavigate(); - const { onlyAddToInvoice } = params || {}; + const navigate = useNavigate(); + const company = useCurrentCompany(); + const userNumberPrecision = useUserNumberPrecision(); const { data: blankInvoice } = useBlankInvoiceQuery(); @@ -40,7 +42,9 @@ export const useInvoiceProducts = (params?: Params) => { product_key: product.product_key, quantity: company?.fill_products ? product.quantity : 1, ...(company?.fill_products && { - line_total: Number((product.price * product.quantity).toFixed(2)), + line_total: Number( + (product.price * product.quantity).toFixed(userNumberPrecision) + ), cost: product.price, notes: product.notes, tax_name1: product.tax_name1, diff --git a/src/pages/products/common/hooks/usePurchaseOrderProducts.ts b/src/pages/products/common/hooks/usePurchaseOrderProducts.ts index 1da5f997ec..73474e17a3 100644 --- a/src/pages/products/common/hooks/usePurchaseOrderProducts.ts +++ b/src/pages/products/common/hooks/usePurchaseOrderProducts.ts @@ -10,6 +10,7 @@ import { blankLineItem } from '$app/common/constants/blank-line-item'; import { useCurrentCompany } from '$app/common/hooks/useCurrentCompany'; +import { useUserNumberPrecision } from '$app/common/hooks/useUserNumberPrecision'; import { InvoiceItemType } from '$app/common/interfaces/invoice-item'; import { Product } from '$app/common/interfaces/product'; import { useBlankPurchaseOrderQuery } from '$app/common/queries/purchase-orders'; @@ -21,6 +22,7 @@ export const usePurchaseOrderProducts = () => { const navigate = useNavigate(); const company = useCurrentCompany(); + const userNumberPrecision = useUserNumberPrecision(); const { data: blankPurchaseOrder } = useBlankPurchaseOrderQuery(); @@ -34,7 +36,9 @@ export const usePurchaseOrderProducts = () => { product_key: product.product_key, quantity: company?.fill_products ? product.quantity : 1, ...(company?.fill_products && { - line_total: Number((product.price * product.quantity).toFixed(2)), + line_total: Number( + (product.price * product.quantity).toFixed(userNumberPrecision) + ), cost: product.price, notes: product.notes, tax_name1: product.tax_name1, diff --git a/src/pages/projects/common/hooks/useInvoiceProject.tsx b/src/pages/projects/common/hooks/useInvoiceProject.tsx index 6895b00683..af279a3930 100644 --- a/src/pages/projects/common/hooks/useInvoiceProject.tsx +++ b/src/pages/projects/common/hooks/useInvoiceProject.tsx @@ -28,8 +28,12 @@ import { useSetAtom } from 'jotai'; import { useCompanyTimeFormat } from '$app/common/hooks/useCompanyTimeFormat'; import { toast } from '$app/common/helpers/toast/toast'; import { useTranslation } from 'react-i18next'; +import { useNumericFormatter } from '$app/common/hooks/useNumericFormatter'; +import { useUserNumberPrecision } from '$app/common/hooks/useUserNumberPrecision'; +import { useGetCurrencySeparators } from '$app/common/hooks/useGetCurrencySeparators'; +import { useResolveDateAndTimeClientFormat } from '$app/pages/clients/common/hooks/useResolveDateAndTimeClientFormat'; -export const calculateTaskHours = (timeLog: string) => { +export const calculateTaskHours = (timeLog: string, precision?: number) => { const parsedTimeLogs = parseTimeLog(timeLog); let hoursSum = 0; @@ -41,7 +45,7 @@ export const calculateTaskHours = (timeLog: string) => { const unixStop = dayjs.unix(stop); hoursSum += Number( - (unixStop.diff(unixStart, 'seconds') / 3600).toFixed(4) + (unixStop.diff(unixStart, 'seconds') / 3600).toFixed(precision) ); } }); @@ -53,15 +57,21 @@ export const calculateTaskHours = (timeLog: string) => { export function useInvoiceProject() { const [t] = useTranslation(); const navigate = useNavigate(); + + const numericFormatter = useNumericFormatter(); + const getCurrencySeparators = useGetCurrencySeparators(); + const resolveDateAndTimeClientFormat = useResolveDateAndTimeClientFormat(); + const company = useCurrentCompany(); + const userNumberPrecision = useUserNumberPrecision(); - const { dateFormat } = useCurrentCompanyDateFormats(); - const { timeFormat } = useCompanyTimeFormat(); const { data } = useBlankInvoiceQuery(); + const { timeFormat } = useCompanyTimeFormat(); + const { dateFormat } = useCurrentCompanyDateFormats(); const setInvoice = useSetAtom(invoiceAtom); - return (tasks: Task[], clientId: string, projectId: string) => { + return async (tasks: Task[], clientId: string, projectId: string) => { if (data) { const invoice: Invoice = { ...data }; @@ -90,6 +100,14 @@ export function useInvoiceProject() { invoice.client_id = clientId; invoice.line_items = []; + const currencySeparators = await getCurrencySeparators( + clientId, + 'client_id' + ); + + const { dateFormat: clientDateFormat, timeFormat: clientTimeFormat } = + await resolveDateAndTimeClientFormat(clientId); + tasks.forEach((task: Task) => { const logs = parseTimeLog(task.time_log); const parsed: string[] = []; @@ -106,9 +124,12 @@ export function useInvoiceProject() { const unixStart = dayjs.unix(start); const unixStop = dayjs.unix(stop); - const hours = ( - unixStop.diff(unixStart, 'seconds') / 3600 - ).toFixed(4); + const hours = numericFormatter( + (unixStop.diff(unixStart, 'seconds') / 3600).toString(), + currencySeparators?.thousandSeparator, + currencySeparators?.decimalSeparator, + currencySeparators?.precision + ); hoursDescription = `• ${hours} ${t('hours')}`; } @@ -116,30 +137,49 @@ export function useInvoiceProject() { const description = []; if (company.invoice_task_datelog || company.invoice_task_timelog) { - description.push('
'); + description.push('
\n'); } if (company.invoice_task_datelog) { - description.push(dayjs.unix(start).format(dateFormat)); + description.push( + dayjs + .unix(start) + .format( + clientDateFormat?.format_moment + ? clientDateFormat.format_moment + : dateFormat + ) + ); } if (company.invoice_task_timelog) { - description.push(dayjs.unix(start).format(timeFormat) + ' - '); + description.push( + dayjs + .unix(start) + .format(clientTimeFormat ? clientTimeFormat : timeFormat) + + ' - ' + ); } if (company.invoice_task_timelog) { - description.push(dayjs.unix(stop).format(timeFormat)); + description.push( + dayjs + .unix(stop) + .format(clientTimeFormat ? clientTimeFormat : timeFormat) + ); } if (company.invoice_task_hours) { description.push(hoursDescription); } - if (company.invoice_task_item_description) { - description.push(intervalDescription); + if (company.invoice_task_item_description && intervalDescription) { + description.push(`\n\n${intervalDescription}`); } if (company.invoice_task_datelog || company.invoice_task_timelog) { + description.push('\n'); + description.push('
\n'); } @@ -147,14 +187,19 @@ export function useInvoiceProject() { } }); - const taskQuantity = calculateTaskHours(task.time_log); + const taskQuantity = calculateTaskHours( + task.time_log, + userNumberPrecision + ); const item: InvoiceItem = { ...blankLineItem(), type_id: InvoiceItemType.Task, cost: task.rate, quantity: taskQuantity, - line_total: Number((task.rate * taskQuantity).toFixed(2)), + line_total: Number( + (task.rate * taskQuantity).toFixed(userNumberPrecision) + ), task_id: task.id, tax_id: '', }; diff --git a/src/pages/recurring-expenses/components/AdditionalInfo.tsx b/src/pages/recurring-expenses/components/AdditionalInfo.tsx index 1280237886..81750a6bb3 100644 --- a/src/pages/recurring-expenses/components/AdditionalInfo.tsx +++ b/src/pages/recurring-expenses/components/AdditionalInfo.tsx @@ -239,8 +239,8 @@ export function AdditionalInfo(props: RecurringExpenseCardProps) { - handleChange('exchange_rate', parseFloat(value)) } @@ -260,7 +260,7 @@ export function AdditionalInfo(props: RecurringExpenseCardProps) { } className="auto" value={(recurringExpense.foreign_amount || 0).toString()} - onChange={(value: string) => + onValueChange={(value: string) => onConvertedAmountChange(parseFloat(value)) } errorMessage={errors?.errors.foreign_amount} diff --git a/src/pages/settings/bank-accounts/common/hooks/useBankAccountColumns.tsx b/src/pages/settings/bank-accounts/common/hooks/useBankAccountColumns.tsx index 5ad2d663a6..07c703119b 100644 --- a/src/pages/settings/bank-accounts/common/hooks/useBankAccountColumns.tsx +++ b/src/pages/settings/bank-accounts/common/hooks/useBankAccountColumns.tsx @@ -21,10 +21,6 @@ import { endpoint } from '$app/common/helpers'; import { MdWarning } from 'react-icons/md'; import { Tooltip } from '$app/components/Tooltip'; -enum IntegrationType { - Yodlee = 'YODLEE', - Nordigen = 'NORDIGEN', -} export const useBankAccountColumns = () => { const { t } = useTranslation(); const company = useCurrentCompany(); @@ -60,24 +56,23 @@ export const useBankAccountColumns = () => { {bankAccount?.bank_account_name} - {bankAccount.integration_type === IntegrationType.Nordigen && - bankAccount.disabled_upstream && ( - +
{ + event.stopPropagation(); + handleConnectNordigen(bankAccount.nordigen_institution_id); + }} > -
{ - event.stopPropagation(); - handleConnectNordigen(bankAccount.nordigen_institution_id); - }} - > - -
- - )} + +
+
+ )}
), }, diff --git a/src/pages/tasks/common/components/TaskSlider.tsx b/src/pages/tasks/common/components/TaskSlider.tsx index 92beeb2cd8..9341c930a5 100644 --- a/src/pages/tasks/common/components/TaskSlider.tsx +++ b/src/pages/tasks/common/components/TaskSlider.tsx @@ -46,6 +46,7 @@ import { calculateTaskHours } from '$app/pages/projects/common/hooks/useInvoiceP import { date as formatDate } from '$app/common/helpers'; import { useFormatTimeLog } from '../../kanban/common/hooks'; import { TaskClock } from '../../kanban/components/TaskClock'; +import { useUserNumberPrecision } from '$app/common/hooks/useUserNumberPrecision'; export const taskSliderAtom = atom(null); export const taskSliderVisibilityAtom = atom(false); @@ -104,6 +105,7 @@ export function TaskSlider() { showEditAction: true, }); const { dateFormat } = useCurrentCompanyDateFormats(); + const userNumberPrecision = useUserNumberPrecision(); const formatMoney = useFormatMoney(); const formatTimeLog = useFormatTimeLog(); @@ -157,7 +159,8 @@ export function TaskSlider() { {task ? formatMoney( - task.rate * calculateTaskHours(task.time_log), + task.rate * + calculateTaskHours(task.time_log, userNumberPrecision), task.client?.country_id, task.client?.settings.currency_id ) diff --git a/src/pages/tasks/common/hooks/useAddTasksOnInvoice.ts b/src/pages/tasks/common/hooks/useAddTasksOnInvoice.ts index 2f36db29bf..9c5ff59af3 100644 --- a/src/pages/tasks/common/hooks/useAddTasksOnInvoice.ts +++ b/src/pages/tasks/common/hooks/useAddTasksOnInvoice.ts @@ -27,6 +27,10 @@ import { Task } from '$app/common/interfaces/task'; import { useCurrentCompany } from '$app/common/hooks/useCurrentCompany'; import { cloneDeep } from 'lodash'; import { useTranslation } from 'react-i18next'; +import { useNumericFormatter } from '$app/common/hooks/useNumericFormatter'; +import { useUserNumberPrecision } from '$app/common/hooks/useUserNumberPrecision'; +import { useGetCurrencySeparators } from '$app/common/hooks/useGetCurrencySeparators'; +import { useResolveDateAndTimeClientFormat } from '$app/pages/clients/common/hooks/useResolveDateAndTimeClientFormat'; interface Params { tasks: Task[]; @@ -34,20 +38,33 @@ interface Params { export function useAddTasksOnInvoice(params: Params) { const [t] = useTranslation(); - const navigate = useNavigate(); const { tasks } = params; - const company = useCurrentCompany(); - const { dateFormat } = useCurrentCompanyDateFormats(); + const navigate = useNavigate(); + const numericFormatter = useNumericFormatter(); + const getCurrencySeparators = useGetCurrencySeparators(); + const resolveDateAndTimeClientFormat = useResolveDateAndTimeClientFormat(); + + const company = useCurrentCompany(); const { timeFormat } = useCompanyTimeFormat(); + const userNumberPrecision = useUserNumberPrecision(); + const { dateFormat } = useCurrentCompanyDateFormats(); const setInvoiceAtom = useSetAtom(invoiceAtom); - return (invoice: Invoice) => { + return async (invoice: Invoice) => { const updatedInvoice = cloneDeep(invoice); if (tasks) { + const currencySeparators = await getCurrencySeparators( + tasks[0]?.client_id, + 'client_id' + ); + + const { dateFormat: clientDateFormat, timeFormat: clientTimeFormat } = + await resolveDateAndTimeClientFormat(tasks[0]?.client_id); + tasks.forEach((task: Task) => { const logs = parseTimeLog(task.time_log); const parsed: string[] = []; @@ -64,9 +81,12 @@ export function useAddTasksOnInvoice(params: Params) { const unixStart = dayjs.unix(start); const unixStop = dayjs.unix(stop); - const hours = ( - unixStop.diff(unixStart, 'seconds') / 3600 - ).toFixed(4); + const hours = numericFormatter( + (unixStop.diff(unixStart, 'seconds') / 3600).toString(), + currencySeparators?.thousandSeparator, + currencySeparators?.decimalSeparator, + currencySeparators?.precision + ); hoursDescription = `• ${hours} ${t('hours')}`; } @@ -74,30 +94,49 @@ export function useAddTasksOnInvoice(params: Params) { const description = []; if (company.invoice_task_datelog || company.invoice_task_timelog) { - description.push('
'); + description.push('
\n'); } if (company.invoice_task_datelog) { - description.push(dayjs.unix(start).format(dateFormat)); + description.push( + dayjs + .unix(start) + .format( + clientDateFormat?.format_moment + ? clientDateFormat.format_moment + : dateFormat + ) + ); } if (company.invoice_task_timelog) { - description.push(dayjs.unix(start).format(timeFormat) + ' - '); + description.push( + dayjs + .unix(start) + .format(clientTimeFormat ? clientTimeFormat : timeFormat) + + ' - ' + ); } if (company.invoice_task_timelog) { - description.push(dayjs.unix(stop).format(timeFormat)); + description.push( + dayjs + .unix(stop) + .format(clientTimeFormat ? clientTimeFormat : timeFormat) + ); } if (company.invoice_task_hours) { description.push(hoursDescription); } - if (company.invoice_task_item_description) { - description.push(intervalDescription); + if (company.invoice_task_item_description && intervalDescription) { + description.push(`\n\n${intervalDescription}`); } if (company.invoice_task_datelog || company.invoice_task_timelog) { + description.push('\n'); + description.push('
\n'); } @@ -105,14 +144,19 @@ export function useAddTasksOnInvoice(params: Params) { } }); - const taskQuantity = calculateTaskHours(task.time_log); + const taskQuantity = calculateTaskHours( + task.time_log, + userNumberPrecision + ); const item: InvoiceItem = { ...blankLineItem(), type_id: InvoiceItemType.Task, cost: task.rate, quantity: taskQuantity, - line_total: Number((task.rate * taskQuantity).toFixed(2)), + line_total: Number( + (task.rate * taskQuantity).toFixed(userNumberPrecision) + ), task_id: task.id, tax_id: '', custom_value1: task.custom_value1, diff --git a/src/pages/tasks/common/hooks/useInvoiceTask.ts b/src/pages/tasks/common/hooks/useInvoiceTask.ts index ca7a44dcc0..8747fecd61 100644 --- a/src/pages/tasks/common/hooks/useInvoiceTask.ts +++ b/src/pages/tasks/common/hooks/useInvoiceTask.ts @@ -29,6 +29,10 @@ import { useTranslation } from 'react-i18next'; import { toast } from '$app/common/helpers/toast/toast'; import { useCompanyTimeFormat } from '$app/common/hooks/useCompanyTimeFormat'; import { useFormatNumber } from '$app/common/hooks/useFormatNumber'; +import { useUserNumberPrecision } from '$app/common/hooks/useUserNumberPrecision'; +import { useNumericFormatter } from '$app/common/hooks/useNumericFormatter'; +import { useGetCurrencySeparators } from '$app/common/hooks/useGetCurrencySeparators'; +import { useResolveDateAndTimeClientFormat } from '$app/pages/clients/common/hooks/useResolveDateAndTimeClientFormat'; interface Params { onlyAddToInvoice?: boolean; @@ -36,20 +40,25 @@ interface Params { export function useInvoiceTask(params?: Params) { const [t] = useTranslation(); - const navigate = useNavigate(); const { onlyAddToInvoice } = params || {}; + const navigate = useNavigate(); + const numericFormatter = useNumericFormatter(); + const getCurrencySeparators = useGetCurrencySeparators(); + const company = useCurrentCompany(); const { data } = useBlankInvoiceQuery(); const { timeFormat } = useCompanyTimeFormat(); + const userNumberPrecision = useUserNumberPrecision(); const { dateFormat } = useCurrentCompanyDateFormats(); const setInvoice = useSetAtom(invoiceAtom); const formatNumber = useFormatNumber(); + const resolveDateAndTimeClientFormat = useResolveDateAndTimeClientFormat(); - const calculateTaskHours = (timeLog: string) => { + const calculateTaskHours = (timeLog: string, precision?: number) => { const parsedTimeLogs = parseTimeLog(timeLog); let hoursSum = 0; @@ -65,7 +74,7 @@ export function useInvoiceTask(params?: Params) { const unixStop = dayjs.unix(stop); hoursSum += unixStop.diff(unixStart, 'seconds') / 3600; - hoursSum = Number(hoursSum.toFixed(4)); + hoursSum = Number(hoursSum.toFixed(precision || userNumberPrecision)); } }); } @@ -107,6 +116,14 @@ export function useInvoiceTask(params?: Params) { invoice.project_id = tasks[0]?.project_id; } + const currencySeparators = await getCurrencySeparators( + tasks[0]?.client_id, + 'client_id' + ); + + const { dateFormat: clientDateFormat, timeFormat: clientTimeFormat } = + await resolveDateAndTimeClientFormat(tasks[0]?.client_id); + tasks.forEach((task: Task) => { const logs = parseTimeLog(task.time_log); const parsed: string[] = []; @@ -123,9 +140,12 @@ export function useInvoiceTask(params?: Params) { const unixStart = dayjs.unix(start); const unixStop = dayjs.unix(stop); - const hours = ( - unixStop.diff(unixStart, 'seconds') / 3600 - ).toFixed(4); + const hours = numericFormatter( + (unixStop.diff(unixStart, 'seconds') / 3600).toString(), + currencySeparators?.thousandSeparator, + currencySeparators?.decimalSeparator, + currencySeparators?.precision + ); hoursDescription = `• ${formatNumber(hours)} ${t('hours')}`; } @@ -133,30 +153,49 @@ export function useInvoiceTask(params?: Params) { const description = []; if (company.invoice_task_datelog || company.invoice_task_timelog) { - description.push('
'); + description.push('
\n'); } if (company.invoice_task_datelog) { - description.push(dayjs.unix(start).format(dateFormat)); + description.push( + dayjs + .unix(start) + .format( + clientDateFormat?.format_moment + ? clientDateFormat.format_moment + : dateFormat + ) + ); } if (company.invoice_task_timelog) { - description.push(dayjs.unix(start).format(timeFormat) + ' - '); + description.push( + dayjs + .unix(start) + .format(clientTimeFormat ? clientTimeFormat : timeFormat) + + ' - ' + ); } if (company.invoice_task_timelog) { - description.push(dayjs.unix(stop).format(timeFormat)); + description.push( + dayjs + .unix(stop) + .format(clientTimeFormat ? clientTimeFormat : timeFormat) + ); } if (company.invoice_task_hours) { description.push(hoursDescription); } - if (company.invoice_task_item_description) { - description.push(intervalDescription); + if (company.invoice_task_item_description && intervalDescription) { + description.push(`\n\n${intervalDescription}`); } if (company.invoice_task_datelog || company.invoice_task_timelog) { + description.push('\n'); + description.push('
\n'); } @@ -164,14 +203,19 @@ export function useInvoiceTask(params?: Params) { } }); - const taskQuantity = calculateTaskHours(task.time_log); + const taskQuantity = calculateTaskHours( + task.time_log, + userNumberPrecision + ); const item: InvoiceItem = { ...blankLineItem(), type_id: InvoiceItemType.Task, cost: task.rate, quantity: taskQuantity, - line_total: Number((task.rate * taskQuantity).toFixed(2)), + line_total: Number( + (task.rate * taskQuantity).toFixed(userNumberPrecision) + ), task_id: task.id, tax_id: '', custom_value1: task.custom_value1, diff --git a/src/pages/transactions/components/TransactionForm.tsx b/src/pages/transactions/components/TransactionForm.tsx index 9f5842bb30..081fa0998d 100644 --- a/src/pages/transactions/components/TransactionForm.tsx +++ b/src/pages/transactions/components/TransactionForm.tsx @@ -92,7 +92,7 @@ export function TransactionForm(props: Props) { precision={props.currencySeparators.precision} className="auto" value={props.transaction.amount.toString()} - onChange={(value: string) => + onValueChange={(value: string) => props.handleChange('amount', Number(value)) } errorMessage={props.errors?.errors.amount} From 7a70eb4bd5e6b1378741938f371afa0cd8630b1f Mon Sep 17 00:00:00 2001 From: Civolilah Date: Tue, 17 Dec 2024 19:10:43 +0100 Subject: [PATCH 14/36] Added shadcn component --- components.json | 20 ++ components/ui/button.tsx | 56 ++++ lib/utils.ts | 6 + package-lock.json | 290 ++++++++++++------ package.json | 6 + src/components/cards/Card.tsx | 9 +- .../components/ResizableDashboardCards.tsx | 8 + src/resources/css/app.css | 63 ++++ tailwind.config.js | 74 ++++- 9 files changed, 415 insertions(+), 117 deletions(-) create mode 100644 components.json create mode 100644 components/ui/button.tsx create mode 100644 lib/utils.ts diff --git a/components.json b/components.json new file mode 100644 index 0000000000..289139b33f --- /dev/null +++ b/components.json @@ -0,0 +1,20 @@ +{ + "$schema": "https://ui.shadcn.com/schema.json", + "style": "default", + "rsc": false, + "tsx": true, + "tailwind": { + "config": "tailwind.config.js", + "css": "src/resources/css/app.css", + "baseColor": "gray", + "cssVariables": true, + "prefix": "" + }, + "aliases": { + "components": "~/components", + "utils": "~/lib/utils", + "ui": "~/components/ui", + "lib": "~/lib", + "hooks": "~/hooks" + } +} \ No newline at end of file diff --git a/components/ui/button.tsx b/components/ui/button.tsx new file mode 100644 index 0000000000..d754ca03b9 --- /dev/null +++ b/components/ui/button.tsx @@ -0,0 +1,56 @@ +import * as React from "react" +import { Slot } from "@radix-ui/react-slot" +import { cva, type VariantProps } from "class-variance-authority" + +import { cn } from "~/lib/utils" + +const buttonVariants = cva( + "inline-flex items-center justify-center whitespace-nowrap rounded-md text-sm font-medium ring-offset-background transition-colors focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:pointer-events-none disabled:opacity-50", + { + variants: { + variant: { + default: "bg-primary text-primary-foreground hover:bg-primary/90", + destructive: + "bg-destructive text-destructive-foreground hover:bg-destructive/90", + outline: + "border border-input bg-background hover:bg-accent hover:text-accent-foreground", + secondary: + "bg-secondary text-secondary-foreground hover:bg-secondary/80", + ghost: "hover:bg-accent hover:text-accent-foreground", + link: "text-primary underline-offset-4 hover:underline", + }, + size: { + default: "h-10 px-4 py-2", + sm: "h-9 rounded-md px-3", + lg: "h-11 rounded-md px-8", + icon: "h-10 w-10", + }, + }, + defaultVariants: { + variant: "default", + size: "default", + }, + } +) + +export interface ButtonProps + extends React.ButtonHTMLAttributes, + VariantProps { + asChild?: boolean +} + +const Button = React.forwardRef( + ({ className, variant, size, asChild = false, ...props }, ref) => { + const Comp = asChild ? Slot : "button" + return ( + + ) + } +) +Button.displayName = "Button" + +export { Button, buttonVariants } diff --git a/lib/utils.ts b/lib/utils.ts new file mode 100644 index 0000000000..bd0c391ddd --- /dev/null +++ b/lib/utils.ts @@ -0,0 +1,6 @@ +import { clsx, type ClassValue } from "clsx" +import { twMerge } from "tailwind-merge" + +export function cn(...inputs: ClassValue[]) { + return twMerge(clsx(inputs)) +} diff --git a/package-lock.json b/package-lock.json index c91872673f..7085fdcff8 100644 --- a/package-lock.json +++ b/package-lock.json @@ -14,6 +14,7 @@ "@headlessui/tailwindcss": "^0.1.3", "@hello-pangea/dnd": "^16.2.0", "@monaco-editor/react": "^4.4.6", + "@radix-ui/react-slot": "^1.1.0", "@react-oauth/google": "^0.9.0", "@reduxjs/toolkit": "^1.9.1", "@sentry/react": "^7.77.0", @@ -23,7 +24,9 @@ "antd": "^5.6.1", "array-move": "^4.0.0", "axios": "^0.27.2", + "class-variance-authority": "^0.7.0", "classnames": "^2.3.2", + "clsx": "^2.1.1", "collect.js": "^4.34.3", "currency.js": "^2.0.4", "dayjs": "^1.11.7", @@ -37,6 +40,7 @@ "jotai": "^2.0.3", "js-sha256": "^0.11.0", "lodash": "^4.17.21", + "lucide-react": "^0.447.0", "mitt": "^3.0.1", "playwright": "^1.44.0", "pretty-bytes": "^6.0.0", @@ -74,7 +78,9 @@ "remove": "^0.1.5", "socket.io-client": "^4.7.5", "styled-components": "^6.0.7", + "tailwind-merge": "^2.5.2", "tailwind-scrollbar": "^3.0.0", + "tailwindcss-animate": "^1.0.7", "uuid": "^9.0.0" }, "devDependencies": { @@ -244,12 +250,13 @@ } }, "node_modules/@babel/code-frame": { - "version": "7.22.13", - "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.22.13.tgz", - "integrity": "sha512-XktuhWlJ5g+3TJXc5upd9Ks1HutSArik6jf2eAjYFyIOf4ej3RN+184cZbzDvbPnuTJIUhPKKJE3cIsYTiAT3w==", + "version": "7.25.7", + "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.25.7.tgz", + "integrity": "sha512-0xZJFNE5XMpENsgfHYTw8FbX4kv53mFLn2i3XPoq69LyhYSCBJtitaHx9QnsVTrsogI4Z3+HtEfZ2/GFPOtf5g==", + "license": "MIT", "dependencies": { - "@babel/highlight": "^7.22.13", - "chalk": "^2.4.2" + "@babel/highlight": "^7.25.7", + "picocolors": "^1.0.0" }, "engines": { "node": ">=6.9.0" @@ -310,15 +317,16 @@ } }, "node_modules/@babel/generator": { - "version": "7.23.0", - "resolved": "https://registry.npmjs.org/@babel/generator/-/generator-7.23.0.tgz", - "integrity": "sha512-lN85QRR+5IbYrMWM6Y4pE/noaQtg4pNiqeNGX60eqOfo6gtEj6uw/JagelB8vVztSd7R6M5n1+PQkDbHbBRU4g==", + "version": "7.25.7", + "resolved": "https://registry.npmjs.org/@babel/generator/-/generator-7.25.7.tgz", + "integrity": "sha512-5Dqpl5fyV9pIAD62yK9P7fcA768uVPUyrQmqpqstHWgMma4feF1x/oFysBCVZLY5wJ2GkMUCdsNDnGZrPoR6rA==", "dev": true, + "license": "MIT", "dependencies": { - "@babel/types": "^7.23.0", - "@jridgewell/gen-mapping": "^0.3.2", - "@jridgewell/trace-mapping": "^0.3.17", - "jsesc": "^2.5.1" + "@babel/types": "^7.25.7", + "@jridgewell/gen-mapping": "^0.3.5", + "@jridgewell/trace-mapping": "^0.3.25", + "jsesc": "^3.0.2" }, "engines": { "node": ">=6.9.0" @@ -358,31 +366,6 @@ "node": ">=6.9.0" } }, - "node_modules/@babel/helper-function-name": { - "version": "7.23.0", - "resolved": "https://registry.npmjs.org/@babel/helper-function-name/-/helper-function-name-7.23.0.tgz", - "integrity": "sha512-OErEqsrxjZTJciZ4Oo+eoZqeW9UIiOcuYKRJA4ZAgV9myA+pOXhhmpfNCKjEH/auVfEYVFJ6y1Tc4r0eIApqiw==", - "dev": true, - "dependencies": { - "@babel/template": "^7.22.15", - "@babel/types": "^7.23.0" - }, - "engines": { - "node": ">=6.9.0" - } - }, - "node_modules/@babel/helper-hoist-variables": { - "version": "7.22.5", - "resolved": "https://registry.npmjs.org/@babel/helper-hoist-variables/-/helper-hoist-variables-7.22.5.tgz", - "integrity": "sha512-wGjk9QZVzvknA6yKIUURb8zY3grXCcOZt+/7Wcy8O2uctxhplmUPkOdlgoNhmdVee2c92JXbf1xpMtVNbfoxRw==", - "dev": true, - "dependencies": { - "@babel/types": "^7.22.5" - }, - "engines": { - "node": ">=6.9.0" - } - }, "node_modules/@babel/helper-module-imports": { "version": "7.22.15", "resolved": "https://registry.npmjs.org/@babel/helper-module-imports/-/helper-module-imports-7.22.15.tgz", @@ -414,10 +397,11 @@ } }, "node_modules/@babel/helper-plugin-utils": { - "version": "7.22.5", - "resolved": "https://registry.npmjs.org/@babel/helper-plugin-utils/-/helper-plugin-utils-7.22.5.tgz", - "integrity": "sha512-uLls06UVKgFG9QD4OeFYLEGteMIAa5kpTPcFL28yuCIIzsf6ZyKZMllKVOCZFhiZ5ptnwX4mtKdWCBE/uT4amg==", + "version": "7.25.7", + "resolved": "https://registry.npmjs.org/@babel/helper-plugin-utils/-/helper-plugin-utils-7.25.7.tgz", + "integrity": "sha512-eaPZai0PiqCi09pPs3pAFfl/zYgGaE6IdXtYvmf0qlcDTd3WCtO7JWCcRd64e0EQrcYgiHibEZnOGsSY4QSgaw==", "dev": true, + "license": "MIT", "engines": { "node": ">=6.9.0" } @@ -447,17 +431,19 @@ } }, "node_modules/@babel/helper-string-parser": { - "version": "7.22.5", - "resolved": "https://registry.npmjs.org/@babel/helper-string-parser/-/helper-string-parser-7.22.5.tgz", - "integrity": "sha512-mM4COjgZox8U+JcXQwPijIZLElkgEpO5rsERVDJTc2qfCDfERyob6k5WegS14SX18IIjv+XD+GrqNumY5JRCDw==", + "version": "7.25.7", + "resolved": "https://registry.npmjs.org/@babel/helper-string-parser/-/helper-string-parser-7.25.7.tgz", + "integrity": "sha512-CbkjYdsJNHFk8uqpEkpCvRs3YRp9tY6FmFY7wLMSYuGYkrdUi7r2lc4/wqsvlHoMznX3WJ9IP8giGPq68T/Y6g==", + "license": "MIT", "engines": { "node": ">=6.9.0" } }, "node_modules/@babel/helper-validator-identifier": { - "version": "7.22.20", - "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.22.20.tgz", - "integrity": "sha512-Y4OZ+ytlatR8AI+8KZfKuL5urKp7qey08ha31L8b3BwewJAoJamTzyvxPR/5D+KkdJCGPq/+8TukHBlY10FX9A==", + "version": "7.25.7", + "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.25.7.tgz", + "integrity": "sha512-AM6TzwYqGChO45oiuPqwL2t20/HdMC1rTPAesnBCgPCSF1x3oN9MVUwQV2iyz4xqWrctwK5RNC8LV22kaQCNYg==", + "license": "MIT", "engines": { "node": ">=6.9.0" } @@ -486,23 +472,29 @@ } }, "node_modules/@babel/highlight": { - "version": "7.22.20", - "resolved": "https://registry.npmjs.org/@babel/highlight/-/highlight-7.22.20.tgz", - "integrity": "sha512-dkdMCN3py0+ksCgYmGG8jKeGA/8Tk+gJwSYYlFGxG5lmhfKNoAy004YpLxpS1W2J8m/EK2Ew+yOs9pVRwO89mg==", + "version": "7.25.7", + "resolved": "https://registry.npmjs.org/@babel/highlight/-/highlight-7.25.7.tgz", + "integrity": "sha512-iYyACpW3iW8Fw+ZybQK+drQre+ns/tKpXbNESfrhNnPLIklLbXr7MYJ6gPEd0iETGLOK+SxMjVvKb/ffmk+FEw==", + "license": "MIT", "dependencies": { - "@babel/helper-validator-identifier": "^7.22.20", + "@babel/helper-validator-identifier": "^7.25.7", "chalk": "^2.4.2", - "js-tokens": "^4.0.0" + "js-tokens": "^4.0.0", + "picocolors": "^1.0.0" }, "engines": { "node": ">=6.9.0" } }, "node_modules/@babel/parser": { - "version": "7.23.0", - "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.23.0.tgz", - "integrity": "sha512-vvPKKdMemU85V9WE/l5wZEmImpCtLqbnTvqDS2U1fJ96KrxoW7KrXhNsNCblQlg8Ck4b85yxdTyelsMUgFUXiw==", + "version": "7.25.7", + "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.25.7.tgz", + "integrity": "sha512-aZn7ETtQsjjGG5HruveUK06cU3Hljuhd9Iojm4M8WWv3wLE6OkE5PWbDUkItmMgegmccaITudyuW5RPYrYlgWw==", "dev": true, + "license": "MIT", + "dependencies": { + "@babel/types": "^7.25.7" + }, "bin": { "parser": "bin/babel-parser.js" }, @@ -673,12 +665,13 @@ } }, "node_modules/@babel/plugin-syntax-typescript": { - "version": "7.22.5", - "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-typescript/-/plugin-syntax-typescript-7.22.5.tgz", - "integrity": "sha512-1mS2o03i7t1c6VzH6fdQ3OA8tcEIxwG18zIPRp+UY1Ihv6W+XZzBCVxExF9upussPXJ0xE9XRHwMoNs1ep/nRQ==", + "version": "7.25.7", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-typescript/-/plugin-syntax-typescript-7.25.7.tgz", + "integrity": "sha512-rR+5FDjpCHqqZN2bzZm18bVYGaejGq5ZkpVCJLXor/+zlSrSoc4KWcHI0URVWjl/68Dyr1uwZUz/1njycEAv9g==", "dev": true, + "license": "MIT", "dependencies": { - "@babel/helper-plugin-utils": "^7.22.5" + "@babel/helper-plugin-utils": "^7.25.7" }, "engines": { "node": ">=6.9.0" @@ -729,34 +722,33 @@ } }, "node_modules/@babel/template": { - "version": "7.22.15", - "resolved": "https://registry.npmjs.org/@babel/template/-/template-7.22.15.tgz", - "integrity": "sha512-QPErUVm4uyJa60rkI73qneDacvdvzxshT3kksGqlGWYdOTIUOwJ7RDUL8sGqslY1uXWSL6xMFKEXDS3ox2uF0w==", + "version": "7.25.7", + "resolved": "https://registry.npmjs.org/@babel/template/-/template-7.25.7.tgz", + "integrity": "sha512-wRwtAgI3bAS+JGU2upWNL9lSlDcRCqD05BZ1n3X2ONLH1WilFP6O1otQjeMK/1g0pvYcXC7b/qVUB1keofjtZA==", "dev": true, + "license": "MIT", "dependencies": { - "@babel/code-frame": "^7.22.13", - "@babel/parser": "^7.22.15", - "@babel/types": "^7.22.15" + "@babel/code-frame": "^7.25.7", + "@babel/parser": "^7.25.7", + "@babel/types": "^7.25.7" }, "engines": { "node": ">=6.9.0" } }, "node_modules/@babel/traverse": { - "version": "7.23.2", - "resolved": "https://registry.npmjs.org/@babel/traverse/-/traverse-7.23.2.tgz", - "integrity": "sha512-azpe59SQ48qG6nu2CzcMLbxUudtN+dOM9kDbUqGq3HXUJRlo7i8fvPoxQUzYgLZ4cMVmuZgm8vvBpNeRhd6XSw==", + "version": "7.25.7", + "resolved": "https://registry.npmjs.org/@babel/traverse/-/traverse-7.25.7.tgz", + "integrity": "sha512-jatJPT1Zjqvh/1FyJs6qAHL+Dzb7sTb+xr7Q+gM1b+1oBsMsQQ4FkVKb6dFlJvLlVssqkRzV05Jzervt9yhnzg==", "dev": true, + "license": "MIT", "dependencies": { - "@babel/code-frame": "^7.22.13", - "@babel/generator": "^7.23.0", - "@babel/helper-environment-visitor": "^7.22.20", - "@babel/helper-function-name": "^7.23.0", - "@babel/helper-hoist-variables": "^7.22.5", - "@babel/helper-split-export-declaration": "^7.22.6", - "@babel/parser": "^7.23.0", - "@babel/types": "^7.23.0", - "debug": "^4.1.0", + "@babel/code-frame": "^7.25.7", + "@babel/generator": "^7.25.7", + "@babel/parser": "^7.25.7", + "@babel/template": "^7.25.7", + "@babel/types": "^7.25.7", + "debug": "^4.3.1", "globals": "^11.1.0" }, "engines": { @@ -764,12 +756,13 @@ } }, "node_modules/@babel/types": { - "version": "7.23.0", - "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.23.0.tgz", - "integrity": "sha512-0oIyUfKoI3mSqMvsxBdclDwxXKXAUA8v/apZbc+iSyARYou1o8ZGDxbUYyLFoW2arqS2jDGqJuZvv1d/io1axg==", + "version": "7.25.7", + "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.25.7.tgz", + "integrity": "sha512-vwIVdXG+j+FOpkwqHRcBgHLYNL7XMkufrlaFvL9o6Ai9sJn9+PdyIL5qa0XzTZw084c+u9LOls53eoZWP/W5WQ==", + "license": "MIT", "dependencies": { - "@babel/helper-string-parser": "^7.22.5", - "@babel/helper-validator-identifier": "^7.22.20", + "@babel/helper-string-parser": "^7.25.7", + "@babel/helper-validator-identifier": "^7.25.7", "to-fast-properties": "^2.0.0" }, "engines": { @@ -2239,13 +2232,14 @@ } }, "node_modules/@jridgewell/gen-mapping": { - "version": "0.3.3", - "resolved": "https://registry.npmjs.org/@jridgewell/gen-mapping/-/gen-mapping-0.3.3.tgz", - "integrity": "sha512-HLhSWOLRi875zjjMG/r+Nv0oCW8umGb0BgEhyX3dDX3egwZtB8PqLnjz3yedt8R5StBrzcg4aBpnh8UA9D1BoQ==", + "version": "0.3.5", + "resolved": "https://registry.npmjs.org/@jridgewell/gen-mapping/-/gen-mapping-0.3.5.tgz", + "integrity": "sha512-IzL8ZoEDIBRWEzlCcRhOaCupYyN5gdIK+Q6fbFdPDg6HqX6jpkItn7DFIpW9LQzXG6Df9sA7+OKnq0qlz/GaQg==", + "license": "MIT", "dependencies": { - "@jridgewell/set-array": "^1.0.1", + "@jridgewell/set-array": "^1.2.1", "@jridgewell/sourcemap-codec": "^1.4.10", - "@jridgewell/trace-mapping": "^0.3.9" + "@jridgewell/trace-mapping": "^0.3.24" }, "engines": { "node": ">=6.0.0" @@ -2260,9 +2254,10 @@ } }, "node_modules/@jridgewell/set-array": { - "version": "1.1.2", - "resolved": "https://registry.npmjs.org/@jridgewell/set-array/-/set-array-1.1.2.tgz", - "integrity": "sha512-xnkseuNADM0gt2bs+BvhO0p78Mk762YnZdsuzFV018NoG1Sj1SCQvpSqa7XUaTam5vAGasABV9qXASMKnFMwMw==", + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/@jridgewell/set-array/-/set-array-1.2.1.tgz", + "integrity": "sha512-R8gLRTZeyp03ymzP/6Lil/28tGeGEzhx1q2k703KGWRAI1VdvPIXdG70VJc2pAMw3NA6JKL5hhFu1sJX0Mnn/A==", + "license": "MIT", "engines": { "node": ">=6.0.0" } @@ -2273,9 +2268,10 @@ "integrity": "sha512-eF2rxCRulEKXHTRiDrDy6erMYWqNw4LPdQ8UQA4huuxaQsVeRPFl2oM8oDGxMFhJUWZf9McpLtJasDDZb/Bpeg==" }, "node_modules/@jridgewell/trace-mapping": { - "version": "0.3.20", - "resolved": "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.20.tgz", - "integrity": "sha512-R8LcPeWZol2zR8mmH3JeKQ6QRCFb7XgUhV9ZlGhHLGyg4wpPiPZNQOOWhFZhxKw8u//yTbNGI42Bx/3paXEQ+Q==", + "version": "0.3.25", + "resolved": "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.25.tgz", + "integrity": "sha512-vNk6aEwybGtawWmy/PzwnGDOjCkLWSD2wqvjGGAgOAwCGWySYXfYoxt00IJkTF+8Lb57DwOb3Aa0o9CApepiYQ==", + "license": "MIT", "dependencies": { "@jridgewell/resolve-uri": "^3.1.0", "@jridgewell/sourcemap-codec": "^1.4.14" @@ -2361,6 +2357,39 @@ "url": "https://opencollective.com/popperjs" } }, + "node_modules/@radix-ui/react-compose-refs": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@radix-ui/react-compose-refs/-/react-compose-refs-1.1.0.tgz", + "integrity": "sha512-b4inOtiaOnYf9KWyO3jAeeCG6FeyfY6ldiEPanbUjWd+xIk5wZeHa8yVwmrJ2vderhu/BQvzCrJI0lHd+wIiqw==", + "license": "MIT", + "peerDependencies": { + "@types/react": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-slot": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@radix-ui/react-slot/-/react-slot-1.1.0.tgz", + "integrity": "sha512-FUCf5XMfmW4dtYl69pdS4DbxKy8nj4M7SafBgPllysxmdachynNflAdp/gCsnYWNDnge6tI9onzMp5ARYc1KNw==", + "license": "MIT", + "dependencies": { + "@radix-ui/react-compose-refs": "1.1.0" + }, + "peerDependencies": { + "@types/react": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, "node_modules/@rc-component/color-picker": { "version": "1.4.1", "resolved": "https://registry.npmjs.org/@rc-component/color-picker/-/color-picker-1.4.1.tgz", @@ -3395,6 +3424,7 @@ "version": "3.2.1", "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-3.2.1.tgz", "integrity": "sha512-VT0ZI6kZRdTh8YyJw3SMbYm/u+NqfsAxEpWO0Pf9sq8/e94WxxOpPKx9FR1FlyCtOVDNOQ+8ntlqFxiRc+r5qA==", + "license": "MIT", "dependencies": { "color-convert": "^1.9.0" }, @@ -4130,6 +4160,7 @@ "version": "2.4.2", "resolved": "https://registry.npmjs.org/chalk/-/chalk-2.4.2.tgz", "integrity": "sha512-Mti+f9lpJNcwF4tWV8/OrTTtF1gZi+f8FqlyAdouralcFWFQWF2+NgCHShjkCb+IFBLq9buZwE1xckQU4peSuQ==", + "license": "MIT", "dependencies": { "ansi-styles": "^3.2.1", "escape-string-regexp": "^1.0.5", @@ -4143,6 +4174,7 @@ "version": "1.0.5", "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-1.0.5.tgz", "integrity": "sha512-vbRorB5FUQWvla16U8R/qgaFIya2qGzwDrNmCZuYKrbdSUMG6I1ZCGQRefkRVhuOkIGVne7BQ35DSfo1qvJqFg==", + "license": "MIT", "engines": { "node": ">=0.8.0" } @@ -4250,6 +4282,27 @@ "integrity": "sha512-0TNiGstbQmCFwt4akjjBg5pLRTSyj/PkWQ1ZoO2zntmg9yLqSRxwEa4iCfQLGjqhiqBfOJa7W/E8wfGrTDmlZQ==", "dev": true }, + "node_modules/class-variance-authority": { + "version": "0.7.0", + "resolved": "https://registry.npmjs.org/class-variance-authority/-/class-variance-authority-0.7.0.tgz", + "integrity": "sha512-jFI8IQw4hczaL4ALINxqLEXQbWcNjoSkloa4IaufXCJr6QawJyw7tuRysRsrE8w2p/4gGaxKIt/hX3qz/IbD1A==", + "license": "Apache-2.0", + "dependencies": { + "clsx": "2.0.0" + }, + "funding": { + "url": "https://joebell.co.uk" + } + }, + "node_modules/class-variance-authority/node_modules/clsx": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/clsx/-/clsx-2.0.0.tgz", + "integrity": "sha512-rQ1+kcj+ttHG0MKVGBUXwayCCF1oh39BF5COIpRzuCEv8Mwjv0XucrI2ExNTOn9IlLifGClWQcU9BrZORvtw6Q==", + "license": "MIT", + "engines": { + "node": ">=6" + } + }, "node_modules/classnames": { "version": "2.3.2", "resolved": "https://registry.npmjs.org/classnames/-/classnames-2.3.2.tgz", @@ -5676,9 +5729,10 @@ } }, "node_modules/fast-glob": { - "version": "3.3.1", - "resolved": "https://registry.npmjs.org/fast-glob/-/fast-glob-3.3.1.tgz", - "integrity": "sha512-kNFPyjhh5cKjrUltxs+wFx+ZkbRaxxmZ+X0ZU31SOsxCEtP9VPgtq2teZw1DebupL5GmDaNQ6yKMMVcM41iqDg==", + "version": "3.3.2", + "resolved": "https://registry.npmjs.org/fast-glob/-/fast-glob-3.3.2.tgz", + "integrity": "sha512-oX2ruAFQwf/Orj8m737Y5adxDQO0LAB7/S5MnxCdTNDd4p6BsyIVsv9JQsATbTSq8KHRpLwIHbVlUNatxd+1Ow==", + "license": "MIT", "dependencies": { "@nodelib/fs.stat": "^2.0.2", "@nodelib/fs.walk": "^1.2.3", @@ -6150,6 +6204,7 @@ "version": "3.0.0", "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-3.0.0.tgz", "integrity": "sha512-sKJf1+ceQBr4SMkvQnBDNDtf4TXpVhVGateu0t918bl30FnbE2m4vNLX+VWe/dpjlb+HugGYzW7uQXH98HPEYw==", + "license": "MIT", "engines": { "node": ">=4" } @@ -8719,15 +8774,16 @@ } }, "node_modules/jsesc": { - "version": "2.5.2", - "resolved": "https://registry.npmjs.org/jsesc/-/jsesc-2.5.2.tgz", - "integrity": "sha512-OYu7XEzjkCQ3C5Ps3QIZsQfNpqoJyZZA99wd9aWd05NCtC5pWOkShK2mkL6HXQR6/Cy2lbNdPlZBpuQHXE63gA==", + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/jsesc/-/jsesc-3.0.2.tgz", + "integrity": "sha512-xKqzzWXDttJuOcawBt4KnKHHIf5oQ/Cxax+0PWFG+DFDgHNAdi+TXECADI+RYiFUMmx8792xsMbbgXj4CwnP4g==", "dev": true, + "license": "MIT", "bin": { "jsesc": "bin/jsesc" }, "engines": { - "node": ">=4" + "node": ">=6" } }, "node_modules/json-buffer": { @@ -8934,6 +8990,15 @@ "yallist": "^3.0.2" } }, + "node_modules/lucide-react": { + "version": "0.447.0", + "resolved": "https://registry.npmjs.org/lucide-react/-/lucide-react-0.447.0.tgz", + "integrity": "sha512-SZ//hQmvi+kDKrNepArVkYK7/jfeZ5uFNEnYmd45RKZcbGD78KLnrcNXmgeg6m+xNHFvTG+CblszXCy4n6DN4w==", + "license": "ISC", + "peerDependencies": { + "react": "^16.5.1 || ^17.0.0 || ^18.0.0 || ^19.0.0-rc" + } + }, "node_modules/magic-string": { "version": "0.27.0", "resolved": "https://registry.npmjs.org/magic-string/-/magic-string-0.27.0.tgz", @@ -12672,6 +12737,7 @@ "version": "5.5.0", "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-5.5.0.tgz", "integrity": "sha512-QjVjwdXIt408MIiAqCX4oUKsgU2EqAGzs2Ppkm4aQYbjm+ZEWEcW4SfFNTr4uMNZma0ey4f5lgLrkB0aX0QMow==", + "license": "MIT", "dependencies": { "has-flag": "^3.0.0" }, @@ -12690,6 +12756,16 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/tailwind-merge": { + "version": "2.5.2", + "resolved": "https://registry.npmjs.org/tailwind-merge/-/tailwind-merge-2.5.2.tgz", + "integrity": "sha512-kjEBm+pvD+6eAwzJL2Bi+02/9LFLal1Gs61+QB7HvTfQQ0aXwC5LGT8PEt1gS0CWKktKe6ysPTAy3cBC5MeiIg==", + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/dcastil" + } + }, "node_modules/tailwind-scrollbar": { "version": "3.0.5", "resolved": "https://registry.npmjs.org/tailwind-scrollbar/-/tailwind-scrollbar-3.0.5.tgz", @@ -12737,6 +12813,15 @@ "node": ">=14.0.0" } }, + "node_modules/tailwindcss-animate": { + "version": "1.0.7", + "resolved": "https://registry.npmjs.org/tailwindcss-animate/-/tailwindcss-animate-1.0.7.tgz", + "integrity": "sha512-bl6mpH3T7I3UFxuvDEXLxy/VuFxBk5bbzplh7tXI68mwMokNYd1t9qPBHlnyTwfa4JGC4zP516I1hYYtQ/vspA==", + "license": "MIT", + "peerDependencies": { + "tailwindcss": ">=3.0.0 || insiders" + } + }, "node_modules/test-exclude": { "version": "6.0.0", "resolved": "https://registry.npmjs.org/test-exclude/-/test-exclude-6.0.0.tgz", @@ -12785,9 +12870,10 @@ } }, "node_modules/tiny-invariant": { - "version": "1.3.1", - "resolved": "https://registry.npmjs.org/tiny-invariant/-/tiny-invariant-1.3.1.tgz", - "integrity": "sha512-AD5ih2NlSssTCwsMznbvwMZpJ1cbhkGd2uueNxzv2jDlEeZdU04JQfRnggJQ8DrcVBGjAsCKwFBbDlVNtEMlzw==" + "version": "1.3.3", + "resolved": "https://registry.npmjs.org/tiny-invariant/-/tiny-invariant-1.3.3.tgz", + "integrity": "sha512-+FbBPE1o9QAYvviau/qC5SE3caw21q3xkvWKBtja5vgqOWIHHJ3ioaq1VPfn/Szqctz2bU/oYeKd9/z5BL+PVg==", + "license": "MIT" }, "node_modules/tiny-warning": { "version": "1.0.3", diff --git a/package.json b/package.json index 1574774bfb..430cd85589 100644 --- a/package.json +++ b/package.json @@ -9,6 +9,7 @@ "@headlessui/tailwindcss": "^0.1.3", "@hello-pangea/dnd": "^16.2.0", "@monaco-editor/react": "^4.4.6", + "@radix-ui/react-slot": "^1.1.0", "@react-oauth/google": "^0.9.0", "@reduxjs/toolkit": "^1.9.1", "@sentry/react": "^7.77.0", @@ -18,7 +19,9 @@ "antd": "^5.6.1", "array-move": "^4.0.0", "axios": "^0.27.2", + "class-variance-authority": "^0.7.0", "classnames": "^2.3.2", + "clsx": "^2.1.1", "collect.js": "^4.34.3", "currency.js": "^2.0.4", "dayjs": "^1.11.7", @@ -32,6 +35,7 @@ "jotai": "^2.0.3", "js-sha256": "^0.11.0", "lodash": "^4.17.21", + "lucide-react": "^0.447.0", "mitt": "^3.0.1", "playwright": "^1.44.0", "pretty-bytes": "^6.0.0", @@ -69,7 +73,9 @@ "remove": "^0.1.5", "socket.io-client": "^4.7.5", "styled-components": "^6.0.7", + "tailwind-merge": "^2.5.2", "tailwind-scrollbar": "^3.0.0", + "tailwindcss-animate": "^1.0.7", "uuid": "^9.0.0" }, "scripts": { diff --git a/src/components/cards/Card.tsx b/src/components/cards/Card.tsx index cb6c3fecae..70b2cc695f 100644 --- a/src/components/cards/Card.tsx +++ b/src/components/cards/Card.tsx @@ -26,6 +26,8 @@ import { DropdownElement } from '$app/components/dropdown/DropdownElement'; import { ChevronDown, ChevronUp } from 'react-feather'; import { useColorScheme } from '$app/common/colors'; +import { Button as ButtonBase } from '../../../components/ui/button'; + export interface ButtonOption { text: string; onClick: (event: FormEvent) => unknown; @@ -56,17 +58,22 @@ interface Props { withoutHeaderBorder?: boolean; topRight?: ReactNode; height?: 'full'; + renderFromShadcn?: boolean; } export function Card(props: Props) { const [t] = useTranslation(); - const { padding = 'regular', height } = props; + const { padding = 'regular', height, renderFromShadcn } = props; const [isCollapsed, setIsCollpased] = useState(props.collapsed); const colors = useColorScheme(); + if (renderFromShadcn) { + return asdkas dk; + } + return (
+ {t('remove')} + + ) + } + renderFromShadcn >
diff --git a/src/resources/css/app.css b/src/resources/css/app.css index b5c61c9567..80ea3b7873 100644 --- a/src/resources/css/app.css +++ b/src/resources/css/app.css @@ -1,3 +1,66 @@ @tailwind base; @tailwind components; @tailwind utilities; +@layer base { + :root { + --background: 0 0% 100%; + --foreground: 224 71.4% 4.1%; + --card: 0 0% 100%; + --card-foreground: 224 71.4% 4.1%; + --popover: 0 0% 100%; + --popover-foreground: 224 71.4% 4.1%; + --primary: 220.9 39.3% 11%; + --primary-foreground: 210 20% 98%; + --secondary: 220 14.3% 95.9%; + --secondary-foreground: 220.9 39.3% 11%; + --muted: 220 14.3% 95.9%; + --muted-foreground: 220 8.9% 46.1%; + --accent: 220 14.3% 95.9%; + --accent-foreground: 220.9 39.3% 11%; + --destructive: 0 84.2% 60.2%; + --destructive-foreground: 210 20% 98%; + --border: 220 13% 91%; + --input: 220 13% 91%; + --ring: 224 71.4% 4.1%; + --chart-1: 12 76% 61%; + --chart-2: 173 58% 39%; + --chart-3: 197 37% 24%; + --chart-4: 43 74% 66%; + --chart-5: 27 87% 67%; + --radius: 0.5rem + } + .dark { + --background: 224 71.4% 4.1%; + --foreground: 210 20% 98%; + --card: 224 71.4% 4.1%; + --card-foreground: 210 20% 98%; + --popover: 224 71.4% 4.1%; + --popover-foreground: 210 20% 98%; + --primary: 210 20% 98%; + --primary-foreground: 220.9 39.3% 11%; + --secondary: 215 27.9% 16.9%; + --secondary-foreground: 210 20% 98%; + --muted: 215 27.9% 16.9%; + --muted-foreground: 217.9 10.6% 64.9%; + --accent: 215 27.9% 16.9%; + --accent-foreground: 210 20% 98%; + --destructive: 0 62.8% 30.6%; + --destructive-foreground: 210 20% 98%; + --border: 215 27.9% 16.9%; + --input: 215 27.9% 16.9%; + --ring: 216 12.2% 83.9%; + --chart-1: 220 70% 50%; + --chart-2: 160 60% 45%; + --chart-3: 30 80% 55%; + --chart-4: 280 65% 60%; + --chart-5: 340 75% 55% + } +} +@layer base { + * { + @apply border-border; + } + body { + @apply bg-background text-foreground; + } +} diff --git a/tailwind.config.js b/tailwind.config.js index b03625e5d4..1fda281441 100644 --- a/tailwind.config.js +++ b/tailwind.config.js @@ -2,20 +2,65 @@ const defaultTheme = require('tailwindcss/defaultTheme'); module.exports = { content: ['./index.html', './src/**/*.{js,ts,jsx,tsx}'], - darkMode: 'class', // or 'media' or 'class' + darkMode: ['class', 'class'], // or 'media' or 'class' theme: { - extend: { - fontFamily: { - sans: ['Inter var', ...defaultTheme.fontFamily.sans], - }, - colors: { - ninja: { - gray: '#242930', - 'gray-darker': '#2F2E2E', - 'gray-lighter': '#363D47', - }, - }, - }, + extend: { + fontFamily: { + sans: ['Inter var', ...defaultTheme.fontFamily.sans] + }, + colors: { + ninja: { + gray: '#242930', + 'gray-darker': '#2F2E2E', + 'gray-lighter': '#363D47' + }, + background: 'hsl(var(--background))', + foreground: 'hsl(var(--foreground))', + card: { + DEFAULT: 'hsl(var(--card))', + foreground: 'hsl(var(--card-foreground))' + }, + popover: { + DEFAULT: 'hsl(var(--popover))', + foreground: 'hsl(var(--popover-foreground))' + }, + primary: { + DEFAULT: 'hsl(var(--primary))', + foreground: 'hsl(var(--primary-foreground))' + }, + secondary: { + DEFAULT: 'hsl(var(--secondary))', + foreground: 'hsl(var(--secondary-foreground))' + }, + muted: { + DEFAULT: 'hsl(var(--muted))', + foreground: 'hsl(var(--muted-foreground))' + }, + accent: { + DEFAULT: 'hsl(var(--accent))', + foreground: 'hsl(var(--accent-foreground))' + }, + destructive: { + DEFAULT: 'hsl(var(--destructive))', + foreground: 'hsl(var(--destructive-foreground))' + }, + border: 'hsl(var(--border))', + input: 'hsl(var(--input))', + ring: 'hsl(var(--ring))', + chart: { + '1': 'hsl(var(--chart-1))', + '2': 'hsl(var(--chart-2))', + '3': 'hsl(var(--chart-3))', + '4': 'hsl(var(--chart-4))', + '5': 'hsl(var(--chart-5))' + } + }, + borderRadius: { + lg: 'var(--radius)', + md: 'calc(var(--radius) - 2px)', + sm: 'calc(var(--radius) - 4px)' + } + } }, variants: { extend: {}, @@ -24,5 +69,6 @@ module.exports = { require('@tailwindcss/forms'), require('tailwind-scrollbar'), require('@tailwindcss/typography'), - ], + require("tailwindcss-animate") +], }; From efa842b2f3db9526e31989ddf0417b1840ef5658 Mon Sep 17 00:00:00 2001 From: Civolilah Date: Tue, 17 Dec 2024 19:18:16 +0100 Subject: [PATCH 15/36] Clean up --- src/pages/tasks/common/components/TaskSlider.tsx | 2 -- src/pages/tasks/common/hooks/useInvoiceTask.ts | 4 ---- 2 files changed, 6 deletions(-) diff --git a/src/pages/tasks/common/components/TaskSlider.tsx b/src/pages/tasks/common/components/TaskSlider.tsx index 208f78bf76..794db40f33 100644 --- a/src/pages/tasks/common/components/TaskSlider.tsx +++ b/src/pages/tasks/common/components/TaskSlider.tsx @@ -46,7 +46,6 @@ import { calculateTaskHours } from '$app/pages/projects/common/hooks/useInvoiceP import { date as formatDate } from '$app/common/helpers'; import { useFormatTimeLog } from '../../kanban/common/hooks'; import { TaskClock } from '../../kanban/components/TaskClock'; -import { useUserNumberPrecision } from '$app/common/hooks/useUserNumberPrecision'; import { useCompanyTimeFormat } from '$app/common/hooks/useCompanyTimeFormat'; import { useUserNumberPrecision } from '$app/common/hooks/useUserNumberPrecision'; @@ -106,7 +105,6 @@ export function TaskSlider() { }); const { timeFormat } = useCompanyTimeFormat(); - const userNumberPrecision = useUserNumberPrecision(); const { dateFormat } = useCurrentCompanyDateFormats(); const userNumberPrecision = useUserNumberPrecision(); diff --git a/src/pages/tasks/common/hooks/useInvoiceTask.ts b/src/pages/tasks/common/hooks/useInvoiceTask.ts index 73558e14c8..c0c5da8c77 100644 --- a/src/pages/tasks/common/hooks/useInvoiceTask.ts +++ b/src/pages/tasks/common/hooks/useInvoiceTask.ts @@ -32,10 +32,6 @@ import { useUserNumberPrecision } from '$app/common/hooks/useUserNumberPrecision import { useNumericFormatter } from '$app/common/hooks/useNumericFormatter'; import { useGetCurrencySeparators } from '$app/common/hooks/useGetCurrencySeparators'; import { useResolveDateAndTimeClientFormat } from '$app/pages/clients/common/hooks/useResolveDateAndTimeClientFormat'; -import { useUserNumberPrecision } from '$app/common/hooks/useUserNumberPrecision'; -import { useNumericFormatter } from '$app/common/hooks/useNumericFormatter'; -import { useGetCurrencySeparators } from '$app/common/hooks/useGetCurrencySeparators'; -import { useResolveDateAndTimeClientFormat } from '$app/pages/clients/common/hooks/useResolveDateAndTimeClientFormat'; interface Params { onlyAddToInvoice?: boolean; From e6cd7401ba2571b22faba74abc51b6d092529a61 Mon Sep 17 00:00:00 2001 From: Civolilah Date: Wed, 18 Dec 2024 16:13:59 +0100 Subject: [PATCH 16/36] Converting dashboard cards into new styling --- components/ui/card.tsx | 79 +++++++++++++++++++ src/components/DataTable.tsx | 2 + src/components/cards/Card.tsx | 33 +++++++- src/components/tables/Table.tsx | 7 +- src/pages/dashboard/components/Activity.tsx | 1 + .../dashboard/components/RecentPayments.tsx | 1 + .../components/ResizableDashboardCards.tsx | 1 + 7 files changed, 120 insertions(+), 4 deletions(-) create mode 100644 components/ui/card.tsx diff --git a/components/ui/card.tsx b/components/ui/card.tsx new file mode 100644 index 0000000000..08e7a1510d --- /dev/null +++ b/components/ui/card.tsx @@ -0,0 +1,79 @@ +import * as React from "react" + +import { cn } from "~/lib/utils" + +const Card = React.forwardRef< + HTMLDivElement, + React.HTMLAttributes +>(({ className, ...props }, ref) => ( +
+)) +Card.displayName = "Card" + +const CardHeader = React.forwardRef< + HTMLDivElement, + React.HTMLAttributes +>(({ className, ...props }, ref) => ( +
+)) +CardHeader.displayName = "CardHeader" + +const CardTitle = React.forwardRef< + HTMLDivElement, + React.HTMLAttributes +>(({ className, ...props }, ref) => ( +
+)) +CardTitle.displayName = "CardTitle" + +const CardDescription = React.forwardRef< + HTMLDivElement, + React.HTMLAttributes +>(({ className, ...props }, ref) => ( +
+)) +CardDescription.displayName = "CardDescription" + +const CardContent = React.forwardRef< + HTMLDivElement, + React.HTMLAttributes +>(({ className, ...props }, ref) => ( +
+)) +CardContent.displayName = "CardContent" + +const CardFooter = React.forwardRef< + HTMLDivElement, + React.HTMLAttributes +>(({ className, ...props }, ref) => ( +
+)) +CardFooter.displayName = "CardFooter" + +export { Card, CardHeader, CardFooter, CardTitle, CardDescription, CardContent } diff --git a/src/components/DataTable.tsx b/src/components/DataTable.tsx index 8e7a1722cc..01fb60b2d7 100644 --- a/src/components/DataTable.tsx +++ b/src/components/DataTable.tsx @@ -152,6 +152,7 @@ interface Props extends CommonProps { withoutPerPageAsPreference?: boolean; withoutSortQueryParameter?: boolean; showRestoreBulk?: (selectedResources: T[]) => boolean; + withTransparentBackground?: boolean; } export type ResourceAction = (resource: T) => ReactElement; @@ -589,6 +590,7 @@ export function DataTable(props: Props) { } isDataLoading={isLoading} style={props.style} + withTransparentBackground={props.withTransparentBackground} > {!props.withoutActions && !hideEditableOptions && ( diff --git a/src/components/cards/Card.tsx b/src/components/cards/Card.tsx index 70b2cc695f..eda0ad4e8d 100644 --- a/src/components/cards/Card.tsx +++ b/src/components/cards/Card.tsx @@ -26,7 +26,12 @@ import { DropdownElement } from '$app/components/dropdown/DropdownElement'; import { ChevronDown, ChevronUp } from 'react-feather'; import { useColorScheme } from '$app/common/colors'; -import { Button as ButtonBase } from '../../../components/ui/button'; +import { + CardDescription, + CardHeader, + CardTitle, + Card as ShadcnCard, +} from '../../../components/ui/card'; export interface ButtonOption { text: string; @@ -71,7 +76,31 @@ export function Card(props: Props) { const colors = useColorScheme(); if (renderFromShadcn) { - return asdkas dk; + console.log(props.title); + return ( + + {Boolean(props.title || props.description) && ( + + {props.title && {props.title}} + + {props.description && ( + {props.description} + )} + + )} + + {props.children} + + ); } return ( diff --git a/src/components/tables/Table.tsx b/src/components/tables/Table.tsx index 7426b9aa8e..0aa38530cf 100644 --- a/src/components/tables/Table.tsx +++ b/src/components/tables/Table.tsx @@ -21,10 +21,11 @@ interface Props extends CommonProps { withoutRightBorder?: boolean; onVerticalOverflowChange?: (overflow: boolean) => void; isDataLoading?: boolean; + withTransparentBackground?: boolean; } export function Table(props: Props) { - const { onVerticalOverflowChange } = props; + const { onVerticalOverflowChange, withTransparentBackground } = props; const [tableParentHeight, setTableParentHeight] = useState(); const [tableHeight, setTableHeight] = useState(); @@ -99,7 +100,9 @@ export function Table(props: Props) { } )} style={{ - backgroundColor: colors.$1, + backgroundColor: withTransparentBackground + ? 'transparent' + : colors.$1, color: colors.$3, borderColor: colors.$4, }} diff --git a/src/pages/dashboard/components/Activity.tsx b/src/pages/dashboard/components/Activity.tsx index 8a0f494eb3..2a0a969484 100644 --- a/src/pages/dashboard/components/Activity.tsx +++ b/src/pages/dashboard/components/Activity.tsx @@ -37,6 +37,7 @@ export function Activity() { height="full" withoutBodyPadding withScrollableBody + renderFromShadcn > {isLoading && ( diff --git a/src/pages/dashboard/components/RecentPayments.tsx b/src/pages/dashboard/components/RecentPayments.tsx index bbfe736a47..71f4e006ed 100644 --- a/src/pages/dashboard/components/RecentPayments.tsx +++ b/src/pages/dashboard/components/RecentPayments.tsx @@ -96,6 +96,7 @@ export function RecentPayments() { title={t('recent_payments')} className="h-full relative" withoutBodyPadding + renderFromShadcn >
Date: Thu, 19 Dec 2024 19:36:58 +0100 Subject: [PATCH 17/36] Improving logic on the dashboard --- components/ui/card.tsx | 50 +++--- src/components/cards/Card.tsx | 1 - .../dashboard/components/DashboardCard.tsx | 8 +- .../dashboard/components/ExpiredQuotes.tsx | 1 + .../dashboard/components/PastDueInvoices.tsx | 1 + .../components/ResizableDashboardCards.tsx | 146 ++++++++++++------ .../dashboard/components/UpcomingInvoices.tsx | 1 + .../dashboard/components/UpcomingQuotes.tsx | 1 + .../components/UpcomingRecurringInvoices.tsx | 1 + 9 files changed, 133 insertions(+), 77 deletions(-) diff --git a/components/ui/card.tsx b/components/ui/card.tsx index 08e7a1510d..6363cd7ca4 100644 --- a/components/ui/card.tsx +++ b/components/ui/card.tsx @@ -1,6 +1,6 @@ -import * as React from "react" +import * as React from 'react'; -import { cn } from "~/lib/utils" +import { cn } from '~/lib/utils'; const Card = React.forwardRef< HTMLDivElement, @@ -9,13 +9,14 @@ const Card = React.forwardRef<
-)) -Card.displayName = "Card" +)); +Card.displayName = 'Card'; const CardHeader = React.forwardRef< HTMLDivElement, @@ -23,11 +24,11 @@ const CardHeader = React.forwardRef< >(({ className, ...props }, ref) => (
-)) -CardHeader.displayName = "CardHeader" +)); +CardHeader.displayName = 'CardHeader'; const CardTitle = React.forwardRef< HTMLDivElement, @@ -36,13 +37,13 @@ const CardTitle = React.forwardRef<
-)) -CardTitle.displayName = "CardTitle" +)); +CardTitle.displayName = 'CardTitle'; const CardDescription = React.forwardRef< HTMLDivElement, @@ -50,19 +51,19 @@ const CardDescription = React.forwardRef< >(({ className, ...props }, ref) => (
-)) -CardDescription.displayName = "CardDescription" +)); +CardDescription.displayName = 'CardDescription'; const CardContent = React.forwardRef< HTMLDivElement, React.HTMLAttributes >(({ className, ...props }, ref) => ( -
-)) -CardContent.displayName = "CardContent" +
+)); +CardContent.displayName = 'CardContent'; const CardFooter = React.forwardRef< HTMLDivElement, @@ -70,10 +71,17 @@ const CardFooter = React.forwardRef< >(({ className, ...props }, ref) => (
-)) -CardFooter.displayName = "CardFooter" +)); +CardFooter.displayName = 'CardFooter'; -export { Card, CardHeader, CardFooter, CardTitle, CardDescription, CardContent } +export { + Card, + CardHeader, + CardFooter, + CardTitle, + CardDescription, + CardContent, +}; diff --git a/src/components/cards/Card.tsx b/src/components/cards/Card.tsx index eda0ad4e8d..0929797043 100644 --- a/src/components/cards/Card.tsx +++ b/src/components/cards/Card.tsx @@ -76,7 +76,6 @@ export function Card(props: Props) { const colors = useColorScheme(); if (renderFromShadcn) { - console.log(props.title); return ( + {isFormBusy && (
@@ -98,7 +98,7 @@ export function DashboardCard(props: CardProps) { : responseData} - + {t( PERIOD_LABELS[field.period as keyof typeof PERIOD_LABELS] ?? field.period @@ -106,6 +106,6 @@ export function DashboardCard(props: CardProps) {
)} - +
); } diff --git a/src/pages/dashboard/components/ExpiredQuotes.tsx b/src/pages/dashboard/components/ExpiredQuotes.tsx index f38d21bc58..6fd202eeee 100644 --- a/src/pages/dashboard/components/ExpiredQuotes.tsx +++ b/src/pages/dashboard/components/ExpiredQuotes.tsx @@ -77,6 +77,7 @@ export function ExpiredQuotes() { withoutBodyPadding withoutHeaderBorder height="full" + renderFromShadcn >
{ - const currentHeight = - document.getElementById('cardsContainer')?.clientHeight; + const currentWidth = document.getElementById('cardsContainer')?.clientWidth; + + if (!currentWidth) return; + + const cardWidth = 240; + const totalCards = + user?.company_user?.settings.dashboard_fields?.length || 0; + const cardsPerRow = Math.floor( + (currentWidth - (totalCards - 1) * 12) / cardWidth + ); + const numberOfRows = Math.ceil( + totalCards / (totalCards ? cardsPerRow || 1 : 0) + ); + + console.log(totalCards, cardsPerRow, numberOfRows); setLayouts((currentLayouts) => { const updatedLayouts = cloneDeep(currentLayouts); - const cardsNumbers = - user?.company_user?.settings.dashboard_fields?.length; - Object.keys(updatedLayouts).forEach((breakpoint) => { updatedLayouts[breakpoint] = updatedLayouts[breakpoint].map((item) => item.i === '1' ? { ...item, - h: - currentHeight && cardsNumbers - ? (currentHeight + 80) / 30 - : cardsNumbers - ? 6.3 - : 0, + h: totalCards ? numberOfRows * 7 : 0, } : item ); @@ -819,6 +824,8 @@ export function ResizableDashboardCards() { }, [chart.data]); useEffect(() => { + console.log(layoutBreakpoint); + if (layoutBreakpoint) { if (settings?.dashboard_cards_configuration && !isLayoutsInitialized) { //setLayouts(cloneDeep(settings?.dashboard_cards_configuration)); @@ -832,6 +839,18 @@ export function ResizableDashboardCards() { if (isLayoutsInitialized) { updateLayoutHeight(); } + + const handleResize = () => { + if (isLayoutsInitialized) { + updateLayoutHeight(); + } + }; + + window.addEventListener('resize', handleResize); + + return () => { + window.removeEventListener('resize', handleResize); + }; }, [ user?.company_user?.settings.dashboard_fields?.length, isLayoutsInitialized, @@ -1033,28 +1052,26 @@ export function ResizableDashboardCards() {
- {Boolean(user?.company_user?.settings.dashboard_fields?.length) && ( -
- {user?.company_user?.settings.dashboard_fields?.map( - (field, index) => ( - - ) - )} -
- )} +
+ {user?.company_user?.settings.dashboard_fields?.map( + (field, index) => ( + + ) + )} +
{company && (
-
- {`${user?.first_name} ${user?.last_name}`} +
+ {`${user?.first_name} ${user?.last_name}`} - {t('recent_transactions')} + + {t('recent_transactions')} +
@@ -1088,10 +1107,15 @@ export function ResizableDashboardCards() { style={{ borderColor: colors.$4 }} className="flex justify-between items-center border-b py-3 px-6" > - {t('invoices')} + + {t('invoices')} + - +
{formatMoney( totalsData[currency]?.invoices?.invoiced_amount || 0, @@ -1099,7 +1123,7 @@ export function ResizableDashboardCards() { currency.toString(), 2 )} - +
@@ -1107,16 +1131,21 @@ export function ResizableDashboardCards() { style={{ borderColor: colors.$4 }} className="flex justify-between items-center border-b py-3 px-6" > - {t('payments')} + + {t('payments')} + - +
{formatMoney( totalsData[currency]?.revenue?.paid_to_date || 0, company.settings.country_id, currency.toString(), 2 )} - +
@@ -1124,16 +1153,21 @@ export function ResizableDashboardCards() { style={{ borderColor: colors.$4 }} className="flex justify-between items-center border-b py-3 px-6" > - {t('expenses')} + + {t('expenses')} + - +
{formatMoney( totalsData[currency]?.expenses?.amount || 0, company.settings.country_id, currency.toString(), 2 )} - +
@@ -1141,16 +1175,21 @@ export function ResizableDashboardCards() { style={{ borderColor: colors.$4 }} className="flex justify-between items-center border-b py-3 px-6" > - {t('outstanding')} + + {t('outstanding')} + - +
{formatMoney( totalsData[currency]?.outstanding?.amount || 0, company.settings.country_id, currency.toString(), 2 )} - +
@@ -1158,13 +1197,18 @@ export function ResizableDashboardCards() { style={{ borderColor: colors.$4 }} className="flex justify-between items-center border-b py-3 px-6" > - {t('total_invoices_outstanding')} + + {t('total_invoices_outstanding')} + - +
{totalsData[currency]?.outstanding ?.outstanding_count || 0} - +
diff --git a/src/pages/dashboard/components/UpcomingInvoices.tsx b/src/pages/dashboard/components/UpcomingInvoices.tsx index fdd9fc0ede..c254b53722 100644 --- a/src/pages/dashboard/components/UpcomingInvoices.tsx +++ b/src/pages/dashboard/components/UpcomingInvoices.tsx @@ -86,6 +86,7 @@ export function UpcomingInvoices() { title={t('upcoming_invoices')} className="h-full relative" withoutBodyPadding + renderFromShadcn >
Date: Thu, 19 Dec 2024 23:21:25 +0100 Subject: [PATCH 18/36] Improved logic --- .../components/ResizableDashboardCards.tsx | 89 +++++++++++++------ 1 file changed, 63 insertions(+), 26 deletions(-) diff --git a/src/pages/dashboard/components/ResizableDashboardCards.tsx b/src/pages/dashboard/components/ResizableDashboardCards.tsx index 4c7ccb6a75..c03b88d933 100644 --- a/src/pages/dashboard/components/ResizableDashboardCards.tsx +++ b/src/pages/dashboard/components/ResizableDashboardCards.tsx @@ -150,7 +150,7 @@ const GLOBAL_DATE_RANGES: Record = { const initialLayouts = { lg: [ - { i: '0', x: 80, y: 0, w: 100, h: 2.8, isResizable: false, static: true }, + { i: '0', x: 30, y: 0, w: 100, h: 2.8, isResizable: false, static: true }, { i: '1', x: 0, @@ -244,7 +244,17 @@ const initialLayouts = { }, ], md: [ - { i: '1', x: 80, y: 0, w: 100, h: 2.8, isResizable: false, static: true }, + { i: '0', x: 30, y: 0, w: 100, h: 2.8, isResizable: false, static: true }, + { + i: '1', + x: 0, + y: 1, + w: 100, + h: 6.3, + minH: 6.3, + minW: 100, + isResizable: false, + }, { i: '2', x: 0, @@ -337,7 +347,17 @@ const initialLayouts = { }, ], sm: [ - { i: '1', x: 80, y: 0, w: 100, h: 2.8, isResizable: false, static: true }, + { i: '0', x: 80, y: 0, w: 100, h: 2.8, isResizable: false, static: true }, + { + i: '1', + x: 0, + y: 1, + w: 100, + h: 6.3, + minH: 6.3, + minW: 100, + isResizable: false, + }, { i: '2', x: 0, @@ -430,7 +450,17 @@ const initialLayouts = { }, ], xs: [ - { i: '1', x: 80, y: 0, w: 100, h: 2.8, isResizable: false, static: true }, + { i: '0', x: 80, y: 0, w: 100, h: 2.8, isResizable: false, static: true }, + { + i: '1', + x: 0, + y: 1, + w: 100, + h: 6.3, + minH: 6.3, + minW: 100, + isResizable: false, + }, { i: '2', x: 0, @@ -523,7 +553,17 @@ const initialLayouts = { }, ], xxs: [ - { i: '1', x: 80, y: 0, w: 100, h: 2.8, isResizable: false, static: true }, + { i: '0', x: 80, y: 0, w: 100, h: 2.8, isResizable: false, static: true }, + { + i: '1', + x: 0, + y: 1, + w: 100, + h: 6.3, + minH: 6.3, + minW: 100, + isResizable: false, + }, { i: '2', x: 0, @@ -728,17 +768,14 @@ export function ResizableDashboardCards() { if (!currentWidth) return; const cardWidth = 240; + const gap = 12; const totalCards = user?.company_user?.settings.dashboard_fields?.length || 0; - const cardsPerRow = Math.floor( - (currentWidth - (totalCards - 1) * 12) / cardWidth - ); + const cardsPerRow = Math.floor((currentWidth + gap) / (cardWidth + gap)); const numberOfRows = Math.ceil( totalCards / (totalCards ? cardsPerRow || 1 : 0) ); - console.log(totalCards, cardsPerRow, numberOfRows); - setLayouts((currentLayouts) => { const updatedLayouts = cloneDeep(currentLayouts); @@ -824,11 +861,9 @@ export function ResizableDashboardCards() { }, [chart.data]); useEffect(() => { - console.log(layoutBreakpoint); - if (layoutBreakpoint) { if (settings?.dashboard_cards_configuration && !isLayoutsInitialized) { - //setLayouts(cloneDeep(settings?.dashboard_cards_configuration)); + setLayouts(cloneDeep(settings?.dashboard_cards_configuration)); } setIsLayoutsInitialized(true); @@ -836,16 +871,14 @@ export function ResizableDashboardCards() { }, [layoutBreakpoint]); useEffect(() => { - if (isLayoutsInitialized) { - updateLayoutHeight(); - } - const handleResize = () => { if (isLayoutsInitialized) { updateLayoutHeight(); } }; + updateLayoutHeight(); + window.addEventListener('resize', handleResize); return () => { @@ -1035,16 +1068,20 @@ export function ResizableDashboardCards() { {isEditMode && (
+ onClick={() => { layoutBreakpoint && - setLayouts((currentLayouts) => ({ - ...currentLayouts, - [layoutBreakpoint]: - initialLayouts[ - layoutBreakpoint as keyof typeof initialLayouts - ], - })) - } + setLayouts((currentLayouts) => ({ + ...currentLayouts, + [layoutBreakpoint]: + initialLayouts[ + layoutBreakpoint as keyof typeof initialLayouts + ], + })); + + setTimeout(() => { + updateLayoutHeight(); + }, 100); + }} >
From 26a1adb7d95c0a221ce01963e997400fa03a912e Mon Sep 17 00:00:00 2001 From: Civolilah Date: Sat, 21 Dec 2024 17:12:51 +0100 Subject: [PATCH 19/36] Polished logic for dashboard configuration --- src/components/DataTable.tsx | 9 ++- src/components/tables/Table.tsx | 10 ++- src/pages/dashboard/components/Activity.tsx | 6 +- .../dashboard/components/ExpiredQuotes.tsx | 10 +-- .../dashboard/components/PastDueInvoices.tsx | 9 +-- .../dashboard/components/RecentPayments.tsx | 9 +-- .../components/ResizableDashboardCards.tsx | 61 ++++++++++++------- .../dashboard/components/UpcomingInvoices.tsx | 9 +-- .../dashboard/components/UpcomingQuotes.tsx | 9 +-- .../components/UpcomingRecurringInvoices.tsx | 10 +-- 10 files changed, 92 insertions(+), 50 deletions(-) diff --git a/src/components/DataTable.tsx b/src/components/DataTable.tsx index 01fb60b2d7..56fcad33d4 100644 --- a/src/components/DataTable.tsx +++ b/src/components/DataTable.tsx @@ -153,6 +153,7 @@ interface Props extends CommonProps { withoutSortQueryParameter?: boolean; showRestoreBulk?: (selectedResources: T[]) => boolean; withTransparentBackground?: boolean; + height?: 'full'; } export type ResourceAction = (resource: T) => ReactElement; @@ -468,7 +469,12 @@ export function DataTable(props: Props) { }, []); return ( -
+
{!props.withoutActions && ( (props: Props) { } isDataLoading={isLoading} style={props.style} + height={props.height} withTransparentBackground={props.withTransparentBackground} > diff --git a/src/components/tables/Table.tsx b/src/components/tables/Table.tsx index 0aa38530cf..a834bd67fe 100644 --- a/src/components/tables/Table.tsx +++ b/src/components/tables/Table.tsx @@ -22,6 +22,7 @@ interface Props extends CommonProps { onVerticalOverflowChange?: (overflow: boolean) => void; isDataLoading?: boolean; withTransparentBackground?: boolean; + height?: 'full'; } export function Table(props: Props) { @@ -83,11 +84,17 @@ export function Table(props: Props) { className={classNames('flex flex-col', { 'mt-2': !props.withoutPadding, })} + style={{ + height: props.height === 'full' ? '100%' : undefined, + }} >
diff --git a/src/pages/dashboard/components/Activity.tsx b/src/pages/dashboard/components/Activity.tsx index 2a0a969484..9f88567495 100644 --- a/src/pages/dashboard/components/Activity.tsx +++ b/src/pages/dashboard/components/Activity.tsx @@ -36,7 +36,6 @@ export function Activity() { className="relative" height="full" withoutBodyPadding - withScrollableBody renderFromShadcn > {isLoading && ( @@ -49,7 +48,10 @@ export function Activity() { {t('error_refresh_page')} )} -
+
{data?.data.data && data.data.data.map((record: ActivityRecord, index: number) => ( diff --git a/src/pages/dashboard/components/ExpiredQuotes.tsx b/src/pages/dashboard/components/ExpiredQuotes.tsx index 6fd202eeee..6e85dd2e24 100644 --- a/src/pages/dashboard/components/ExpiredQuotes.tsx +++ b/src/pages/dashboard/components/ExpiredQuotes.tsx @@ -79,11 +79,15 @@ export function ExpiredQuotes() { height="full" renderFromShadcn > -
+
diff --git a/src/pages/dashboard/components/PastDueInvoices.tsx b/src/pages/dashboard/components/PastDueInvoices.tsx index 86979d9e5d..75e20c123e 100644 --- a/src/pages/dashboard/components/PastDueInvoices.tsx +++ b/src/pages/dashboard/components/PastDueInvoices.tsx @@ -85,11 +85,15 @@ export function PastDueInvoices() { withoutHeaderBorder renderFromShadcn > -
+
diff --git a/src/pages/dashboard/components/RecentPayments.tsx b/src/pages/dashboard/components/RecentPayments.tsx index 71f4e006ed..c4862c0146 100644 --- a/src/pages/dashboard/components/RecentPayments.tsx +++ b/src/pages/dashboard/components/RecentPayments.tsx @@ -98,11 +98,15 @@ export function RecentPayments() { withoutBodyPadding renderFromShadcn > -
+
diff --git a/src/pages/dashboard/components/ResizableDashboardCards.tsx b/src/pages/dashboard/components/ResizableDashboardCards.tsx index c03b88d933..f8d892714d 100644 --- a/src/pages/dashboard/components/ResizableDashboardCards.tsx +++ b/src/pages/dashboard/components/ResizableDashboardCards.tsx @@ -157,8 +157,6 @@ const initialLayouts = { y: 1, w: 100, h: 6.3, - minH: 6.3, - minW: 100, isResizable: false, }, { @@ -167,8 +165,10 @@ const initialLayouts = { y: 2, w: 33, h: 25.4, - minH: 18.144, - minW: 18, + minH: 20, + minW: 25, + maxH: 30, + maxW: 40, }, { i: '3', @@ -176,8 +176,10 @@ const initialLayouts = { y: 2, w: 66, h: 25.4, - minH: 18.144, - minW: 18, + minH: 20, + minW: 40, + maxH: 30, + maxW: 100, }, { i: '4', @@ -185,8 +187,10 @@ const initialLayouts = { y: 3, w: 49.5, h: 20, - minH: 18.144, - minW: 18, + minH: 16, + minW: 35, + maxH: 30, + maxW: 70, }, { i: '5', @@ -194,8 +198,10 @@ const initialLayouts = { y: 3, w: 49.5, h: 20, - minH: 18.144, - minW: 18, + minH: 16, + minW: 35, + maxH: 30, + maxW: 70, }, { i: '6', @@ -203,8 +209,10 @@ const initialLayouts = { y: 4, w: 49.5, h: 20, - minH: 18.144, - minW: 18, + minH: 16, + minW: 35, + maxH: 30, + maxW: 70, }, { i: '7', @@ -212,8 +220,10 @@ const initialLayouts = { y: 4, w: 49.5, h: 20, - minH: 18.144, - minW: 18, + minH: 16, + minW: 35, + maxH: 30, + maxW: 70, }, { i: '8', @@ -221,8 +231,10 @@ const initialLayouts = { y: 5, w: 49.5, h: 20, - minH: 18.144, - minW: 18, + minH: 16, + minW: 35, + maxH: 30, + maxW: 70, }, { i: '9', @@ -230,8 +242,10 @@ const initialLayouts = { y: 5, w: 49.5, h: 20, - minH: 18.144, - minW: 18, + minH: 16, + minW: 35, + maxH: 30, + maxW: 70, }, { i: '10', @@ -239,8 +253,10 @@ const initialLayouts = { y: 6, w: 49.5, h: 20, - minH: 18.144, - minW: 18, + minH: 16, + minW: 35, + maxH: 30, + maxW: 70, }, ], md: [ @@ -863,7 +879,7 @@ export function ResizableDashboardCards() { useEffect(() => { if (layoutBreakpoint) { if (settings?.dashboard_cards_configuration && !isLayoutsInitialized) { - setLayouts(cloneDeep(settings?.dashboard_cards_configuration)); + //setLayouts(cloneDeep(settings?.dashboard_cards_configuration)); } setIsLayoutsInitialized(true); @@ -937,7 +953,8 @@ export function ResizableDashboardCards() { } onResizeStop={onResizeStop} onDragStop={onDragStop} - resizeHandles={['s', 'w', 'e', 'n', 'sw', 'nw', 'se', 'ne']} + //resizeHandles={['s', 'w', 'e', 'n', 'sw', 'nw', 'se', 'ne']} + resizeHandles={['se']} > {(totals.isLoading || !isLayoutsInitialized) && (
diff --git a/src/pages/dashboard/components/UpcomingInvoices.tsx b/src/pages/dashboard/components/UpcomingInvoices.tsx index c254b53722..042a8389b2 100644 --- a/src/pages/dashboard/components/UpcomingInvoices.tsx +++ b/src/pages/dashboard/components/UpcomingInvoices.tsx @@ -88,11 +88,15 @@ export function UpcomingInvoices() { withoutBodyPadding renderFromShadcn > -
+
diff --git a/src/pages/dashboard/components/UpcomingQuotes.tsx b/src/pages/dashboard/components/UpcomingQuotes.tsx index 3ce978e8a3..bb872c234a 100644 --- a/src/pages/dashboard/components/UpcomingQuotes.tsx +++ b/src/pages/dashboard/components/UpcomingQuotes.tsx @@ -79,11 +79,15 @@ export function UpcomingQuotes() { height="full" renderFromShadcn > -
+
diff --git a/src/pages/dashboard/components/UpcomingRecurringInvoices.tsx b/src/pages/dashboard/components/UpcomingRecurringInvoices.tsx index 7c856b544d..c62bbc237b 100644 --- a/src/pages/dashboard/components/UpcomingRecurringInvoices.tsx +++ b/src/pages/dashboard/components/UpcomingRecurringInvoices.tsx @@ -102,11 +102,15 @@ export function UpcomingRecurringInvoices(props: Props) { height="full" renderFromShadcn > -
+
From 0f8733ef61a6038e3a5975a687f2f29c6ff01760 Mon Sep 17 00:00:00 2001 From: Civolilah Date: Sun, 22 Dec 2024 16:34:30 +0100 Subject: [PATCH 20/36] Implemented restoring modal for cards --- src/common/hooks/useReactSettings.ts | 26 +- src/components/cards/Card.tsx | 16 +- .../components/ResizableDashboardCards.tsx | 569 +++++++++++------- .../components/RestoreCardsModal.tsx | 174 ++++++ 4 files changed, 556 insertions(+), 229 deletions(-) create mode 100644 src/pages/dashboard/components/RestoreCardsModal.tsx diff --git a/src/common/hooks/useReactSettings.ts b/src/common/hooks/useReactSettings.ts index e4e243f179..b7da0c1382 100644 --- a/src/common/hooks/useReactSettings.ts +++ b/src/common/hooks/useReactSettings.ts @@ -16,6 +16,7 @@ import { Record as ClientMapRecord } from '../constants/exports/client-map'; import { Entity } from '$app/components/CommonActionsPreferenceModal'; import { PerPage } from '$app/components/DataTable'; import { ThemeColorField } from '$app/pages/settings/user/components/StatusColorTheme'; +import { DashboardGridLayouts } from '$app/pages/dashboard/components/ResizableDashboardCards'; export type ChartsDefaultView = 'day' | 'week' | 'month'; @@ -52,6 +53,28 @@ export type ImportTemplates = Record>; type ColorTheme = Record; +export type DashboardConfigurationBreakpoint = + | 'xxs' + | 'xs' + | 'sm' + | 'md' + | 'lg'; + +export interface DashboardCardConfiguration { + i: string; + h: number; + w: number; + x: number; + y: number; + minH: number; + minW: number; + maxH: number; + maxW: number; + isResizable: boolean; + moved: boolean; + static: boolean; +} + export interface ReactSettings { show_pdf_preview: boolean; react_table_columns?: Record; @@ -67,7 +90,8 @@ export interface ReactSettings { show_table_footer?: boolean; dark_mode?: boolean; color_theme?: ColorTheme; - dashboard_cards_configuration?: any; + dashboard_cards_configuration?: DashboardGridLayouts; + removed_dashboard_cards?: Record; } export type ReactTableColumns = diff --git a/src/components/cards/Card.tsx b/src/components/cards/Card.tsx index 0929797043..69d0a45f55 100644 --- a/src/components/cards/Card.tsx +++ b/src/components/cards/Card.tsx @@ -89,11 +89,19 @@ export function Card(props: Props) { > {Boolean(props.title || props.description) && ( - {props.title && {props.title}} +
+
+ {props.title && {props.title}} - {props.description && ( - {props.description} - )} + {props.description && ( + {props.description} + )} +
+ + {props.topRight && ( +
{props.topRight}
+ )} +
)} diff --git a/src/pages/dashboard/components/ResizableDashboardCards.tsx b/src/pages/dashboard/components/ResizableDashboardCards.tsx index f8d892714d..96e60f7192 100644 --- a/src/pages/dashboard/components/ResizableDashboardCards.tsx +++ b/src/pages/dashboard/components/ResizableDashboardCards.tsx @@ -59,6 +59,8 @@ import { updateUser } from '$app/common/stores/slices/user'; import { useDispatch } from 'react-redux'; import { DashboardCard } from './DashboardCard'; import { MdRefresh } from 'react-icons/md'; +import { toast } from '$app/common/helpers/toast/toast'; +import { RestoreCardsModal } from './RestoreCardsModal'; const ResponsiveGridLayout = WidthProvider(Responsive); @@ -74,6 +76,8 @@ interface Currency { label: string; } +type CardName = 'account_login_text'; + export type DashboardGridLayouts = GridLayout.Layouts; export interface ChartData { @@ -150,12 +154,20 @@ const GLOBAL_DATE_RANGES: Record = { const initialLayouts = { lg: [ - { i: '0', x: 30, y: 0, w: 100, h: 2.8, isResizable: false, static: true }, + { + i: '0', + x: 300, + y: 0, + w: 1000, + h: 2.8, + isResizable: false, + static: true, + }, { i: '1', x: 0, y: 1, - w: 100, + w: 1000, h: 6.3, isResizable: false, }, @@ -163,512 +175,572 @@ const initialLayouts = { i: '2', x: 0, y: 2, - w: 33, + w: 330, h: 25.4, minH: 20, - minW: 25, + minW: 250, maxH: 30, - maxW: 40, + maxW: 400, }, { i: '3', - x: 40, + x: 400, y: 2, - w: 66, + w: 660, h: 25.4, minH: 20, - minW: 40, + minW: 400, maxH: 30, - maxW: 100, + maxW: 1000, }, { i: '4', x: 0, y: 3, - w: 49.5, + w: 495, h: 20, minH: 16, - minW: 35, + minW: 350, maxH: 30, - maxW: 70, + maxW: 700, }, { i: '5', - x: 51, + x: 510, y: 3, - w: 49.5, + w: 495, h: 20, minH: 16, - minW: 35, + minW: 350, maxH: 30, - maxW: 70, + maxW: 700, }, { i: '6', x: 0, y: 4, - w: 49.5, + w: 495, h: 20, minH: 16, - minW: 35, + minW: 350, maxH: 30, - maxW: 70, + maxW: 700, }, { i: '7', - x: 51, + x: 510, y: 4, - w: 49.5, + w: 495, h: 20, minH: 16, - minW: 35, + minW: 350, maxH: 30, - maxW: 70, + maxW: 700, }, { i: '8', x: 0, y: 5, - w: 49.5, + w: 495, h: 20, minH: 16, - minW: 35, + minW: 350, maxH: 30, - maxW: 70, + maxW: 700, }, { i: '9', - x: 51, + x: 510, y: 5, - w: 49.5, + w: 495, h: 20, minH: 16, - minW: 35, + minW: 350, maxH: 30, - maxW: 70, + maxW: 700, }, { i: '10', x: 0, y: 6, - w: 49.5, + w: 495, h: 20, minH: 16, - minW: 35, + minW: 350, maxH: 30, - maxW: 70, + maxW: 700, }, ], md: [ - { i: '0', x: 30, y: 0, w: 100, h: 2.8, isResizable: false, static: true }, + { + i: '0', + x: 300, + y: 0, + w: 1000, + h: 2.8, + isResizable: false, + static: true, + }, { i: '1', x: 0, y: 1, - w: 100, + w: 1000, h: 6.3, - minH: 6.3, - minW: 100, isResizable: false, }, { i: '2', x: 0, y: 1, - w: 100, + w: 1000, h: 25.4, - minH: 18.144, - minW: 18, + minH: 20, + minW: 400, + maxH: 30, + maxW: 1000, }, { i: '3', x: 0, y: 2, - w: 100, + w: 1000, h: 25.4, - minH: 18.144, - minW: 18, + minH: 20, + minW: 400, + maxH: 30, + maxW: 1000, }, { i: '4', x: 0, y: 3, - w: 100, + w: 1000, h: 20, - minH: 18.144, - minW: 18, + minH: 16, + minW: 400, + maxH: 30, + maxW: 1000, }, { i: '5', x: 0, y: 4, - w: 100, + w: 1000, h: 20, - minH: 18.144, - minW: 18, + minH: 16, + minW: 400, + maxH: 30, + maxW: 1000, }, { i: '6', x: 0, y: 5, - w: 100, + w: 1000, h: 20, - minH: 18.144, - minW: 18, + minH: 16, + minW: 400, + maxH: 30, + maxW: 1000, }, { i: '7', x: 0, y: 6, - w: 100, + w: 1000, h: 20, - minH: 18.144, - minW: 18, + minH: 16, + minW: 400, + maxH: 30, + maxW: 1000, }, { i: '8', x: 0, y: 7, - w: 100, + w: 1000, h: 20, - minH: 18.144, - minW: 18, + minH: 16, + minW: 400, + maxH: 30, + maxW: 1000, }, { i: '9', x: 0, y: 8, - w: 100, + w: 1000, h: 20, - minH: 18.144, - minW: 18, + minH: 16, + minW: 400, + maxH: 30, + maxW: 1000, }, { i: '10', x: 0, y: 9, - w: 100, + w: 1000, h: 20, - minH: 18.144, - minW: 18, - }, - { - i: '11', - x: 0, - y: 1, - w: 40, - h: 30, - minH: 10, - minW: 10, + minH: 16, + minW: 400, + maxH: 30, + maxW: 1000, }, ], sm: [ - { i: '0', x: 80, y: 0, w: 100, h: 2.8, isResizable: false, static: true }, + { + i: '0', + x: 300, + y: 0, + w: 1000, + h: 2.8, + isResizable: false, + static: true, + }, { i: '1', x: 0, y: 1, - w: 100, + w: 1000, h: 6.3, - minH: 6.3, - minW: 100, isResizable: false, }, { i: '2', x: 0, y: 1, - w: 100, + w: 1000, h: 25.4, - minH: 18.144, - minW: 18, + minH: 20, + minW: 400, + maxH: 30, + maxW: 1000, }, { i: '3', x: 0, y: 2, - w: 100, + w: 1000, h: 25.4, - minH: 18.144, - minW: 18, + minH: 20, + minW: 400, + maxH: 30, + maxW: 1000, }, { i: '4', x: 0, y: 3, - w: 100, + w: 1000, h: 20, - minH: 18.144, - minW: 18, + minH: 16, + minW: 400, + maxH: 30, + maxW: 1000, }, { i: '5', x: 0, y: 4, - w: 100, + w: 1000, h: 20, - minH: 18.144, - minW: 18, + minH: 16, + minW: 400, + maxH: 30, + maxW: 1000, }, { i: '6', x: 0, y: 5, - w: 100, + w: 1000, h: 20, - minH: 18.144, - minW: 18, + minH: 16, + minW: 400, + maxH: 30, + maxW: 1000, }, { i: '7', x: 0, y: 6, - w: 100, + w: 1000, h: 20, - minH: 18.144, - minW: 18, + minH: 16, + minW: 400, + maxH: 30, + maxW: 1000, }, { i: '8', x: 0, y: 7, - w: 100, + w: 1000, h: 20, - minH: 18.144, - minW: 18, + minH: 16, + minW: 400, + maxH: 30, + maxW: 1000, }, { i: '9', x: 0, y: 8, - w: 100, + w: 1000, h: 20, - minH: 18.144, - minW: 18, + minH: 16, + minW: 400, + maxH: 30, + maxW: 1000, }, { i: '10', x: 0, y: 9, - w: 100, + w: 1000, h: 20, - minH: 18.144, - minW: 18, - }, - { - i: '11', - x: 0, - y: 1, - w: 40, - h: 30, - minH: 10, - minW: 10, + minH: 16, + minW: 400, + maxH: 30, + maxW: 1000, }, ], xs: [ - { i: '0', x: 80, y: 0, w: 100, h: 2.8, isResizable: false, static: true }, + { + i: '0', + x: 300, + y: 0, + w: 1000, + h: 2.8, + isResizable: false, + static: true, + }, { i: '1', x: 0, y: 1, - w: 100, + w: 1000, h: 6.3, - minH: 6.3, - minW: 100, isResizable: false, }, { i: '2', x: 0, y: 1, - w: 100, + w: 1000, h: 25.4, - minH: 18.144, - minW: 18, + minH: 20, + minW: 400, + maxH: 30, + maxW: 1000, }, { i: '3', x: 0, y: 2, - w: 100, + w: 1000, h: 25.4, - minH: 18.144, - minW: 18, + minH: 20, + minW: 400, + maxH: 30, + maxW: 1000, }, { i: '4', x: 0, y: 3, - w: 100, + w: 1000, h: 20, - minH: 18.144, - minW: 18, + minH: 16, + minW: 400, + maxH: 30, + maxW: 1000, }, { i: '5', x: 0, y: 4, - w: 100, + w: 1000, h: 20, - minH: 18.144, - minW: 18, + minH: 16, + minW: 400, + maxH: 30, + maxW: 1000, }, { i: '6', x: 0, y: 5, - w: 100, + w: 1000, h: 20, - minH: 18.144, - minW: 18, + minH: 16, + minW: 400, + maxH: 30, + maxW: 1000, }, { i: '7', x: 0, y: 6, - w: 100, + w: 1000, h: 20, - minH: 18.144, - minW: 18, + minH: 16, + minW: 400, + maxH: 30, + maxW: 1000, }, { i: '8', x: 0, y: 7, - w: 100, + w: 1000, h: 20, - minH: 18.144, - minW: 18, + minH: 16, + minW: 400, + maxH: 30, + maxW: 1000, }, { i: '9', x: 0, y: 8, - w: 100, + w: 1000, h: 20, - minH: 18.144, - minW: 18, + minH: 16, + minW: 400, + maxH: 30, + maxW: 1000, }, { i: '10', x: 0, y: 9, - w: 100, + w: 1000, h: 20, - minH: 18.144, - minW: 18, - }, - { - i: '11', - x: 0, - y: 1, - w: 40, - h: 30, - minH: 10, - minW: 10, + minH: 16, + minW: 400, + maxH: 30, + maxW: 1000, }, ], xxs: [ - { i: '0', x: 80, y: 0, w: 100, h: 2.8, isResizable: false, static: true }, + { + i: '0', + x: 300, + y: 0, + w: 1000, + h: 2.8, + isResizable: false, + static: true, + }, { i: '1', x: 0, y: 1, - w: 100, + w: 1000, h: 6.3, - minH: 6.3, - minW: 100, isResizable: false, }, { i: '2', x: 0, y: 1, - w: 100, + w: 1000, h: 25.4, - minH: 18.144, - minW: 18, + minH: 20, + minW: 400, + maxH: 30, + maxW: 1000, }, { i: '3', x: 0, y: 2, - w: 100, + w: 1000, h: 25.4, - minH: 18.144, - minW: 18, + minH: 20, + minW: 400, + maxH: 30, + maxW: 1000, }, { i: '4', x: 0, y: 3, - w: 100, + w: 1000, h: 20, - minH: 18.144, - minW: 18, + minH: 16, + minW: 400, + maxH: 30, + maxW: 1000, }, { i: '5', x: 0, y: 4, - w: 100, + w: 1000, h: 20, - minH: 18.144, - minW: 18, + minH: 16, + minW: 400, + maxH: 30, + maxW: 1000, }, { i: '6', x: 0, y: 5, - w: 100, + w: 1000, h: 20, - minH: 18.144, - minW: 18, + minH: 16, + minW: 400, + maxH: 30, + maxW: 1000, }, { i: '7', x: 0, y: 6, - w: 100, + w: 1000, h: 20, - minH: 18.144, - minW: 18, + minH: 16, + minW: 400, + maxH: 30, + maxW: 1000, }, { i: '8', x: 0, y: 7, - w: 100, + w: 1000, h: 20, - minH: 18.144, - minW: 18, + minH: 16, + minW: 400, + maxH: 30, + maxW: 1000, }, { i: '9', x: 0, y: 8, - w: 100, + w: 1000, h: 20, - minH: 18.144, - minW: 18, + minH: 16, + minW: 400, + maxH: 30, + maxW: 1000, }, { i: '10', x: 0, y: 9, - w: 100, + w: 1000, h: 20, - minH: 18.144, - minW: 18, - }, - { - i: '11', - x: 0, - y: 1, - w: 40, - h: 30, - minH: 10, - minW: 10, + minH: 16, + minW: 400, + maxH: 30, + maxW: 1000, }, ], }; @@ -694,9 +766,9 @@ export function ResizableDashboardCards() { const [layoutBreakpoint, setLayoutBreakpoint] = useState(); const [layouts, setLayouts] = useState(initialLayouts); + const [isEditMode, setIsEditMode] = useState(false); const [isLayoutsInitialized, setIsLayoutsInitialized] = useState(false); - const [isEditMode, setIsEditMode] = useState(false); const chartScale = settings?.preferences?.dashboard_charts?.default_view || 'month'; @@ -822,6 +894,35 @@ export function ResizableDashboardCards() { // delete updatedUser.company_user.react_settings // .dashboard_cards_configuration; + // delete updatedUser.company_user.react_settings.removed_dashboard_cards; + + request( + 'PUT', + endpoint('/api/v1/company_users/:id', { id: updatedUser.id }), + updatedUser + ).then((response: GenericSingleResourceResponse) => { + set(updatedUser, 'company_user', response.data.data); + + $refetch(['company_users']); + + dispatch(updateUser(updatedUser)); + }); + }; + + const handleRemoveCard = (cardName: CardName) => { + toast.processing(); + + const updatedUser = cloneDeep(user) as User; + + const removedCards = + settings?.removed_dashboard_cards?.[layoutBreakpoint || ''] || []; + + set( + updatedUser, + `company_user.react_settings.removed_dashboard_cards.${layoutBreakpoint}`, + [...removedCards, cardName] + ); + request( 'PUT', endpoint('/api/v1/company_users/:id', { id: updatedUser.id }), @@ -829,12 +930,22 @@ export function ResizableDashboardCards() { ).then((response: GenericSingleResourceResponse) => { set(updatedUser, 'company_user', response.data.data); + toast.success('removed'); + $refetch(['company_users']); dispatch(updateUser(updatedUser)); }); }; + const isCardRemoved = (cardName: CardName) => { + if (!layoutBreakpoint) return false; + + return settings?.removed_dashboard_cards?.[ + layoutBreakpoint || '' + ]?.includes(cardName); + }; + useEffect(() => { setBody((current) => ({ ...current, @@ -879,10 +990,10 @@ export function ResizableDashboardCards() { useEffect(() => { if (layoutBreakpoint) { if (settings?.dashboard_cards_configuration && !isLayoutsInitialized) { - //setLayouts(cloneDeep(settings?.dashboard_cards_configuration)); - } + setLayouts(cloneDeep(settings?.dashboard_cards_configuration)); - setIsLayoutsInitialized(true); + setIsLayoutsInitialized(true); + } } }, [layoutBreakpoint]); @@ -941,7 +1052,7 @@ export function ResizableDashboardCards() { xxs: 0, }} layouts={layouts} - cols={{ lg: 100, md: 100, sm: 60, xs: 40, xxs: 30 }} + cols={{ lg: 1000, md: 1000, sm: 1000, xs: 1000, xxs: 1000 }} draggableHandle=".drag-handle" margin={[0, 20]} rowHeight={1} @@ -955,6 +1066,7 @@ export function ResizableDashboardCards() { onDragStop={onDragStop} //resizeHandles={['s', 'w', 'e', 'n', 'sw', 'nw', 'se', 'ne']} resizeHandles={['se']} + compactType="vertical" > {(totals.isLoading || !isLayoutsInitialized) && (
@@ -1083,25 +1195,32 @@ export function ResizableDashboardCards() {
{isEditMode && ( -
{ - layoutBreakpoint && - setLayouts((currentLayouts) => ({ - ...currentLayouts, - [layoutBreakpoint]: - initialLayouts[ - layoutBreakpoint as keyof typeof initialLayouts - ], - })); - - setTimeout(() => { - updateLayoutHeight(); - }, 100); - }} - > - -
+ <> + + +
{ + layoutBreakpoint && + setLayouts((currentLayouts) => ({ + ...currentLayouts, + [layoutBreakpoint]: + initialLayouts[ + layoutBreakpoint as keyof typeof initialLayouts + ], + })); + + setTimeout(() => { + updateLayoutHeight(); + }, 100); + }} + > + +
+ )}
@@ -1127,7 +1246,7 @@ export function ResizableDashboardCards() { )}
- {company && ( + {company && !isCardRemoved('account_login_text') ? (
- {t('remove')} - - ) + } renderFromShadcn > @@ -1269,7 +1390,7 @@ export function ResizableDashboardCards() {
- )} + ) : null} {chartData && (
props.theme.hoverBgColor}; + } + } +`; + +interface Props { + layoutBreakpoint: string | undefined; + updateLayoutHeight: () => void; +} + +export function RestoreCardsModal(props: Props) { + const [t] = useTranslation(); + + const { updateLayoutHeight, layoutBreakpoint } = props; + + const dispatch = useDispatch(); + + const user = useCurrentUser(); + const colors = useColorScheme(); + const settings = useReactSettings(); + + const [isFormBusy, setIsFormBusy] = useState(false); + const [isModalOpen, setIsModalOpen] = useState(false); + const [currentCards, setCurrentCards] = useState(); + + const handleOnClose = () => { + setIsModalOpen(false); + setCurrentCards(undefined); + }; + + const handleRestoreCards = () => { + if (!isFormBusy && currentCards) { + toast.processing(); + setIsFormBusy(true); + + const updatedUser = cloneDeep(user) as User; + + set( + updatedUser, + `company_user.react_settings.removed_dashboard_cards.${layoutBreakpoint}`, + [...currentCards] + ); + + request( + 'PUT', + endpoint('/api/v1/company_users/:id', { id: updatedUser.id }), + updatedUser + ) + .then((response: GenericSingleResourceResponse) => { + set(updatedUser, 'company_user', response.data.data); + + toast.success('restored'); + + $refetch(['company_users']); + + dispatch(updateUser(updatedUser)); + + setTimeout(() => { + handleOnClose(); + }, 100); + + setTimeout(() => { + updateLayoutHeight(); + }, 200); + }) + .finally(() => setIsFormBusy(false)); + } + }; + + useEffect(() => { + if ( + settings?.removed_dashboard_cards && + isModalOpen && + !currentCards && + layoutBreakpoint + ) { + setCurrentCards(settings?.removed_dashboard_cards[layoutBreakpoint]); + } + }, [settings?.removed_dashboard_cards, isModalOpen, layoutBreakpoint]); + + return ( + <> + handleOnClose()} + disableClosing={isFormBusy} + > + {Boolean(!currentCards?.length) && ( + {t('no_records_found')} + )} + + {Boolean(currentCards?.length) && ( +
+ {currentCards?.map((card) => ( + + setCurrentCards((current) => + current?.filter((c) => c !== card) + ) + } + > + {t(card)} + +
+ +
+
+ ))} +
+ )} + + +
+ +
setIsModalOpen(true)} + > + +
+ + ); +} From 27459eede338a2257860e58045a4fa59fe19470e Mon Sep 17 00:00:00 2001 From: Civolilah Date: Mon, 23 Dec 2024 00:26:11 +0100 Subject: [PATCH 21/36] Adjusted calculation of height --- .../dashboard/components/DashboardCard.tsx | 16 +- .../components/ResizableDashboardCards.tsx | 319 ++++++++++++++++-- 2 files changed, 301 insertions(+), 34 deletions(-) diff --git a/src/pages/dashboard/components/DashboardCard.tsx b/src/pages/dashboard/components/DashboardCard.tsx index 56a2dbfbfa..3ca1333cb9 100644 --- a/src/pages/dashboard/components/DashboardCard.tsx +++ b/src/pages/dashboard/components/DashboardCard.tsx @@ -18,6 +18,7 @@ import { request } from '$app/common/helpers/request'; import { endpoint } from '$app/common/helpers'; import { Spinner } from '$app/components/Spinner'; import { Card as ShadcnCard } from '../../../../components/ui/card'; +import classNames from 'classnames'; interface DashboardCardsProps { dateRange: string; @@ -28,6 +29,7 @@ interface DashboardCardsProps { interface CardProps extends DashboardCardsProps { field: DashboardField; + layoutBreakpoint: string | undefined; } export const PERIOD_LABELS = { @@ -81,7 +83,19 @@ export function DashboardCard(props: CardProps) { }, [field]); return ( - + {isFormBusy && (
diff --git a/src/pages/dashboard/components/ResizableDashboardCards.tsx b/src/pages/dashboard/components/ResizableDashboardCards.tsx index 96e60f7192..9cd223e301 100644 --- a/src/pages/dashboard/components/ResizableDashboardCards.tsx +++ b/src/pages/dashboard/components/ResizableDashboardCards.tsx @@ -153,6 +153,242 @@ const GLOBAL_DATE_RANGES: Record = { }; const initialLayouts = { + xxl: [ + { + i: '0', + x: 300, + y: 0, + w: 1000, + h: 2.8, + isResizable: false, + static: true, + }, + { + i: '1', + x: 0, + y: 1, + w: 1000, + h: 6.3, + isResizable: false, + }, + { + i: '2', + x: 0, + y: 2, + w: 330, + h: 25.4, + minH: 20, + minW: 250, + maxH: 30, + maxW: 400, + }, + { + i: '3', + x: 400, + y: 2, + w: 660, + h: 25.4, + minH: 20, + minW: 400, + maxH: 30, + maxW: 1000, + }, + { + i: '4', + x: 0, + y: 3, + w: 495, + h: 20, + minH: 16, + minW: 350, + maxH: 30, + maxW: 700, + }, + { + i: '5', + x: 510, + y: 3, + w: 495, + h: 20, + minH: 16, + minW: 350, + maxH: 30, + maxW: 700, + }, + { + i: '6', + x: 0, + y: 4, + w: 495, + h: 20, + minH: 16, + minW: 350, + maxH: 30, + maxW: 700, + }, + { + i: '7', + x: 510, + y: 4, + w: 495, + h: 20, + minH: 16, + minW: 350, + maxH: 30, + maxW: 700, + }, + { + i: '8', + x: 0, + y: 5, + w: 495, + h: 20, + minH: 16, + minW: 350, + maxH: 30, + maxW: 700, + }, + { + i: '9', + x: 510, + y: 5, + w: 495, + h: 20, + minH: 16, + minW: 350, + maxH: 30, + maxW: 700, + }, + { + i: '10', + x: 0, + y: 6, + w: 495, + h: 20, + minH: 16, + minW: 350, + maxH: 30, + maxW: 700, + }, + ], + xl: [ + { + i: '0', + x: 300, + y: 0, + w: 1000, + h: 2.8, + isResizable: false, + static: true, + }, + { + i: '1', + x: 0, + y: 1, + w: 1000, + h: 6.3, + isResizable: false, + }, + { + i: '2', + x: 0, + y: 2, + w: 330, + h: 25.4, + minH: 20, + minW: 250, + maxH: 30, + maxW: 400, + }, + { + i: '3', + x: 400, + y: 2, + w: 660, + h: 25.4, + minH: 20, + minW: 400, + maxH: 30, + maxW: 1000, + }, + { + i: '4', + x: 0, + y: 3, + w: 495, + h: 20, + minH: 16, + minW: 350, + maxH: 30, + maxW: 700, + }, + { + i: '5', + x: 510, + y: 3, + w: 495, + h: 20, + minH: 16, + minW: 350, + maxH: 30, + maxW: 700, + }, + { + i: '6', + x: 0, + y: 4, + w: 495, + h: 20, + minH: 16, + minW: 350, + maxH: 30, + maxW: 700, + }, + { + i: '7', + x: 510, + y: 4, + w: 495, + h: 20, + minH: 16, + minW: 350, + maxH: 30, + maxW: 700, + }, + { + i: '8', + x: 0, + y: 5, + w: 495, + h: 20, + minH: 16, + minW: 350, + maxH: 30, + maxW: 700, + }, + { + i: '9', + x: 510, + y: 5, + w: 495, + h: 20, + minH: 16, + minW: 350, + maxH: 30, + maxW: 700, + }, + { + i: '10', + x: 0, + y: 6, + w: 495, + h: 20, + minH: 16, + minW: 350, + maxH: 30, + maxW: 700, + }, + ], lg: [ { i: '0', @@ -851,18 +1087,38 @@ export function ResizableDashboardCards() { }; const updateLayoutHeight = () => { - const currentWidth = document.getElementById('cardsContainer')?.clientWidth; - - if (!currentWidth) return; - - const cardWidth = 240; - const gap = 12; const totalCards = user?.company_user?.settings.dashboard_fields?.length || 0; - const cardsPerRow = Math.floor((currentWidth + gap) / (cardWidth + gap)); - const numberOfRows = Math.ceil( - totalCards / (totalCards ? cardsPerRow || 1 : 0) - ); + let cardsPerRow = 0; + + switch (layoutBreakpoint) { + case 'xxl': + cardsPerRow = 6; + break; + case 'xl': + cardsPerRow = 5; + break; + case 'lg': + cardsPerRow = 4; + break; + case 'md': + cardsPerRow = 3; + break; + case 'sm': + cardsPerRow = 2; + break; + case 'xs': + cardsPerRow = 1; + break; + case 'xxs': + cardsPerRow = 1; + break; + default: + cardsPerRow = 6; + break; + } + + const numberOfRows = Math.ceil(totalCards / cardsPerRow); setLayouts((currentLayouts) => { const updatedLayouts = cloneDeep(currentLayouts); @@ -994,28 +1250,11 @@ export function ResizableDashboardCards() { setIsLayoutsInitialized(true); } + + updateLayoutHeight(); } }, [layoutBreakpoint]); - useEffect(() => { - const handleResize = () => { - if (isLayoutsInitialized) { - updateLayoutHeight(); - } - }; - - updateLayoutHeight(); - - window.addEventListener('resize', handleResize); - - return () => { - window.removeEventListener('resize', handleResize); - }; - }, [ - user?.company_user?.settings.dashboard_fields?.length, - isLayoutsInitialized, - ]); - useDebounce( () => { if ( @@ -1045,6 +1284,8 @@ export function ResizableDashboardCards() { {(totals.isLoading || !isLayoutsInitialized) && (
@@ -1228,8 +1476,12 @@ export function ResizableDashboardCards() {
{user?.company_user?.settings.dashboard_fields?.map( @@ -1241,6 +1493,7 @@ export function ResizableDashboardCards() { startDate={dates.start_date} endDate={dates.end_date} currencyId={currency.toString()} + layoutBreakpoint={layoutBreakpoint} /> ) )} From ca5a281e59e76402821d95caf405b02317fa5ff6 Mon Sep 17 00:00:00 2001 From: Civolilah Date: Mon, 23 Dec 2024 01:58:42 +0100 Subject: [PATCH 22/36] Implemented new way of drag handling --- src/components/cards/Card.tsx | 8 +- .../components/ResizableDashboardCards.tsx | 91 +++++++++++-------- .../components/RestoreCardsModal.tsx | 12 ++- 3 files changed, 68 insertions(+), 43 deletions(-) diff --git a/src/components/cards/Card.tsx b/src/components/cards/Card.tsx index 69d0a45f55..eb395c56ad 100644 --- a/src/components/cards/Card.tsx +++ b/src/components/cards/Card.tsx @@ -64,6 +64,7 @@ interface Props { topRight?: ReactNode; height?: 'full'; renderFromShadcn?: boolean; + titleDescriptionParentClassName?: string; } export function Card(props: Props) { @@ -90,7 +91,12 @@ export function Card(props: Props) { {Boolean(props.title || props.description) && (
-
+
{props.title && {props.title}} {props.description && ( diff --git a/src/pages/dashboard/components/ResizableDashboardCards.tsx b/src/pages/dashboard/components/ResizableDashboardCards.tsx index 9cd223e301..074692d7d4 100644 --- a/src/pages/dashboard/components/ResizableDashboardCards.tsx +++ b/src/pages/dashboard/components/ResizableDashboardCards.tsx @@ -152,7 +152,7 @@ const GLOBAL_DATE_RANGES: Record = { }, }; -const initialLayouts = { +export const initialLayouts = { xxl: [ { i: '0', @@ -1255,6 +1255,12 @@ export function ResizableDashboardCards() { } }, [layoutBreakpoint]); + useEffect(() => { + if (isLayoutsInitialized && settings?.dashboard_cards_configuration) { + //setLayouts(cloneDeep(settings?.dashboard_cards_configuration)); + } + }, [settings?.dashboard_cards_configuration]); + useDebounce( () => { if ( @@ -1447,6 +1453,7 @@ export function ResizableDashboardCards() {
-
- {user?.company_user?.settings.dashboard_fields?.map( - (field, index) => ( - - ) - )} -
- - {company && !isCardRemoved('account_login_text') ? ( + {settings?.dashboard_cards_configuration && (
+ {user?.company_user?.settings.dashboard_fields?.map( + (field, index) => ( + + ) + )} +
+ )} + + {company && !isCardRemoved('account_login_text') ? ( +
handleRemoveCard('account_login_text')} - > - {t('remove')} - + isEditMode && ( + + ) } renderFromShadcn > -
+
{`${user?.first_name} ${user?.last_name}`} diff --git a/src/pages/dashboard/components/RestoreCardsModal.tsx b/src/pages/dashboard/components/RestoreCardsModal.tsx index e530a857f6..e119a85f8d 100644 --- a/src/pages/dashboard/components/RestoreCardsModal.tsx +++ b/src/pages/dashboard/components/RestoreCardsModal.tsx @@ -29,6 +29,7 @@ import { CompanyUser } from '$app/common/interfaces/company-user'; import { $refetch } from '$app/common/hooks/useRefetch'; import { useDispatch } from 'react-redux'; import { updateUser } from '$app/common/stores/slices/user'; +import { initialLayouts } from './ResizableDashboardCards'; const StyledDiv = styled.div` &:hover { @@ -41,6 +42,7 @@ const StyledDiv = styled.div` interface Props { layoutBreakpoint: string | undefined; updateLayoutHeight: () => void; + setLayouts: any; } export function RestoreCardsModal(props: Props) { @@ -91,12 +93,16 @@ export function RestoreCardsModal(props: Props) { dispatch(updateUser(updatedUser)); setTimeout(() => { - handleOnClose(); - }, 100); + props.setLayouts(cloneDeep(initialLayouts)); + }, 200); setTimeout(() => { updateLayoutHeight(); - }, 200); + }, 250); + + setTimeout(() => { + handleOnClose(); + }, 100); }) .finally(() => setIsFormBusy(false)); } From 91a837c573ee2dd818fa31d5259860be1ed64e1b Mon Sep 17 00:00:00 2001 From: Civolilah Date: Tue, 24 Dec 2024 00:23:59 +0100 Subject: [PATCH 23/36] Adjusted logic --- .../components/ResizableDashboardCards.tsx | 52 +++++++++++++++---- .../components/RestoreCardsModal.tsx | 22 ++++---- 2 files changed, 52 insertions(+), 22 deletions(-) diff --git a/src/pages/dashboard/components/ResizableDashboardCards.tsx b/src/pages/dashboard/components/ResizableDashboardCards.tsx index 074692d7d4..3f6d7ed614 100644 --- a/src/pages/dashboard/components/ResizableDashboardCards.tsx +++ b/src/pages/dashboard/components/ResizableDashboardCards.tsx @@ -1255,12 +1255,6 @@ export function ResizableDashboardCards() { } }, [layoutBreakpoint]); - useEffect(() => { - if (isLayoutsInitialized && settings?.dashboard_cards_configuration) { - //setLayouts(cloneDeep(settings?.dashboard_cards_configuration)); - } - }, [settings?.dashboard_cards_configuration]); - useDebounce( () => { if ( @@ -1319,6 +1313,45 @@ export function ResizableDashboardCards() { } onResizeStop={onResizeStop} onDragStop={onDragStop} + onLayoutChange={(currentLayout) => { + if (layoutBreakpoint) { + let isAnyRestored = false; + + setLayouts((currentLayouts) => ({ + ...currentLayouts, + [layoutBreakpoint]: currentLayout.map((layoutCard) => { + if (layoutCard.h === 1 && layoutCard.w === 1) { + const initialCardLayout = initialLayouts[ + layoutBreakpoint as keyof typeof initialLayouts + ]?.find((initial) => initial.i === layoutCard.i); + + if (initialCardLayout) { + isAnyRestored = true; + } + + return initialCardLayout + ? { ...initialCardLayout, y: Infinity } + : layoutCard; + } + + return layoutCard; + }), + })); + + setTimeout(() => { + updateLayoutHeight(); + }, 100); + + setTimeout(() => { + if (isAnyRestored) { + window.scrollTo({ + top: 10100, + behavior: 'smooth', + }); + } + }, 250); + } + }} //resizeHandles={['s', 'w', 'e', 'n', 'sw', 'nw', 'se', 'ne']} resizeHandles={['se']} > @@ -1452,7 +1485,6 @@ export function ResizableDashboardCards() { <> @@ -1480,16 +1512,16 @@ export function ResizableDashboardCards() {
- {settings?.dashboard_cards_configuration && ( + {user?.company_user?.settings.dashboard_fields?.length ? (
{user?.company_user?.settings.dashboard_fields?.map( @@ -1506,7 +1538,7 @@ export function ResizableDashboardCards() { ) )}
- )} + ) : null} {company && !isCardRemoved('account_login_text') ? (
diff --git a/src/pages/dashboard/components/RestoreCardsModal.tsx b/src/pages/dashboard/components/RestoreCardsModal.tsx index e119a85f8d..65457843df 100644 --- a/src/pages/dashboard/components/RestoreCardsModal.tsx +++ b/src/pages/dashboard/components/RestoreCardsModal.tsx @@ -18,7 +18,7 @@ import { Button } from '$app/components/forms'; import { Icon } from '$app/components/icons/Icon'; import { Modal } from '$app/components/Modal'; import { cloneDeep } from 'lodash'; -import { useEffect, useState } from 'react'; +import { Dispatch, SetStateAction, useEffect, useState } from 'react'; import { useTranslation } from 'react-i18next'; import { MdClose, MdRestorePage } from 'react-icons/md'; import styled from 'styled-components'; @@ -29,7 +29,7 @@ import { CompanyUser } from '$app/common/interfaces/company-user'; import { $refetch } from '$app/common/hooks/useRefetch'; import { useDispatch } from 'react-redux'; import { updateUser } from '$app/common/stores/slices/user'; -import { initialLayouts } from './ResizableDashboardCards'; +import { DashboardGridLayouts } from './ResizableDashboardCards'; const StyledDiv = styled.div` &:hover { @@ -41,14 +41,13 @@ const StyledDiv = styled.div` interface Props { layoutBreakpoint: string | undefined; - updateLayoutHeight: () => void; - setLayouts: any; + setLayouts: Dispatch>; } export function RestoreCardsModal(props: Props) { const [t] = useTranslation(); - const { updateLayoutHeight, layoutBreakpoint } = props; + const { layoutBreakpoint } = props; const dispatch = useDispatch(); @@ -92,13 +91,12 @@ export function RestoreCardsModal(props: Props) { dispatch(updateUser(updatedUser)); - setTimeout(() => { - props.setLayouts(cloneDeep(initialLayouts)); - }, 200); - - setTimeout(() => { - updateLayoutHeight(); - }, 250); + props.setLayouts( + cloneDeep( + response.data.data.react_settings + .dashboard_cards_configuration as DashboardGridLayouts + ) + ); setTimeout(() => { handleOnClose(); From c320edc6bf360ea7edbb3417efbdfea003e8f24e Mon Sep 17 00:00:00 2001 From: Civolilah Date: Tue, 24 Dec 2024 20:41:23 +0100 Subject: [PATCH 24/36] Adjusted logic for configuring dashboard --- src/pages/dashboard/components/Activity.tsx | 22 +- .../dashboard/components/ExpiredQuotes.tsx | 8 +- .../dashboard/components/PastDueInvoices.tsx | 8 +- .../dashboard/components/RecentPayments.tsx | 8 +- .../components/ResizableDashboardCards.tsx | 327 ++++++++++++------ .../components/RestoreCardsModal.tsx | 33 +- .../components/RestoreLayoutAction.tsx | 65 ++++ .../dashboard/components/UpcomingInvoices.tsx | 8 +- .../dashboard/components/UpcomingQuotes.tsx | 8 +- .../components/UpcomingRecurringInvoices.tsx | 3 + 10 files changed, 355 insertions(+), 135 deletions(-) create mode 100644 src/pages/dashboard/components/RestoreLayoutAction.tsx diff --git a/src/pages/dashboard/components/Activity.tsx b/src/pages/dashboard/components/Activity.tsx index 9f88567495..10b771de48 100644 --- a/src/pages/dashboard/components/Activity.tsx +++ b/src/pages/dashboard/components/Activity.tsx @@ -8,7 +8,7 @@ * @license https://www.elastic.co/licensing/elastic-license */ -import { endpoint } from '$app/common/helpers'; +import { classNames, endpoint } from '$app/common/helpers'; import { Spinner } from '$app/components/Spinner'; import { request } from '$app/common/helpers/request'; import { useQuery } from 'react-query'; @@ -17,9 +17,14 @@ import { useTranslation } from 'react-i18next'; import { NonClickableElement } from '$app/components/cards/NonClickableElement'; import { ActivityRecord } from '$app/common/interfaces/activity-record'; import { useGenerateActivityElement } from '../hooks/useGenerateActivityElement'; -import React from 'react'; +import React, { ReactNode } from 'react'; -export function Activity() { +interface Props { + isEditMode?: boolean; + topRight?: ReactNode; +} + +export function Activity({ topRight, isEditMode }: Props) { const [t] = useTranslation(); const { data, isLoading, isError } = useQuery( @@ -33,9 +38,12 @@ export function Activity() { return ( {isLoading && ( @@ -49,8 +57,10 @@ export function Activity() { )}
{data?.data.data && diff --git a/src/pages/dashboard/components/ExpiredQuotes.tsx b/src/pages/dashboard/components/ExpiredQuotes.tsx index 6e85dd2e24..53941058a9 100644 --- a/src/pages/dashboard/components/ExpiredQuotes.tsx +++ b/src/pages/dashboard/components/ExpiredQuotes.tsx @@ -18,8 +18,13 @@ import dayjs from 'dayjs'; import { Badge } from '$app/components/Badge'; import { useDisableNavigation } from '$app/common/hooks/useDisableNavigation'; import { DynamicLink } from '$app/components/DynamicLink'; +import { ReactNode } from 'react'; -export function ExpiredQuotes() { +interface Props { + topRight?: ReactNode; +} + +export function ExpiredQuotes({ topRight }: Props) { const [t] = useTranslation(); const formatMoney = useFormatMoney(); @@ -77,6 +82,7 @@ export function ExpiredQuotes() { withoutBodyPadding withoutHeaderBorder height="full" + topRight={topRight} renderFromShadcn >
(false); const [isLayoutsInitialized, setIsLayoutsInitialized] = useState(false); + const [areCardsRestored, setAreCardsRestored] = useState(false); const chartScale = settings?.preferences?.dashboard_charts?.default_view || 'month'; @@ -1202,6 +1212,45 @@ export function ResizableDashboardCards() { ]?.includes(cardName); }; + const handleOnLayoutChange = (currentLayout: GridLayout.Layout[]) => { + if (layoutBreakpoint) { + let isAnyRestored = false; + + setLayouts((currentLayouts) => ({ + ...currentLayouts, + [layoutBreakpoint]: currentLayout.map((layoutCard) => { + if (layoutCard.h === 1 && layoutCard.w === 1) { + const initialCardLayout = initialLayouts[ + layoutBreakpoint as keyof typeof initialLayouts + ]?.find((initial) => initial.i === layoutCard.i); + + if (initialCardLayout) { + isAnyRestored = true; + } + + return initialCardLayout + ? { ...initialCardLayout, y: Infinity } + : layoutCard; + } + + return layoutCard; + }), + })); + + setTimeout(() => { + if (isAnyRestored) { + setAreCardsRestored(false); + + window.scrollTo({ + top: + document.querySelector('.responsive-grid-box')?.scrollHeight || 0, + behavior: 'smooth', + }); + } + }, 450); + } + }; + useEffect(() => { setBody((current) => ({ ...current, @@ -1209,6 +1258,10 @@ export function ResizableDashboardCards() { })); }, [settings?.preferences?.dashboard_charts?.range]); + useEffect(() => { + updateLayoutHeight(); + }, [user?.company_user?.settings.dashboard_fields?.length]); + useEffect(() => { if (totals.data) { setTotalsData(totals.data); @@ -1282,7 +1335,7 @@ export function ResizableDashboardCards() {
{!totals.isLoading ? ( { - if (layoutBreakpoint) { - let isAnyRestored = false; - - setLayouts((currentLayouts) => ({ - ...currentLayouts, - [layoutBreakpoint]: currentLayout.map((layoutCard) => { - if (layoutCard.h === 1 && layoutCard.w === 1) { - const initialCardLayout = initialLayouts[ - layoutBreakpoint as keyof typeof initialLayouts - ]?.find((initial) => initial.i === layoutCard.i); - - if (initialCardLayout) { - isAnyRestored = true; - } - - return initialCardLayout - ? { ...initialCardLayout, y: Infinity } - : layoutCard; - } - - return layoutCard; - }), - })); - - setTimeout(() => { - updateLayoutHeight(); - }, 100); - - setTimeout(() => { - if (isAnyRestored) { - window.scrollTo({ - top: 10100, - behavior: 'smooth', - }); - } - }, 250); - } - }} + onLayoutChange={(current) => + areCardsRestored && handleOnLayoutChange(current) + } //resizeHandles={['s', 'w', 'e', 'n', 'sw', 'nw', 'se', 'ne']} resizeHandles={['se']} > @@ -1486,27 +1503,14 @@ export function ResizableDashboardCards() { -
{ - layoutBreakpoint && - setLayouts((currentLayouts) => ({ - ...currentLayouts, - [layoutBreakpoint]: - initialLayouts[ - layoutBreakpoint as keyof typeof initialLayouts - ], - })); - - setTimeout(() => { - updateLayoutHeight(); - }, 100); - }} - > - -
+ )}
@@ -1690,105 +1694,210 @@ export function ResizableDashboardCards() {
) : null} - {chartData && ( -
+ {chartData && !isCardRemoved('overview') ? ( +
handleRemoveCard('overview')} + > + {t('remove')} + + ) + } renderFromShadcn > - +
+ +
- )} + ) : null} -
- -
+ {!isCardRemoved('activity') ? ( +
+ handleRemoveCard('activity')} + > + {t('remove')} + + ) + } + /> +
+ ) : null} -
- -
+ {!isCardRemoved('recent_payments') ? ( +
+ handleRemoveCard('recent_payments')} + > + {t('remove')} + + ) + } + /> +
+ ) : null} - {enabled(ModuleBitmask.Invoices) && ( + {enabled(ModuleBitmask.Invoices) && + !isCardRemoved('upcoming_invoices') ? (
- + handleRemoveCard('upcoming_invoices')} + > + {t('remove')} + + ) + } + />
- )} + ) : null} - {enabled(ModuleBitmask.Invoices) && ( + {enabled(ModuleBitmask.Invoices) && + !isCardRemoved('past_due_invoices') ? (
- + handleRemoveCard('past_due_invoices')} + > + {t('remove')} + + ) + } + />
- )} + ) : null} - {enabled(ModuleBitmask.Quotes) && ( + {enabled(ModuleBitmask.Quotes) && !isCardRemoved('expired_quotes') ? (
- + handleRemoveCard('expired_quotes')} + > + {t('remove')} + + ) + } + />
- )} + ) : null} - {enabled(ModuleBitmask.Quotes) && ( + {enabled(ModuleBitmask.Quotes) && + !isCardRemoved('upcoming_quotes') ? (
- + handleRemoveCard('upcoming_quotes')} + > + {t('remove')} + + ) + } + />
- )} + ) : null} - {enabled(ModuleBitmask.RecurringInvoices) && ( + {enabled(ModuleBitmask.RecurringInvoices) && + !isCardRemoved('upcoming_recurring_invoices') ? (
- + + handleRemoveCard('upcoming_recurring_invoices') + } + > + {t('remove')} + + ) + } + />
- )} + ) : null} ) : (
diff --git a/src/pages/dashboard/components/RestoreCardsModal.tsx b/src/pages/dashboard/components/RestoreCardsModal.tsx index 65457843df..f5450f0973 100644 --- a/src/pages/dashboard/components/RestoreCardsModal.tsx +++ b/src/pages/dashboard/components/RestoreCardsModal.tsx @@ -20,7 +20,7 @@ import { Modal } from '$app/components/Modal'; import { cloneDeep } from 'lodash'; import { Dispatch, SetStateAction, useEffect, useState } from 'react'; import { useTranslation } from 'react-i18next'; -import { MdClose, MdRestorePage } from 'react-icons/md'; +import { MdRefresh, MdRestorePage } from 'react-icons/md'; import styled from 'styled-components'; import { request } from '$app/common/helpers/request'; import { endpoint } from '$app/common/helpers'; @@ -42,12 +42,13 @@ const StyledDiv = styled.div` interface Props { layoutBreakpoint: string | undefined; setLayouts: Dispatch>; + setAreCardsRestored: Dispatch>; } export function RestoreCardsModal(props: Props) { const [t] = useTranslation(); - const { layoutBreakpoint } = props; + const { layoutBreakpoint, setAreCardsRestored } = props; const dispatch = useDispatch(); @@ -83,24 +84,26 @@ export function RestoreCardsModal(props: Props) { updatedUser ) .then((response: GenericSingleResourceResponse) => { - set(updatedUser, 'company_user', response.data.data); + setAreCardsRestored(true); - toast.success('restored'); + setTimeout(() => { + set(updatedUser, 'company_user', response.data.data); - $refetch(['company_users']); + toast.success('restored'); - dispatch(updateUser(updatedUser)); + $refetch(['company_users']); - props.setLayouts( - cloneDeep( - response.data.data.react_settings - .dashboard_cards_configuration as DashboardGridLayouts - ) - ); + dispatch(updateUser(updatedUser)); + + props.setLayouts( + cloneDeep( + response.data.data.react_settings + .dashboard_cards_configuration as DashboardGridLayouts + ) + ); - setTimeout(() => { handleOnClose(); - }, 100); + }, 50); }) .finally(() => setIsFormBusy(false)); } @@ -145,7 +148,7 @@ export function RestoreCardsModal(props: Props) { {t(card)}
- +
))} diff --git a/src/pages/dashboard/components/RestoreLayoutAction.tsx b/src/pages/dashboard/components/RestoreLayoutAction.tsx new file mode 100644 index 0000000000..35ae665c33 --- /dev/null +++ b/src/pages/dashboard/components/RestoreLayoutAction.tsx @@ -0,0 +1,65 @@ +/** + * Invoice Ninja (https://invoiceninja.com). + * + * @link https://github.com/invoiceninja/invoiceninja source repository + * + * @copyright Copyright (c) 2022. Invoice Ninja LLC (https://invoiceninja.com) + * + * @license https://www.elastic.co/licensing/elastic-license + */ + +import { Dispatch } from 'react'; +import { initialLayouts } from './ResizableDashboardCards'; +import { MdRefresh } from 'react-icons/md'; +import { DashboardGridLayouts } from './ResizableDashboardCards'; +import { SetStateAction } from 'react'; +import { Icon } from '$app/components/icons/Icon'; +import { + ConfirmActionModal, + confirmActionModalAtom, +} from '$app/pages/recurring-invoices/common/components/ConfirmActionModal'; +import { useSetAtom } from 'jotai'; + +interface Props { + layoutBreakpoint: string | undefined; + updateLayoutHeight: () => void; + setLayouts: Dispatch>; +} + +export function RestoreLayoutAction(props: Props) { + const { layoutBreakpoint, updateLayoutHeight, setLayouts } = props; + + const setIsModalVisible = useSetAtom(confirmActionModalAtom); + + if (!layoutBreakpoint) { + return null; + } + + return ( + <> + { + layoutBreakpoint && + setLayouts((currentLayouts) => ({ + ...currentLayouts, + [layoutBreakpoint]: + initialLayouts[layoutBreakpoint as keyof typeof initialLayouts], + })); + + setIsModalVisible(false); + + setTimeout(() => { + updateLayoutHeight(); + }, 100); + }} + /> + +
setIsModalVisible(true)} + > + +
+ + ); +} diff --git a/src/pages/dashboard/components/UpcomingInvoices.tsx b/src/pages/dashboard/components/UpcomingInvoices.tsx index 042a8389b2..48d8929adb 100644 --- a/src/pages/dashboard/components/UpcomingInvoices.tsx +++ b/src/pages/dashboard/components/UpcomingInvoices.tsx @@ -19,8 +19,13 @@ import { useDisableNavigation } from '$app/common/hooks/useDisableNavigation'; import { useCurrentCompanyDateFormats } from '$app/common/hooks/useCurrentCompanyDateFormats'; import { DynamicLink } from '$app/components/DynamicLink'; import { useTranslation } from 'react-i18next'; +import { ReactNode } from 'react'; -export function UpcomingInvoices() { +interface Props { + topRight?: ReactNode; +} + +export function UpcomingInvoices({ topRight }: Props) { const [t] = useTranslation(); const formatMoney = useFormatMoney(); @@ -86,6 +91,7 @@ export function UpcomingInvoices() { title={t('upcoming_invoices')} className="h-full relative" withoutBodyPadding + topRight={topRight} renderFromShadcn >
Date: Wed, 25 Dec 2024 13:27:48 +0100 Subject: [PATCH 25/36] Adjusted heights of cards when edit mode is turned on --- src/pages/dashboard/components/Activity.tsx | 3 +- .../dashboard/components/ExpiredQuotes.tsx | 15 +- .../dashboard/components/PastDueInvoices.tsx | 15 +- .../dashboard/components/RecentPayments.tsx | 11 +- .../dashboard/components/ResizableContent.tsx | 163 ------------------ .../components/ResizableDashboardCards.tsx | 48 ++---- .../dashboard/components/UpcomingInvoices.tsx | 15 +- .../dashboard/components/UpcomingQuotes.tsx | 15 +- .../components/UpcomingRecurringInvoices.tsx | 23 ++- 9 files changed, 87 insertions(+), 221 deletions(-) delete mode 100644 src/pages/dashboard/components/ResizableContent.tsx diff --git a/src/pages/dashboard/components/Activity.tsx b/src/pages/dashboard/components/Activity.tsx index 10b771de48..e7f3947a16 100644 --- a/src/pages/dashboard/components/Activity.tsx +++ b/src/pages/dashboard/components/Activity.tsx @@ -38,7 +38,8 @@ export function Activity({ topRight, isEditMode }: Props) { return (
{ - const enabled = useEnabled(); - - const [width, setWidth] = useState(1000); - const containerRef = useRef(null); - - useEffect(() => { - const resizeObserver = new ResizeObserver((entries) => { - if (entries[0]) { - setWidth(entries[0].contentRect.width); - } - }); - - if (containerRef.current) { - resizeObserver.observe(containerRef.current); - } - - return () => { - if (containerRef.current) { - resizeObserver.unobserve(containerRef.current); - } - }; - }, []); - - return ( -
- -
- -
- -
- -
- - {enabled(ModuleBitmask.Invoices) && ( -
- -
- )} - - {enabled(ModuleBitmask.Invoices) && ( -
- -
- )} - - {enabled(ModuleBitmask.Quotes) && ( -
- -
- )} - - {enabled(ModuleBitmask.Quotes) && ( -
- -
- )} - - {enabled(ModuleBitmask.RecurringInvoices) && ( -
- -
- )} -
-
- ); -}; - -export default GridLayoutComponent; diff --git a/src/pages/dashboard/components/ResizableDashboardCards.tsx b/src/pages/dashboard/components/ResizableDashboardCards.tsx index 4116331955..69bed0abab 100644 --- a/src/pages/dashboard/components/ResizableDashboardCards.tsx +++ b/src/pages/dashboard/components/ResizableDashboardCards.tsx @@ -1756,13 +1756,9 @@ export function ResizableDashboardCards() { ) : null} {!isCardRemoved('recent_payments') ? ( -
+
+
+
+
+
+
Date: Wed, 25 Dec 2024 16:22:54 +0100 Subject: [PATCH 26/36] Cleanup --- src/components/cards/Card.tsx | 8 +- src/pages/dashboard/components/Activity.tsx | 9 +- .../dashboard/components/ExpiredQuotes.tsx | 8 +- .../dashboard/components/PastDueInvoices.tsx | 8 +- .../dashboard/components/RecentPayments.tsx | 6 +- .../components/ResizableDashboardCards.tsx | 115 ++++++++++++------ .../dashboard/components/UpcomingInvoices.tsx | 8 +- .../dashboard/components/UpcomingQuotes.tsx | 8 +- .../components/UpcomingRecurringInvoices.tsx | 8 +- 9 files changed, 87 insertions(+), 91 deletions(-) diff --git a/src/components/cards/Card.tsx b/src/components/cards/Card.tsx index eb395c56ad..69d0a45f55 100644 --- a/src/components/cards/Card.tsx +++ b/src/components/cards/Card.tsx @@ -64,7 +64,6 @@ interface Props { topRight?: ReactNode; height?: 'full'; renderFromShadcn?: boolean; - titleDescriptionParentClassName?: string; } export function Card(props: Props) { @@ -91,12 +90,7 @@ export function Card(props: Props) { {Boolean(props.title || props.description) && (
-
+
{props.title && {props.title}} {props.description && ( diff --git a/src/pages/dashboard/components/Activity.tsx b/src/pages/dashboard/components/Activity.tsx index e7f3947a16..70be79c6ae 100644 --- a/src/pages/dashboard/components/Activity.tsx +++ b/src/pages/dashboard/components/Activity.tsx @@ -8,7 +8,7 @@ * @license https://www.elastic.co/licensing/elastic-license */ -import { classNames, endpoint } from '$app/common/helpers'; +import { endpoint } from '$app/common/helpers'; import { Spinner } from '$app/components/Spinner'; import { request } from '$app/common/helpers/request'; import { useQuery } from 'react-query'; @@ -39,9 +39,6 @@ export function Activity({ topRight, isEditMode }: Props) {
diff --git a/src/pages/dashboard/components/ExpiredQuotes.tsx b/src/pages/dashboard/components/ExpiredQuotes.tsx index 59ea3d847b..c8eea9848d 100644 --- a/src/pages/dashboard/components/ExpiredQuotes.tsx +++ b/src/pages/dashboard/components/ExpiredQuotes.tsx @@ -19,7 +19,6 @@ import { Badge } from '$app/components/Badge'; import { useDisableNavigation } from '$app/common/hooks/useDisableNavigation'; import { DynamicLink } from '$app/components/DynamicLink'; import { ReactNode } from 'react'; -import classNames from 'classnames'; interface Props { topRight?: ReactNode; @@ -83,17 +82,12 @@ export function ExpiredQuotes({ topRight, isEditMode }: Props) { className="relative" withoutBodyPadding withoutHeaderBorder - titleDescriptionParentClassName={classNames('drag-handle', { - 'cursor-grab': isEditMode, - })} height="full" topRight={topRight} renderFromShadcn >
{!totals.isLoading ? ( {(totals.isLoading || !isLayoutsInitialized) && (
@@ -1545,17 +1547,20 @@ export function ResizableDashboardCards() { ) : null} {company && !isCardRemoved('account_login_text') ? ( -
+
handleRemoveCard('account_login_text')} @@ -1564,13 +1569,8 @@ export function ResizableDashboardCards() { ) } - renderFromShadcn > -
+
{`${user?.first_name} ${user?.last_name}`} @@ -1695,18 +1695,21 @@ export function ResizableDashboardCards() { ) : null} {chartData && !isCardRemoved('overview') ? ( -
+
handleRemoveCard('overview')} @@ -1717,32 +1720,32 @@ export function ResizableDashboardCards() { } renderFromShadcn > -
- -
+
) : null} {!isCardRemoved('activity') ? ( -
+
handleRemoveCard('activity')} @@ -1756,12 +1759,18 @@ export function ResizableDashboardCards() { ) : null} {!isCardRemoved('recent_payments') ? ( -
+
handleRemoveCard('recent_payments')} @@ -1776,12 +1785,18 @@ export function ResizableDashboardCards() { {enabled(ModuleBitmask.Invoices) && !isCardRemoved('upcoming_invoices') ? ( -
+
handleRemoveCard('upcoming_invoices')} @@ -1796,12 +1811,18 @@ export function ResizableDashboardCards() { {enabled(ModuleBitmask.Invoices) && !isCardRemoved('past_due_invoices') ? ( -
+
handleRemoveCard('past_due_invoices')} @@ -1815,12 +1836,18 @@ export function ResizableDashboardCards() { ) : null} {enabled(ModuleBitmask.Quotes) && !isCardRemoved('expired_quotes') ? ( -
+
handleRemoveCard('expired_quotes')} @@ -1835,12 +1862,18 @@ export function ResizableDashboardCards() { {enabled(ModuleBitmask.Quotes) && !isCardRemoved('upcoming_quotes') ? ( -
+
handleRemoveCard('upcoming_quotes')} @@ -1855,12 +1888,18 @@ export function ResizableDashboardCards() { {enabled(ModuleBitmask.RecurringInvoices) && !isCardRemoved('upcoming_recurring_invoices') ? ( -
+
diff --git a/src/pages/dashboard/components/UpcomingInvoices.tsx b/src/pages/dashboard/components/UpcomingInvoices.tsx index bdc79ea843..3366f171b7 100644 --- a/src/pages/dashboard/components/UpcomingInvoices.tsx +++ b/src/pages/dashboard/components/UpcomingInvoices.tsx @@ -20,7 +20,6 @@ import { useCurrentCompanyDateFormats } from '$app/common/hooks/useCurrentCompan import { DynamicLink } from '$app/components/DynamicLink'; import { useTranslation } from 'react-i18next'; import { ReactNode } from 'react'; -import classNames from 'classnames'; interface Props { topRight?: ReactNode; @@ -92,17 +91,12 @@ export function UpcomingInvoices({ topRight, isEditMode }: Props) {
Date: Sun, 29 Dec 2024 14:40:57 +0100 Subject: [PATCH 27/36] Implemented preferences with new logic --- src/common/hooks/useReactSettings.ts | 2 + src/common/interfaces/company-user.ts | 1 + .../dashboard/components/DashboardCard.tsx | 39 ++-- .../components/DashboardCardSelector.tsx | 200 ++++++------------ .../components/ResizableDashboardCards.tsx | 142 +++++++------ .../components/RestoreLayoutAction.tsx | 15 +- 6 files changed, 187 insertions(+), 212 deletions(-) diff --git a/src/common/hooks/useReactSettings.ts b/src/common/hooks/useReactSettings.ts index b7da0c1382..cd61baaac4 100644 --- a/src/common/hooks/useReactSettings.ts +++ b/src/common/hooks/useReactSettings.ts @@ -17,6 +17,7 @@ import { Entity } from '$app/components/CommonActionsPreferenceModal'; import { PerPage } from '$app/components/DataTable'; import { ThemeColorField } from '$app/pages/settings/user/components/StatusColorTheme'; import { DashboardGridLayouts } from '$app/pages/dashboard/components/ResizableDashboardCards'; +import { DashboardField } from '../interfaces/company-user'; export type ChartsDefaultView = 'day' | 'week' | 'month'; @@ -92,6 +93,7 @@ export interface ReactSettings { color_theme?: ColorTheme; dashboard_cards_configuration?: DashboardGridLayouts; removed_dashboard_cards?: Record; + dashboard_fields?: DashboardField[]; } export type ReactTableColumns = diff --git a/src/common/interfaces/company-user.ts b/src/common/interfaces/company-user.ts index a5d3a4f6f7..f8d94e184c 100644 --- a/src/common/interfaces/company-user.ts +++ b/src/common/interfaces/company-user.ts @@ -51,6 +51,7 @@ export type Field = | 'invoice_paid_expenses'; export interface DashboardField { + id: string; calculate: Calculate; field: Field; format: Format; diff --git a/src/pages/dashboard/components/DashboardCard.tsx b/src/pages/dashboard/components/DashboardCard.tsx index 3ca1333cb9..6d41afe691 100644 --- a/src/pages/dashboard/components/DashboardCard.tsx +++ b/src/pages/dashboard/components/DashboardCard.tsx @@ -10,7 +10,7 @@ import { useFormatMoney } from '$app/common/hooks/money/useFormatMoney'; import { DashboardField } from '$app/common/interfaces/company-user'; -import { useEffect, useState } from 'react'; +import { useEffect, useRef, useState } from 'react'; import { useTranslation } from 'react-i18next'; import { FIELDS_LABELS } from './DashboardCardSelector'; import { useQueryClient } from 'react-query'; @@ -18,7 +18,6 @@ import { request } from '$app/common/helpers/request'; import { endpoint } from '$app/common/helpers'; import { Spinner } from '$app/components/Spinner'; import { Card as ShadcnCard } from '../../../../components/ui/card'; -import classNames from 'classnames'; interface DashboardCardsProps { dateRange: string; @@ -45,9 +44,26 @@ export function DashboardCard(props: CardProps) { const queryClient = useQueryClient(); const formatMoney = useFormatMoney(); + const containerRef = useRef(null); + const [isFormBusy, setIsFormBusy] = useState(false); const [responseData, setResponseData] = useState(); + useEffect(() => { + if (!containerRef.current || isFormBusy) return; + + const observer = new ResizeObserver((entries) => { + const container = entries[0].target as HTMLDivElement; + const { width, height } = entries[0].contentRect; + + const fontSize = (width + height) / 18; + container.style.fontSize = `${fontSize}px`; + }); + + observer.observe(containerRef.current); + return () => observer.disconnect(); + }, [isFormBusy]); + useEffect(() => { (async () => { typeof responseData === 'undefined' && setIsFormBusy(true); @@ -83,19 +99,7 @@ export function DashboardCard(props: CardProps) { }, [field]); return ( - + {isFormBusy && (
@@ -103,7 +107,10 @@ export function DashboardCard(props: CardProps) { )} {!isFormBusy && ( -
+
{t(FIELDS_LABELS[field.field])} diff --git a/src/pages/dashboard/components/DashboardCardSelector.tsx b/src/pages/dashboard/components/DashboardCardSelector.tsx index c81793ed2d..b724e19d86 100644 --- a/src/pages/dashboard/components/DashboardCardSelector.tsx +++ b/src/pages/dashboard/components/DashboardCardSelector.tsx @@ -26,21 +26,16 @@ import { User } from '$app/common/interfaces/user'; import { Button, SelectField } from '$app/components/forms'; import { Icon } from '$app/components/icons/Icon'; import { Modal } from '$app/components/Modal'; -import { - DragDropContext, - Draggable, - Droppable, - DropResult, -} from '@hello-pangea/dnd'; -import { arrayMoveImmutable } from 'array-move'; -import { cloneDeep, isEqual, set } from 'lodash'; -import { useEffect, useState } from 'react'; +import { cloneDeep, set } from 'lodash'; +import { Dispatch, SetStateAction, useEffect, useState } from 'react'; import { useTranslation } from 'react-i18next'; import { CgOptions } from 'react-icons/cg'; -import { MdClose, MdDragHandle } from 'react-icons/md'; +import { MdClose } from 'react-icons/md'; import { useDispatch } from 'react-redux'; import { updateUser } from '$app/common/stores/slices/user'; import { PERIOD_LABELS } from './DashboardCard'; +import { v4 } from 'uuid'; +import { DashboardGridLayouts } from './ResizableDashboardCards'; const FIELDS = [ 'active_invoices', @@ -74,7 +69,11 @@ export const FIELDS_LABELS = { invoice_paid_expenses: 'total_invoice_paid_expenses', }; -export function DashboardCardSelector() { +interface Props { + setLayouts: Dispatch>; +} + +export function DashboardCardSelector({ setLayouts }: Props) { const [t] = useTranslation(); const dispatch = useDispatch(); @@ -82,6 +81,7 @@ export function DashboardCardSelector() { const [currentFields, setCurrentFields] = useState([]); const [currentField, setCurrentField] = useState({ + id: v4(), field: '' as Field, period: 'current', calculate: 'sum', @@ -100,6 +100,7 @@ export function DashboardCardSelector() { setIsFieldsModalOpen(false); setCurrentField({ + id: v4(), field: '' as Field, period: 'current', calculate: 'sum', @@ -107,22 +108,10 @@ export function DashboardCardSelector() { }); }; - const onDragEnd = (result: DropResult) => { - const sorted = arrayMoveImmutable( - currentFields, - result.source.index, - result.destination?.index as unknown as number + const handleDelete = (fieldKey: string) => { + setCurrentFields((currentFields) => + currentFields.filter((field) => field.id !== fieldKey) ); - - setCurrentFields(sorted); - }; - - const handleDelete = (field: DashboardField) => { - const updatedCurrentColumns = currentFields.filter( - (currentField) => !isEqual(currentField, field) - ); - - setCurrentFields(updatedCurrentColumns); }; const handleSaveCards = () => { @@ -132,7 +121,11 @@ export function DashboardCardSelector() { toast.processing(); setIsFormBusy(true); - set(updatedUser, 'company_user.settings.dashboard_fields', currentFields); + set( + updatedUser, + 'company_user.react_settings.dashboard_fields', + cloneDeep(currentFields) + ); request( 'PUT', @@ -148,6 +141,20 @@ export function DashboardCardSelector() { dispatch(updateUser(updatedUser)); + setLayouts((current) => { + const updatedLayouts = cloneDeep(current); + + Object.keys(updatedLayouts).forEach((screenSize) => { + updatedLayouts[screenSize] = updatedLayouts[screenSize].filter( + (layout) => + layout.i.length <= 5 || + currentFields.some((field) => field.id === layout.i) + ); + }); + + return updatedLayouts; + }); + handleCardsModalClose(); }) .finally(() => setIsFormBusy(false)); @@ -157,7 +164,8 @@ export function DashboardCardSelector() { useEffect(() => { if (currentUser && Object.keys(currentUser).length && isCardsModalOpen) { setCurrentFields( - currentUser.company_user?.settings.dashboard_fields ?? [] + cloneDeep(currentUser.company_user?.react_settings?.dashboard_fields) ?? + [] ); } }, [currentUser, isCardsModalOpen]); @@ -184,109 +192,38 @@ export function DashboardCardSelector() { )} - - { - const dashboardField = currentFields[rubric.source.index]; - - return ( -
-
- - -
-

{t(FIELDS_LABELS[dashboardField.field])}

- -
- - {t( - PERIOD_LABELS[ - dashboardField.period as keyof typeof PERIOD_LABELS - ] ?? dashboardField.period - )} - - · - - {t( - dashboardField.calculate === 'avg' - ? 'average' - : dashboardField.calculate - )} - -
-
-
- -
- -
-
- ); - }} - > - {(provided) => ( -
- {currentFields.map((field, index) => ( - - {(provided) => ( -
-
- handleDelete(field)} - /> - -
-

{t(FIELDS_LABELS[field.field])}

- -
- - {t( - PERIOD_LABELS[ - field.period as keyof typeof PERIOD_LABELS - ] ?? field.period - )} - - · - - {t( - field.calculate === 'avg' - ? 'average' - : field.calculate - )} - -
-
-
- -
- -
-
+
+ {currentFields.map((field, index) => ( +
+ handleDelete(field.id)} + /> + +
+

{t(FIELDS_LABELS[field.field])}

+ +
+ + {t( + PERIOD_LABELS[ + field.period as keyof typeof PERIOD_LABELS + ] ?? field.period )} - - ))} - - {provided.placeholder} + + · + + {t( + field.calculate === 'avg' ? 'average' : field.calculate + )} + +
- )} - - +
+ ))} +
- {user?.company_user?.settings.dashboard_fields?.length ? ( + {currentDashboardFields.map((field) => (
- {user?.company_user?.settings.dashboard_fields?.map( - (field, index) => ( - - ) - )} +
- ) : null} + ))} {company && !isCardRemoved('account_login_text') ? (
void; setLayouts: Dispatch>; } export function RestoreLayoutAction(props: Props) { - const { layoutBreakpoint, updateLayoutHeight, setLayouts } = props; + const { layoutBreakpoint, setLayouts } = props; const setIsModalVisible = useSetAtom(confirmActionModalAtom); + const [isRestoring, setIsRestoring] = useState(false); + if (!layoutBreakpoint) { return null; } @@ -46,12 +47,14 @@ export function RestoreLayoutAction(props: Props) { initialLayouts[layoutBreakpoint as keyof typeof initialLayouts], })); - setIsModalVisible(false); + setIsRestoring(true); setTimeout(() => { - updateLayoutHeight(); - }, 100); + setIsRestoring(false); + setIsModalVisible(false); + }, 2500); }} + disabledButton={isRestoring} />
Date: Sun, 29 Dec 2024 21:52:47 +0100 Subject: [PATCH 28/36] Fixed issue with dragging --- .../components/DashboardCardSelector.tsx | 46 ++++--- .../components/ResizableDashboardCards.tsx | 128 +++++++++++++++--- .../components/RestoreLayoutAction.tsx | 14 +- 3 files changed, 151 insertions(+), 37 deletions(-) diff --git a/src/pages/dashboard/components/DashboardCardSelector.tsx b/src/pages/dashboard/components/DashboardCardSelector.tsx index b724e19d86..fcc548e36d 100644 --- a/src/pages/dashboard/components/DashboardCardSelector.tsx +++ b/src/pages/dashboard/components/DashboardCardSelector.tsx @@ -79,7 +79,9 @@ export function DashboardCardSelector({ setLayouts }: Props) { const currentUser = useCurrentUser(); - const [currentFields, setCurrentFields] = useState([]); + const [currentFields, setCurrentFields] = useState< + DashboardField[] | undefined + >(); const [currentField, setCurrentField] = useState({ id: v4(), field: '' as Field, @@ -110,14 +112,14 @@ export function DashboardCardSelector({ setLayouts }: Props) { const handleDelete = (fieldKey: string) => { setCurrentFields((currentFields) => - currentFields.filter((field) => field.id !== fieldKey) + currentFields?.filter((field) => field.id !== fieldKey) ); }; const handleSaveCards = () => { const updatedUser = cloneDeep(currentUser) as User; - if (updatedUser && !isFormBusy) { + if (updatedUser && !isFormBusy && currentFields) { toast.processing(); setIsFormBusy(true); @@ -148,7 +150,7 @@ export function DashboardCardSelector({ setLayouts }: Props) { updatedLayouts[screenSize] = updatedLayouts[screenSize].filter( (layout) => layout.i.length <= 5 || - currentFields.some((field) => field.id === layout.i) + currentFields?.some((field) => field.id === layout.i) ); }); @@ -162,7 +164,12 @@ export function DashboardCardSelector({ setLayouts }: Props) { }; useEffect(() => { - if (currentUser && Object.keys(currentUser).length && isCardsModalOpen) { + if ( + currentUser && + Object.keys(currentUser).length && + isCardsModalOpen && + !currentFields + ) { setCurrentFields( cloneDeep(currentUser.company_user?.react_settings?.dashboard_fields) ?? [] @@ -186,21 +193,22 @@ export function DashboardCardSelector({ setLayouts }: Props) { disableClosing={isFieldsModalOpen} >
- {!currentFields.length && ( + {!currentFields?.length && ( {t('no_records_found')} )}
- {currentFields.map((field, index) => ( + {currentFields?.map((field, index) => (
- handleDelete(field.id)} - /> +
handleDelete(field.id)}> + +

{t(FIELDS_LABELS[field.field])}

@@ -318,10 +326,14 @@ export function DashboardCardSelector({ setLayouts }: Props) {
- {currentDashboardFields.map((field) => ( + {currentDashboardFields?.length ? (
- + {currentDashboardFields.map((field, index) => ( + + ))}
- ))} + ) : null} {company && !isCardRemoved('account_login_text') ? (
>; - updateLayoutHeight: (isRestoring: boolean) => void; + updateLayoutHeight: () => void; } export function RestoreLayoutAction(props: Props) { @@ -35,8 +34,6 @@ export function RestoreLayoutAction(props: Props) { const [isRestoring, setIsRestoring] = useState(false); - const settings = useReactSettings(); - if (!layoutBreakpoint) { return null; } @@ -58,15 +55,12 @@ export function RestoreLayoutAction(props: Props) { setIsRestoring(true); - updateLayoutHeight(true); + updateLayoutHeight(); - setTimeout( - () => { - setIsRestoring(false); - setIsModalVisible(false); - }, - settings?.dashboard_fields?.length ? 2500 : 300 - ); + setTimeout(() => { + setIsRestoring(false); + setIsModalVisible(false); + }, 300); }} disabledButton={isRestoring} /> From b8de71de3fcc659b4113892259d1f1f4ca6c73e3 Mon Sep 17 00:00:00 2001 From: Civolilah Date: Mon, 30 Dec 2024 19:55:58 +0100 Subject: [PATCH 31/36] Cleanup --- src/pages/dashboard/components/ResizableDashboardCards.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/pages/dashboard/components/ResizableDashboardCards.tsx b/src/pages/dashboard/components/ResizableDashboardCards.tsx index 4c736b3d12..b8df015b1d 100644 --- a/src/pages/dashboard/components/ResizableDashboardCards.tsx +++ b/src/pages/dashboard/components/ResizableDashboardCards.tsx @@ -1199,7 +1199,7 @@ export function ResizableDashboardCards() { cloneDeep(layouts) ); - // delete updatedUser.company_user?.react_settings?.dashboard_fields; + // delete updatedUser.company_user?.settings?.dashboard_fields; // delete updatedUser.company_user.react_settings // .dashboard_cards_configuration; From e70a4fc43a05530ea0cf11daa4b5dd070a55f04b Mon Sep 17 00:00:00 2001 From: Civolilah Date: Wed, 8 Jan 2025 08:05:21 +0100 Subject: [PATCH 32/36] Changing approach for a solution for preference cards --- src/common/hooks/useReactSettings.ts | 2 + src/common/interfaces/company-user.ts | 1 + .../components/DashboardCardSelector.tsx | 176 +++++----------- .../components/PreferenceCardsGrid.tsx | 195 ++++++++++++++++++ .../components/ResizableDashboardCards.tsx | 39 ++-- 5 files changed, 262 insertions(+), 151 deletions(-) create mode 100644 src/pages/dashboard/components/PreferenceCardsGrid.tsx diff --git a/src/common/hooks/useReactSettings.ts b/src/common/hooks/useReactSettings.ts index b7da0c1382..cd61baaac4 100644 --- a/src/common/hooks/useReactSettings.ts +++ b/src/common/hooks/useReactSettings.ts @@ -17,6 +17,7 @@ import { Entity } from '$app/components/CommonActionsPreferenceModal'; import { PerPage } from '$app/components/DataTable'; import { ThemeColorField } from '$app/pages/settings/user/components/StatusColorTheme'; import { DashboardGridLayouts } from '$app/pages/dashboard/components/ResizableDashboardCards'; +import { DashboardField } from '../interfaces/company-user'; export type ChartsDefaultView = 'day' | 'week' | 'month'; @@ -92,6 +93,7 @@ export interface ReactSettings { color_theme?: ColorTheme; dashboard_cards_configuration?: DashboardGridLayouts; removed_dashboard_cards?: Record; + dashboard_fields?: DashboardField[]; } export type ReactTableColumns = diff --git a/src/common/interfaces/company-user.ts b/src/common/interfaces/company-user.ts index a5d3a4f6f7..f8d94e184c 100644 --- a/src/common/interfaces/company-user.ts +++ b/src/common/interfaces/company-user.ts @@ -51,6 +51,7 @@ export type Field = | 'invoice_paid_expenses'; export interface DashboardField { + id: string; calculate: Calculate; field: Field; format: Format; diff --git a/src/pages/dashboard/components/DashboardCardSelector.tsx b/src/pages/dashboard/components/DashboardCardSelector.tsx index c81793ed2d..18eb257aba 100644 --- a/src/pages/dashboard/components/DashboardCardSelector.tsx +++ b/src/pages/dashboard/components/DashboardCardSelector.tsx @@ -26,21 +26,15 @@ import { User } from '$app/common/interfaces/user'; import { Button, SelectField } from '$app/components/forms'; import { Icon } from '$app/components/icons/Icon'; import { Modal } from '$app/components/Modal'; -import { - DragDropContext, - Draggable, - Droppable, - DropResult, -} from '@hello-pangea/dnd'; -import { arrayMoveImmutable } from 'array-move'; -import { cloneDeep, isEqual, set } from 'lodash'; +import { cloneDeep, set } from 'lodash'; import { useEffect, useState } from 'react'; import { useTranslation } from 'react-i18next'; import { CgOptions } from 'react-icons/cg'; -import { MdClose, MdDragHandle } from 'react-icons/md'; +import { MdClose } from 'react-icons/md'; import { useDispatch } from 'react-redux'; import { updateUser } from '$app/common/stores/slices/user'; import { PERIOD_LABELS } from './DashboardCard'; +import { v4 } from 'uuid'; const FIELDS = [ 'active_invoices', @@ -82,6 +76,7 @@ export function DashboardCardSelector() { const [currentFields, setCurrentFields] = useState([]); const [currentField, setCurrentField] = useState({ + id: v4(), field: '' as Field, period: 'current', calculate: 'sum', @@ -100,6 +95,7 @@ export function DashboardCardSelector() { setIsFieldsModalOpen(false); setCurrentField({ + id: v4(), field: '' as Field, period: 'current', calculate: 'sum', @@ -107,22 +103,10 @@ export function DashboardCardSelector() { }); }; - const onDragEnd = (result: DropResult) => { - const sorted = arrayMoveImmutable( - currentFields, - result.source.index, - result.destination?.index as unknown as number + const handleDelete = (fieldKey: string) => { + setCurrentFields((currentFields) => + currentFields.filter((field) => field.id !== fieldKey) ); - - setCurrentFields(sorted); - }; - - const handleDelete = (field: DashboardField) => { - const updatedCurrentColumns = currentFields.filter( - (currentField) => !isEqual(currentField, field) - ); - - setCurrentFields(updatedCurrentColumns); }; const handleSaveCards = () => { @@ -132,7 +116,11 @@ export function DashboardCardSelector() { toast.processing(); setIsFormBusy(true); - set(updatedUser, 'company_user.settings.dashboard_fields', currentFields); + set( + updatedUser, + 'company_user.react_settings.dashboard_fields', + currentFields + ); request( 'PUT', @@ -157,7 +145,7 @@ export function DashboardCardSelector() { useEffect(() => { if (currentUser && Object.keys(currentUser).length && isCardsModalOpen) { setCurrentFields( - currentUser.company_user?.settings.dashboard_fields ?? [] + currentUser.company_user?.react_settings.dashboard_fields ?? [] ); } }, [currentUser, isCardsModalOpen]); @@ -184,109 +172,38 @@ export function DashboardCardSelector() { )} - - { - const dashboardField = currentFields[rubric.source.index]; - - return ( -
-
- - -
-

{t(FIELDS_LABELS[dashboardField.field])}

- -
- - {t( - PERIOD_LABELS[ - dashboardField.period as keyof typeof PERIOD_LABELS - ] ?? dashboardField.period - )} - - · - - {t( - dashboardField.calculate === 'avg' - ? 'average' - : dashboardField.calculate - )} - -
-
-
- -
- -
-
- ); - }} - > - {(provided) => ( -
- {currentFields.map((field, index) => ( - - {(provided) => ( -
-
- handleDelete(field)} - /> - -
-

{t(FIELDS_LABELS[field.field])}

- -
- - {t( - PERIOD_LABELS[ - field.period as keyof typeof PERIOD_LABELS - ] ?? field.period - )} - - · - - {t( - field.calculate === 'avg' - ? 'average' - : field.calculate - )} - -
-
-
- -
- -
-
+
+ {currentFields.map((field) => ( +
+ handleDelete(field.id)} + /> + +
+

{t(FIELDS_LABELS[field.field])}

+ +
+ + {t( + PERIOD_LABELS[ + field.period as keyof typeof PERIOD_LABELS + ] ?? field.period )} - - ))} - - {provided.placeholder} + + · + + {t( + field.calculate === 'avg' ? 'average' : field.calculate + )} + +
- )} - - +
+ ))} +