From 18885d0289931ac833b4d65c03098405c322b7a7 Mon Sep 17 00:00:00 2001 From: Tom Antas Date: Tue, 13 Aug 2024 14:14:26 +0200 Subject: [PATCH 1/9] feat: adjust usage of uncategorized in profit and loss --- .../ProfitAndLossChart/ProfitAndLossChart.tsx | 25 +++++++++ .../DetailedChart.tsx | 9 ++- .../DetailedTable.tsx | 4 +- .../ProfitAndLossSummaries.tsx | 44 +++++++++++---- .../useProfitAndLoss/useProfitAndLoss.tsx | 55 +++++++++++++++---- src/styles/profit_and_loss.scss | 14 +++++ src/utils/profitAndLossUtils.ts | 8 +++ 7 files changed, 132 insertions(+), 27 deletions(-) diff --git a/src/components/ProfitAndLossChart/ProfitAndLossChart.tsx b/src/components/ProfitAndLossChart/ProfitAndLossChart.tsx index 56c9a1733..37b9e8951 100644 --- a/src/components/ProfitAndLossChart/ProfitAndLossChart.tsx +++ b/src/components/ProfitAndLossChart/ProfitAndLossChart.tsx @@ -378,6 +378,12 @@ export const ProfitAndLossChart = ({ netProfit > 0 ? 'positive' : netProfit < 0 ? 'negative' : '' const revenue = payload.find(x => x.dataKey === 'revenue')?.value ?? 0 const expenses = payload.find(x => x.dataKey === 'expenses')?.value ?? 0 + const revenueUncategorized = + payload.find(x => x.dataKey === 'revenueUncategorized')?.value ?? 0 + const expensesUncategorized = + payload.find(x => x.dataKey === 'expensesUncategorized')?.value ?? 0 + + console.log('payload', payload) return (
@@ -393,12 +399,31 @@ export const ProfitAndLossChart = ({ ${centsToDollars(revenue)} +
  • + + + ${centsToDollars(revenueUncategorized)} + +
  • ${centsToDollars(Math.abs(expenses))}
  • +
  • + + + {expensesUncategorized && + `$${centsToDollars(expensesUncategorized * -1)}`} + +
  • ) : ( - - {formatMoney(Math.abs(data?.income?.value ?? NaN))} - +
    + + {formatMoney(Math.abs(data?.income?.value ?? NaN))} + + {uncategorizedTotalRevenue && ( + + {formatMoney(uncategorizedTotalRevenue)} + + )} +
    )} @@ -159,13 +172,22 @@ export const ProfitAndLossSummaries = ({ ) : ( - - {formatMoney( - Math.abs((data.income.value ?? 0) - data.net_profit), +
    + + {formatMoney( + Math.abs((data.income.value ?? 0) - data.net_profit), + )} + + {uncategorizedTotalExpenses && ( + + {formatMoney(uncategorizedTotalExpenses)} + )} - +
    )} diff --git a/src/hooks/useProfitAndLoss/useProfitAndLoss.tsx b/src/hooks/useProfitAndLoss/useProfitAndLoss.tsx index 66824bc36..efd11b74a 100644 --- a/src/hooks/useProfitAndLoss/useProfitAndLoss.tsx +++ b/src/hooks/useProfitAndLoss/useProfitAndLoss.tsx @@ -19,6 +19,8 @@ export type Scope = 'expenses' | 'revenue' export type SidebarScope = Scope | undefined +const EXCLUDE_FROM_TOTAL = ['Uncategorized'] + type Props = { startDate?: Date endDate?: Date @@ -46,6 +48,8 @@ type UseProfitAndLoss = (props?: Props) => { filteredTotalRevenue?: number filteredDataExpenses: LineBaseItem[] filteredTotalExpenses?: number + uncategorizedTotalRevenue?: number + uncategorizedTotalExpenses?: number isLoading: boolean isValidating: boolean error: unknown @@ -127,9 +131,17 @@ export const useProfitAndLoss: UseProfitAndLoss = ( }) } - const { filteredDataRevenue, filteredTotalRevenue } = useMemo(() => { + const { + filteredDataRevenue, + filteredTotalRevenue, + uncategorizedTotalRevenue, + } = useMemo(() => { if (!data) { - return { filteredDataRevenue: [], filteredTotalRevenue: undefined } + return { + filteredDataRevenue: [], + filteredTotalRevenue: undefined, + uncategorizedTotalRevenue: undefined, + } } const items = collectRevenueItems(data) const filtered = items.map(x => { @@ -150,7 +162,9 @@ export const useProfitAndLoss: UseProfitAndLoss = ( const month = startDate.getMonth() + 1 const year = startDate.getFullYear() const found = summaryData.find(x => x.month === month && x.year === year) + let uncategorizedTotal = undefined if (found && (found.uncategorizedInflows ?? 0) > 0) { + uncategorizedTotal = found.uncategorizedInflows filtered.push({ name: 'uncategorized', display_name: 'Uncategorized', @@ -183,16 +197,28 @@ export const useProfitAndLoss: UseProfitAndLoss = ( } }) const total = sorted - .filter(x => !x.hidden) + .filter(x => !x.hidden && !EXCLUDE_FROM_TOTAL.includes(x.type)) .reduce((x, { value }) => x + value, 0) - const withShare = applyShare(sorted, total) + const withShare = applyShare(sorted, total, EXCLUDE_FROM_TOTAL) - return { filteredDataRevenue: withShare, filteredTotalRevenue: total } + return { + filteredDataRevenue: withShare, + filteredTotalRevenue: total, + uncategorizedTotalRevenue: uncategorizedTotal, + } }, [data, startDate, filters, sidebarScope, summaryData]) - const { filteredDataExpenses, filteredTotalExpenses } = useMemo(() => { + const { + filteredDataExpenses, + filteredTotalExpenses, + uncategorizedTotalExpenses, + } = useMemo(() => { if (!data) { - return { filteredDataExpenses: [], filteredTotalExpenses: undefined } + return { + filteredDataExpenses: [], + filteredTotalExpenses: undefined, + uncategorizedTotalExpenses: undefined, + } } const items = collectExpensesItems(data) const filtered = items.map(x => { @@ -213,7 +239,9 @@ export const useProfitAndLoss: UseProfitAndLoss = ( const month = startDate.getMonth() + 1 const year = startDate.getFullYear() const found = summaryData.find(x => x.month === month && x.year === year) + let uncategorizedTotal = undefined if (found && (found.uncategorizedOutflows ?? 0) > 0) { + uncategorizedTotal = found.uncategorizedOutflows filtered.push({ name: 'uncategorized', display_name: 'Uncategorized', @@ -245,12 +273,17 @@ export const useProfitAndLoss: UseProfitAndLoss = ( return b.value - a.value } }) + const total = sorted - .filter(x => !x.hidden) + .filter(x => !x.hidden && !EXCLUDE_FROM_TOTAL.includes(x.type)) .reduce((x, { value }) => x + value, 0) - const withShare = applyShare(sorted, total) + const withShare = applyShare(sorted, total, EXCLUDE_FROM_TOTAL) - return { filteredDataExpenses: withShare, filteredTotalExpenses: total } + return { + filteredDataExpenses: withShare, + filteredTotalExpenses: total, + uncategorizedTotalExpenses: uncategorizedTotal, + } }, [data, startDate, filters, sidebarScope, summaryData]) return { @@ -259,6 +292,8 @@ export const useProfitAndLoss: UseProfitAndLoss = ( filteredTotalRevenue, filteredDataExpenses, filteredTotalExpenses, + uncategorizedTotalRevenue, + uncategorizedTotalExpenses, isLoading, isValidating, error: error, diff --git a/src/styles/profit_and_loss.scss b/src/styles/profit_and_loss.scss index b1ec0aba5..a6288906b 100644 --- a/src/styles/profit_and_loss.scss +++ b/src/styles/profit_and_loss.scss @@ -356,6 +356,7 @@ display: flex; flex-direction: column; gap: var(--spacing-4xs); + flex: 1; } .Layer__profit-and-loss-summaries__title { @@ -378,6 +379,19 @@ } } +.Layer__profit-and-loss-summaries__amount-wrapper { + display: flex; + flex: 1; + align-items: center; + justify-content: space-between; + padding-right: var(--spacing-2xs); +} + +.Layer__profit-and-loss-summaries__amount-uncategorized { + font-size: var(--text-sm); + color: var(--text-color-secondary); +} + .Layer__profit-and-loss-summaries__amount--negative { &::before { content: '-$'; diff --git a/src/utils/profitAndLossUtils.ts b/src/utils/profitAndLossUtils.ts index 70fb7417f..d17898d6f 100644 --- a/src/utils/profitAndLossUtils.ts +++ b/src/utils/profitAndLossUtils.ts @@ -63,12 +63,20 @@ export const humanizeTitle = (sidebarView: SidebarScope) => { export const applyShare = ( items: LineBaseItem[], total: number, + exclude?: string[], ): LineBaseItem[] => { return items.map(item => { if (total === 0) { return item } + if (exclude && exclude.includes(item.type)) { + return { + ...item, + share: undefined, + } + } + return { ...item, share: item.value / total, From 57f838741053f1bd2324327f8443771ca902305a Mon Sep 17 00:00:00 2001 From: Tom Antas Date: Tue, 20 Aug 2024 13:03:04 +0200 Subject: [PATCH 2/9] snap --- .../DetailedChart.tsx | 555 +++++++++++++++--- .../ProfitAndLossSummaries.tsx | 125 ++-- .../useProfitAndLoss/useProfitAndLoss.tsx | 4 +- .../useProfitAndLossQuery.tsx | 35 +- src/styles/profit_and_loss.scss | 13 + .../profit_and_loss_detailed_charts.scss | 52 +- src/styles/utils.scss | 4 + src/utils/profitAndLossUtils.ts | 16 +- 8 files changed, 658 insertions(+), 146 deletions(-) diff --git a/src/components/ProfitAndLossDetailedCharts/DetailedChart.tsx b/src/components/ProfitAndLossDetailedCharts/DetailedChart.tsx index 42cf0c0d9..853e61185 100644 --- a/src/components/ProfitAndLossDetailedCharts/DetailedChart.tsx +++ b/src/components/ProfitAndLossDetailedCharts/DetailedChart.tsx @@ -4,6 +4,7 @@ import { centsToDollars as formatMoney } from '../../models/Money' import { LineBaseItem } from '../../types/line_item' import { formatPercent } from '../../utils/format' import { ProfitAndLossDatePicker } from '../ProfitAndLossDatePicker' +import { Text, TextSize, TextWeight } from '../Typography' import { mapTypesToColors } from './DetailedTable' import classNames from 'classnames' import { @@ -28,6 +29,12 @@ interface DetailedChartProps { chartColorsList?: string[] } +interface ChartData { + name: string + value: number + type: string +} + export const DetailedChart = ({ filteredData, filteredTotal, @@ -37,29 +44,105 @@ export const DetailedChart = ({ isLoading, showDatePicker = true, }: DetailedChartProps) => { - const chartData = useMemo(() => { + // const chartData = useMemo(() => { + // if (!filteredData) { + // return [] + // } + // return filteredData.map(x => { + // if (x.hidden) { + // return { + // name: x.display_name, + // value: 0, + // type: x.type, + // } + // } + // return { + // name: x.display_name, + // value: x.value, + // type: x.type, + // } + // }) + // }, [filteredData, isLoading]) + + const { chartData, negativeData, total, negativeTotal } = useMemo(() => { + const chartData: ChartData[] = [] + const negativeData: ChartData[] = [] + let total = 0 + let negativeTotal = 0 if (!filteredData) { - return [] + return { + chartData, + negativeData, + total: undefined, + negativeTotal: undefined, + } } - return filteredData.map(x => { + filteredData.forEach(x => { if (x.hidden) { - return { - name: x.display_name, - value: 0, - type: x.type, + if (x.value < 0) { + negativeData.push({ + name: x.display_name, + value: 0, + type: x.type, + }) + } else { + chartData.push({ + name: x.display_name, + value: 0, + type: x.type, + }) } } - return { - name: x.display_name, - value: x.value, - type: x.type, + + if (x.value < 0) { + negativeData.push({ + name: x.display_name, + value: x.value, + type: x.type, + }) + negativeTotal -= x.value + } else { + chartData.push({ + name: x.display_name, + value: x.value, + type: x.type, + }) + total += x.value } }) + + if (total > negativeTotal) { + negativeData.push({ + name: '', + value: total - negativeTotal, + type: 'empty', + }) + } else { + chartData.push({ + name: '', + value: negativeTotal - total, + type: 'empty', + }) + } + + return { chartData, negativeData, total, negativeTotal } }, [filteredData, isLoading]) + console.log('chartData', chartData, negativeData, total, negativeTotal) + const noValue = chartData.length === 0 || !chartData.find(x => x.value !== 0) - const typeColorMapping = mapTypesToColors(chartData, chartColorsList) + const chartDataWithNoCategorized = chartData.filter( + x => x.type !== 'Uncategorized', + ) + + const typeColorMapping = mapTypesToColors( + chartDataWithNoCategorized, + chartColorsList, + ) + + const uncategorizedTotal = + chartData.find(x => x.type === 'Uncategorized')?.value ?? 0 return (
    @@ -97,7 +180,7 @@ export const DetailedChart = ({ {!isLoading && !noValue ? ( - {chartData.map((entry, index) => { - let fill: string | undefined = typeColorMapping[index].color + {chartDataWithNoCategorized.map((entry, index) => { + const placeholder = entry.type === 'empty' + let fill: string | undefined = + entry.type === 'empty' + ? '#fff' + : typeColorMapping[index].color let active = true if (hoveredItem && entry.name !== hoveredItem) { active = false @@ -132,9 +219,254 @@ export const DetailedChart = ({ ? 'url(#layer-pie-dots-pattern)' : fill, }} - opacity={typeColorMapping[index].opacity} - onMouseEnter={() => setHoveredItem(entry.name)} - onMouseLeave={() => setHoveredItem(undefined)} + opacity={ + placeholder ? 0 : typeColorMapping[index].opacity + } + onMouseEnter={() => + !placeholder && setHoveredItem(entry.name) + } + onMouseLeave={() => + !placeholder && setHoveredItem(undefined) + } + /> + ) + })} + {negativeTotal !== 0 ? ( + <> + + ) : null} + + {/* ------------- */} + + {!isLoading && !noValue && negativeTotal !== 0 ? ( + ({ + ...x, + value: Math.abs(x.value), + }))} + dataKey='value' + nameKey='name' + cx='50%' + cy='50%' + innerRadius={'74%'} + outerRadius={'83%'} + paddingAngle={0.5} + animationDuration={200} + animationEasing='ease-in-out' + > + {negativeData.map((entry, index) => { + let fill: string | undefined = + entry.type === 'empty' ? '#ffffff00' : '#3E4044' + + const opacity = 1 - ((0.2 * index) % 1) // @TODO + + return ( + ) })} @@ -149,7 +481,7 @@ export const DetailedChart = ({ } const positioningProps = { x: cx, - y: (cy || 0) - 15, + y: (cy || 0) + 15, textAnchor: 'middle' as | 'start' | 'middle' @@ -158,11 +490,7 @@ export const DetailedChart = ({ verticalAnchor: 'middle' as 'start' | 'middle' | 'end', } - let text = 'Total' - - if (hoveredItem) { - text = hoveredItem - } + let text = 'Negative' return ( - ) : null} + {/* ------------- */} {!isLoading && noValue ? ( + +
    + + ) +} + +const HorizontalLineChart = ({ + data, + uncategorizedTotal, + netRevenue, + typeColorMapping, +}: { + data?: ChartData[] + uncategorizedTotal: number + netRevenue?: number + typeColorMapping: { + color: any + opacity: any + }[] +}) => { + if (!data) { + return + } + + const total = + data.reduce((x, { value }) => (value < 0 ? x : x + value), 0) + + uncategorizedTotal + + const items = data + .filter(x => x.value >= 0 && x.type !== 'empty') + .map(x => ({ ...x, share: x.value / total })) + + if (uncategorizedTotal > 0) { + items.push({ + name: 'Uncategorized', + value: uncategorizedTotal, + type: 'uncategorized', + share: uncategorizedTotal / total, + }) + } + + console.log('items,', typeColorMapping, items) + + return ( +
    +
    + + Net Revenue + + + {`$${formatMoney(netRevenue)}`} + +
    + {!items || items.length === 0 ? ( +
    + +
    + ) : ( +
    + {items.map((x, index) => { + if (x.type === 'uncategorized') { + return ( + + ) + } + + const { color, opacity } = + typeColorMapping[index] ?? typeColorMapping[0] + return ( + + ) + })} +
    + )} +
    +
    + + Categorized + + + {`$${formatMoney(total)}`} + +
    +
    + + Uncategorized + + {`$${formatMoney(uncategorizedTotal)}`} +
    ) diff --git a/src/components/ProfitAndLossSummaries/ProfitAndLossSummaries.tsx b/src/components/ProfitAndLossSummaries/ProfitAndLossSummaries.tsx index 8167b7305..1dac5f711 100644 --- a/src/components/ProfitAndLossSummaries/ProfitAndLossSummaries.tsx +++ b/src/components/ProfitAndLossSummaries/ProfitAndLossSummaries.tsx @@ -7,6 +7,7 @@ import { collectExpensesItems, collectRevenueItems, } from '../../utils/profitAndLossUtils' +import { Badge, BadgeVariant } from '../Badge' import { ProfitAndLoss as PNL } from '../ProfitAndLoss' import { SkeletonLoader } from '../SkeletonLoader' import { MiniChart } from './MiniChart' @@ -124,32 +125,33 @@ export const ProfitAndLossSummaries = ({ actionable && setSidebarScope('revenue') }} > - -
    - - {stringOverrides?.revenueLabel || revenueLabel || 'Revenue'} - - {isLoading || storedData === undefined ? ( -
    - -
    - ) : ( -
    - - {formatMoney(Math.abs(data?.income?.value ?? NaN))} - - {uncategorizedTotalRevenue && ( +
    + +
    + + {stringOverrides?.revenueLabel || revenueLabel || 'Revenue'} + + {isLoading || storedData === undefined ? ( +
    + +
    + ) : ( +
    - {formatMoney(uncategorizedTotalRevenue)} + {formatMoney(Math.abs(data?.income?.value ?? NaN))} - )} -
    - )} +
    + )} +
    + {!dataItem?.fully_categorized && ( + + Uncategorized + {`$${formatMoney(uncategorizedTotalRevenue)}`} + + )}
    - -
    - - {stringOverrides?.expensesLabel || 'Expenses'} - - {isLoading || storedData === undefined ? ( -
    - -
    - ) : ( -
    - - {formatMoney( - Math.abs((data.income.value ?? 0) - data.net_profit), - )} - - {uncategorizedTotalExpenses && ( +
    + +
    + + {stringOverrides?.expensesLabel || 'Expenses'} + + {isLoading || storedData === undefined ? ( +
    + +
    + ) : ( +
    - {formatMoney(uncategorizedTotalExpenses)} + {formatMoney( + Math.abs((data.income.value ?? 0) - data.net_profit), + )} - )} -
    - )} +
    + )} +
    + {!dataItem?.fully_categorized && ( + + Uncategorized + {`$${formatMoney(uncategorizedTotalExpenses)}`} + + )}
    -
    - - {stringOverrides?.netProfitLabel || 'Net Profit'} - - {isLoading || storedData === undefined ? ( -
    - -
    - ) : ( - - {formatMoney(Math.abs(data.net_profit))} +
    +
    + + {stringOverrides?.netProfitLabel || 'Net Profit'} - )} + {isLoading || storedData === undefined ? ( +
    + +
    + ) : ( + + {formatMoney(Math.abs(data.net_profit))} + + )} +
    diff --git a/src/hooks/useProfitAndLoss/useProfitAndLoss.tsx b/src/hooks/useProfitAndLoss/useProfitAndLoss.tsx index efd11b74a..fc2b5fca6 100644 --- a/src/hooks/useProfitAndLoss/useProfitAndLoss.tsx +++ b/src/hooks/useProfitAndLoss/useProfitAndLoss.tsx @@ -198,7 +198,7 @@ export const useProfitAndLoss: UseProfitAndLoss = ( }) const total = sorted .filter(x => !x.hidden && !EXCLUDE_FROM_TOTAL.includes(x.type)) - .reduce((x, { value }) => x + value, 0) + .reduce((x, { value }) => (value < 0 ? x : x + value), 0) const withShare = applyShare(sorted, total, EXCLUDE_FROM_TOTAL) return { @@ -276,7 +276,7 @@ export const useProfitAndLoss: UseProfitAndLoss = ( const total = sorted .filter(x => !x.hidden && !EXCLUDE_FROM_TOTAL.includes(x.type)) - .reduce((x, { value }) => x + value, 0) + .reduce((x, { value }) => (value < 0 ? x : x + value), 0) const withShare = applyShare(sorted, total, EXCLUDE_FROM_TOTAL) return { diff --git a/src/hooks/useProfitAndLoss/useProfitAndLossQuery.tsx b/src/hooks/useProfitAndLoss/useProfitAndLossQuery.tsx index a449888e3..3a95d2f0f 100644 --- a/src/hooks/useProfitAndLoss/useProfitAndLossQuery.tsx +++ b/src/hooks/useProfitAndLoss/useProfitAndLossQuery.tsx @@ -89,10 +89,43 @@ export const useProfitAndLossQuery: UseProfitAndLossQueryReturn = ( return { startDate, endDate, - data: rawData?.data, + // data: rawData?.data, + data: testData(rawData?.data), isLoading, isValidating, error: rawError, refetch, } } + +const testData = (data?: ProfitAndLoss) => { + if (!data) { + return + } + + const a = { + ...data, + expenses: { + ...data.expenses, + line_items: data?.expenses?.line_items?.map(x => { + if (['FEES', 'MEALS'].includes(x.name ?? '')) { + return { ...x, value: -(x.value ?? 0) } + } + return x + }), + }, + cost_of_goods_sold: { + ...data.cost_of_goods_sold, + line_items: data?.cost_of_goods_sold?.line_items?.map(x => { + if (['LABOR_EXPENSE'].includes(x.name ?? '')) { + return { ...x, value: -(x.value ?? 0) } + } + return x + }), + }, + } as ProfitAndLoss + + console.log('datatest', a) + + return a +} diff --git a/src/styles/profit_and_loss.scss b/src/styles/profit_and_loss.scss index a6288906b..b16663d66 100644 --- a/src/styles/profit_and_loss.scss +++ b/src/styles/profit_and_loss.scss @@ -302,6 +302,7 @@ .Layer__profit-and-loss-summaries__summary { display: flex; flex: 1; + flex-direction: column; gap: var(--spacing-xs); align-items: center; box-shadow: 0px 0px 0px 1px var(--color-base-300); @@ -340,6 +341,18 @@ border-color: transparent; box-shadow: none; } + + & > .Layer__badge { + display: flex; + justify-content: space-between; + width: 100%; + } +} + +.Layer__profit-and-loss-summaries__summary__content { + display: flex; + flex: 1; + width: 100%; } .Layer__profit-and-loss-summaries__loader { diff --git a/src/styles/profit_and_loss_detailed_charts.scss b/src/styles/profit_and_loss_detailed_charts.scss index 177ca4d48..615190529 100644 --- a/src/styles/profit_and_loss_detailed_charts.scss +++ b/src/styles/profit_and_loss_detailed_charts.scss @@ -64,9 +64,9 @@ header.Layer__profit-and-loss-detailed-charts__header--tablet { .chart-container { width: 100%; - height: 280px; + height: calc(280px + 75px); padding-top: var(--spacing-2xl); - padding-bottom: var(--spacing-lg); + padding-bottom: calc(var(--spacing-lg) + 75px); padding-left: var(--spacing-md); padding-right: var(--spacing-md); box-sizing: border-box; @@ -378,3 +378,51 @@ header.Layer__profit-and-loss-detailed-charts__header--tablet { } } } + +.Layer__profit-and-loss-horiztonal-line-chart { + display: flex; + flex-direction: column; + padding: var(--spacing-xs); +} + +.Layer__profit-and-loss-horiztonal-line-chart__bar { + width: 100%; + align-items: center; + display: flex; + border-radius: 4px; + overflow: hidden; + margin: var(--spacing-2xs) 0; +} + +.Layer__profit-and-loss-horiztonal-line-chart__item { + height: 16px; + display: flex; + box-sizing: border-box; + transition: 120ms width ease-in-out; +} + +.Layer__profit-and-loss-horiztonal-line-chart__item--placeholder { + width: 100%; + background: var(--color-base-100); +} + +.Layer__profit-and-loss-horiztonal-line-chart__details-row { + display: flex; + width: 100%; + align-items: center; + justify-content: space-between; +} + +.Layer__profit-and-loss-horiztonal-line-chart__details-col { + display: flex; + flex-direction: column; + gap: var(--spacing-4xs); +} + +.Layer__profit-and-loss-horiztonal-line-chart__details-label { + color: var(--text-color-secondary); +} + +.Layer__profit-and-loss-horiztonal-line-chart__details-value { + color: var(--text-color-primary); +} diff --git a/src/styles/utils.scss b/src/styles/utils.scss index 7410dfab5..69afe44a8 100644 --- a/src/styles/utils.scss +++ b/src/styles/utils.scss @@ -22,6 +22,10 @@ justify-content: space-between; } +.Layer__align-end { + align-items: flex-end; +} + .Layer__nowrap { white-space: nowrap; } diff --git a/src/utils/profitAndLossUtils.ts b/src/utils/profitAndLossUtils.ts index d17898d6f..63a55a15f 100644 --- a/src/utils/profitAndLossUtils.ts +++ b/src/utils/profitAndLossUtils.ts @@ -4,13 +4,15 @@ import { ProfitAndLoss } from '../types/profit_and_loss' const doesLineItemQualifies = (item: LineItem) => { return !( - item.is_contra || - item.value === undefined || - item.value === null || - isNaN(item.value) || - item.value === -Infinity || - item.value === Infinity || - item.value < 0 + ( + item.is_contra || + item.value === undefined || + item.value === null || + isNaN(item.value) || + item.value === -Infinity || + item.value === Infinity + ) + // || item.value < 0 ) } From 65816ea0403434fde6f1fb5494a0a170c80705c8 Mon Sep 17 00:00:00 2001 From: Tom Antas Date: Tue, 20 Aug 2024 13:09:28 +0200 Subject: [PATCH 3/9] snap --- .../ProfitAndLossSummaries/ProfitAndLossSummaries.tsx | 6 ++++++ src/styles/profit_and_loss_detailed_charts.scss | 4 ++-- 2 files changed, 8 insertions(+), 2 deletions(-) diff --git a/src/components/ProfitAndLossSummaries/ProfitAndLossSummaries.tsx b/src/components/ProfitAndLossSummaries/ProfitAndLossSummaries.tsx index 1dac5f711..8c73ccd35 100644 --- a/src/components/ProfitAndLossSummaries/ProfitAndLossSummaries.tsx +++ b/src/components/ProfitAndLossSummaries/ProfitAndLossSummaries.tsx @@ -218,6 +218,12 @@ export const ProfitAndLossSummaries = ({ )}
    + {!dataItem?.fully_categorized && ( + + Uncategorized + {12} tbd transactions + + )} ) diff --git a/src/styles/profit_and_loss_detailed_charts.scss b/src/styles/profit_and_loss_detailed_charts.scss index 615190529..00d65c7cf 100644 --- a/src/styles/profit_and_loss_detailed_charts.scss +++ b/src/styles/profit_and_loss_detailed_charts.scss @@ -389,13 +389,13 @@ header.Layer__profit-and-loss-detailed-charts__header--tablet { width: 100%; align-items: center; display: flex; - border-radius: 4px; + border-radius: 2px; overflow: hidden; margin: var(--spacing-2xs) 0; } .Layer__profit-and-loss-horiztonal-line-chart__item { - height: 16px; + height: 9px; display: flex; box-sizing: border-box; transition: 120ms width ease-in-out; From 9175b1e8fc04200345a3a35aaeac066790664e1b Mon Sep 17 00:00:00 2001 From: Tom Antas Date: Thu, 22 Aug 2024 17:13:26 +0200 Subject: [PATCH 4/9] snap --- .../ProfitAndLossChart/ProfitAndLossChart.tsx | 2 - .../DetailedChart.tsx | 191 ++++++++++-------- .../DetailedTable.tsx | 52 ++++- .../ProfitAndLossSummaries.tsx | 49 ++++- src/config/charts.ts | 2 + .../useProfitAndLossQuery.tsx | 4 +- src/styles/profit_and_loss.scss | 17 ++ src/utils/profitAndLossUtils.ts | 2 +- 8 files changed, 216 insertions(+), 103 deletions(-) diff --git a/src/components/ProfitAndLossChart/ProfitAndLossChart.tsx b/src/components/ProfitAndLossChart/ProfitAndLossChart.tsx index 37b9e8951..5eb9fa07b 100644 --- a/src/components/ProfitAndLossChart/ProfitAndLossChart.tsx +++ b/src/components/ProfitAndLossChart/ProfitAndLossChart.tsx @@ -383,8 +383,6 @@ export const ProfitAndLossChart = ({ const expensesUncategorized = payload.find(x => x.dataKey === 'expensesUncategorized')?.value ?? 0 - console.log('payload', payload) - return (
    {loaded !== 'complete' ? ( diff --git a/src/components/ProfitAndLossDetailedCharts/DetailedChart.tsx b/src/components/ProfitAndLossDetailedCharts/DetailedChart.tsx index 853e61185..35400d6de 100644 --- a/src/components/ProfitAndLossDetailedCharts/DetailedChart.tsx +++ b/src/components/ProfitAndLossDetailedCharts/DetailedChart.tsx @@ -5,7 +5,7 @@ import { LineBaseItem } from '../../types/line_item' import { formatPercent } from '../../utils/format' import { ProfitAndLossDatePicker } from '../ProfitAndLossDatePicker' import { Text, TextSize, TextWeight } from '../Typography' -import { mapTypesToColors } from './DetailedTable' +import { ColorsMapOption, mapTypesToColors } from './DetailedTable' import classNames from 'classnames' import { PieChart, @@ -112,13 +112,13 @@ export const DetailedChart = ({ }) if (total > negativeTotal) { - negativeData.push({ + negativeData.unshift({ name: '', value: total - negativeTotal, type: 'empty', }) } else { - chartData.push({ + chartData.unshift({ name: '', value: negativeTotal - total, type: 'empty', @@ -128,8 +128,6 @@ export const DetailedChart = ({ return { chartData, negativeData, total, negativeTotal } }, [filteredData, isLoading]) - console.log('chartData', chartData, negativeData, total, negativeTotal) - const noValue = chartData.length === 0 || !chartData.find(x => x.value !== 0) const chartDataWithNoCategorized = chartData.filter( @@ -137,7 +135,7 @@ export const DetailedChart = ({ ) const typeColorMapping = mapTypesToColors( - chartDataWithNoCategorized, + chartDataWithNoCategorized.concat(negativeData), chartColorsList, ) @@ -197,7 +195,8 @@ export const DetailedChart = ({ let fill: string | undefined = entry.type === 'empty' ? '#fff' - : typeColorMapping[index].color + : typeColorMapping.find(x => x.name === entry.name) + ?.color ?? '#f2f2f2' let active = true if (hoveredItem && entry.name !== hoveredItem) { active = false @@ -220,7 +219,10 @@ export const DetailedChart = ({ : fill, }} opacity={ - placeholder ? 0 : typeColorMapping[index].opacity + placeholder + ? 0 + : typeColorMapping.find(x => x.name === entry.name) + ?.opacity ?? 1 } onMouseEnter={() => !placeholder && setHoveredItem(entry.name) @@ -231,7 +233,7 @@ export const DetailedChart = ({ /> ) })} - {negativeTotal !== 0 ? ( + {negativeTotal !== 0 && !hoveredItem ? ( <>
    ) diff --git a/src/config/charts.ts b/src/config/charts.ts index ab6eb22a3..c0ab8bb73 100644 --- a/src/config/charts.ts +++ b/src/config/charts.ts @@ -4,6 +4,8 @@ export const INACTIVE_OPACITY_LEVELS = [ export const DEFAULT_CHART_OPACITY = [1, 0.8, 0.6, 0.4, 0.2, 0.1] +export const DEFAULT_CHART_NEGATIVE_COLOR = '#3E4044' + export const DEFAULT_CHART_COLOR_TYPE = [ '#008028', '#7417B3', diff --git a/src/hooks/useProfitAndLoss/useProfitAndLossQuery.tsx b/src/hooks/useProfitAndLoss/useProfitAndLossQuery.tsx index 3a95d2f0f..630329042 100644 --- a/src/hooks/useProfitAndLoss/useProfitAndLossQuery.tsx +++ b/src/hooks/useProfitAndLoss/useProfitAndLossQuery.tsx @@ -89,8 +89,8 @@ export const useProfitAndLossQuery: UseProfitAndLossQueryReturn = ( return { startDate, endDate, - // data: rawData?.data, - data: testData(rawData?.data), + data: rawData?.data, // @TODO - TOM + // data: testData(rawData?.data), isLoading, isValidating, error: rawError, diff --git a/src/styles/profit_and_loss.scss b/src/styles/profit_and_loss.scss index b16663d66..c76a37267 100644 --- a/src/styles/profit_and_loss.scss +++ b/src/styles/profit_and_loss.scss @@ -295,6 +295,23 @@ max-width: 1406px; } +.Layer__profit-and-loss-summaries__info-banner { + border-radius: var(--border-radius-xs); + background-color: var(--color-base-50); + display: flex; + align-items: center; + justify-content: space-between; + width: 100%; + color: var(--color-info-warning-fg); + padding: var(--spacing-2xs); + box-sizing: border-box; +} + +.Layer__profit-and-loss-summaries__info-banner__value { + display: flex; + align-items: center; +} + .Layer__profit-and-loss-summaries.flex-col { flex-direction: column; } diff --git a/src/utils/profitAndLossUtils.ts b/src/utils/profitAndLossUtils.ts index 63a55a15f..15833ac71 100644 --- a/src/utils/profitAndLossUtils.ts +++ b/src/utils/profitAndLossUtils.ts @@ -72,7 +72,7 @@ export const applyShare = ( return item } - if (exclude && exclude.includes(item.type)) { + if ((exclude && exclude.includes(item.type)) || item.value < 0) { return { ...item, share: undefined, From 2ec34ed5d47821e71944bb12830a7059bcd7b851 Mon Sep 17 00:00:00 2001 From: Tom Antas Date: Fri, 23 Aug 2024 12:53:00 +0200 Subject: [PATCH 5/9] summaries ui --- src/components/Badge/Badge.tsx | 16 ++ .../NotificationCard/NotificationCard.tsx | 19 +- .../ProfitAndLossChart/ProfitAndLossChart.tsx | 22 ++- .../DetailedChart.tsx | 165 ++---------------- .../HorizontalLineChart.tsx | 120 +++++++++++++ .../ProfitAndLossDetailedCharts.tsx | 1 + .../ProfitAndLossSummaries.tsx | 78 ++++++--- .../ProfitAndLossView/ProfitAndLossView.tsx | 1 + .../TransactionToReviewCard/Badges.tsx | 84 +++++++++ .../TransactionToReviewCard.tsx | 72 ++++---- .../useProfitAndLoss/useProfitAndLoss.tsx | 13 ++ .../useProfitAndLossQuery.tsx | 4 +- src/styles/accounting_overview.scss | 4 +- src/styles/badge.scss | 13 ++ src/styles/bookkeeping_overview.scss | 5 +- src/styles/charts.scss | 5 + src/styles/notification_card.scss | 22 ++- src/styles/profit_and_loss.scss | 33 +++- .../AccountingOverview/AccountingOverview.tsx | 3 + .../BookkeepingOverview.tsx | 1 + 20 files changed, 442 insertions(+), 239 deletions(-) create mode 100644 src/components/ProfitAndLossDetailedCharts/HorizontalLineChart.tsx create mode 100644 src/components/TransactionToReviewCard/Badges.tsx diff --git a/src/components/Badge/Badge.tsx b/src/components/Badge/Badge.tsx index f3ee7c5fb..a7312d2e4 100644 --- a/src/components/Badge/Badge.tsx +++ b/src/components/Badge/Badge.tsx @@ -14,6 +14,12 @@ export enum BadgeVariant { SUCCESS = 'success', WARNING = 'warning', ERROR = 'error', + LIGHT = 'light', +} + +export enum BadgeRoundings { + SMALL = 'small', + MEDIUM = 'medium', } export interface BadgeProps { @@ -24,6 +30,9 @@ export interface BadgeProps { size?: BadgeSize variant?: BadgeVariant hoverable?: boolean + className?: string + wide?: boolean + roundings?: BadgeRoundings } export const Badge = ({ @@ -34,6 +43,9 @@ export const Badge = ({ size = BadgeSize.MEDIUM, variant = BadgeVariant.DEFAULT, hoverable = false, + className, + wide, + roundings = BadgeRoundings.SMALL, }: BadgeProps) => { const baseProps = { className: classNames( @@ -42,6 +54,10 @@ export const Badge = ({ onClick || tooltip ? 'Layer__badge--clickable' : '', `Layer__badge--${size}`, `Layer__badge--${variant}`, + roundings !== BadgeRoundings.MEDIUM && + `Layer__badge--roundings-${roundings}`, + wide && 'Layer__badge--wide', + className, ), onClick, children, diff --git a/src/components/NotificationCard/NotificationCard.tsx b/src/components/NotificationCard/NotificationCard.tsx index b77611cea..8921eb1cf 100644 --- a/src/components/NotificationCard/NotificationCard.tsx +++ b/src/components/NotificationCard/NotificationCard.tsx @@ -6,22 +6,29 @@ import classNames from 'classnames' export interface NotificationCardProps { onClick: () => void children: ReactNode + bottomBar?: ReactNode className?: string } export const NotificationCard = ({ onClick, children, + bottomBar, className, }: NotificationCardProps) => { return (
    -
    {children}
    - } - withBorder - onClick={() => onClick()} - /> +
    +
    {children}
    + } + withBorder + onClick={() => onClick()} + /> +
    + {bottomBar && ( +
    {bottomBar}
    + )}
    ) } diff --git a/src/components/ProfitAndLossChart/ProfitAndLossChart.tsx b/src/components/ProfitAndLossChart/ProfitAndLossChart.tsx index 5eb9fa07b..0bdc35cd5 100644 --- a/src/components/ProfitAndLossChart/ProfitAndLossChart.tsx +++ b/src/components/ProfitAndLossChart/ProfitAndLossChart.tsx @@ -397,12 +397,11 @@ export const ProfitAndLossChart = ({ ${centsToDollars(revenue)}
    -
  • - - +
  • + + ${centsToDollars(revenueUncategorized)}
  • @@ -412,12 +411,11 @@ export const ProfitAndLossChart = ({ ${centsToDollars(Math.abs(expenses))} -
  • - - +
  • + + {expensesUncategorized && `$${centsToDollars(expensesUncategorized * -1)}`} diff --git a/src/components/ProfitAndLossDetailedCharts/DetailedChart.tsx b/src/components/ProfitAndLossDetailedCharts/DetailedChart.tsx index 35400d6de..8051d2d50 100644 --- a/src/components/ProfitAndLossDetailedCharts/DetailedChart.tsx +++ b/src/components/ProfitAndLossDetailedCharts/DetailedChart.tsx @@ -4,8 +4,8 @@ import { centsToDollars as formatMoney } from '../../models/Money' import { LineBaseItem } from '../../types/line_item' import { formatPercent } from '../../utils/format' import { ProfitAndLossDatePicker } from '../ProfitAndLossDatePicker' -import { Text, TextSize, TextWeight } from '../Typography' -import { ColorsMapOption, mapTypesToColors } from './DetailedTable' +import { mapTypesToColors } from './DetailedTable' +import { HorizontalLineChart } from './HorizontalLineChart' import classNames from 'classnames' import { PieChart, @@ -27,9 +27,10 @@ interface DetailedChartProps { isLoading?: boolean showDatePicker?: boolean chartColorsList?: string[] + showHorizontalChart?: boolean } -interface ChartData { +export interface ChartData { name: string value: number type: string @@ -43,27 +44,8 @@ export const DetailedChart = ({ chartColorsList, isLoading, showDatePicker = true, + showHorizontalChart = false, }: DetailedChartProps) => { - // const chartData = useMemo(() => { - // if (!filteredData) { - // return [] - // } - // return filteredData.map(x => { - // if (x.hidden) { - // return { - // name: x.display_name, - // value: 0, - // type: x.type, - // } - // } - // return { - // name: x.display_name, - // value: x.value, - // type: x.type, - // } - // }) - // }, [filteredData, isLoading]) - const { chartData, negativeData, total, negativeTotal } = useMemo(() => { const chartData: ChartData[] = [] const negativeData: ChartData[] = [] @@ -77,6 +59,7 @@ export const DetailedChart = ({ negativeTotal: undefined, } } + filteredData.forEach(x => { if (x.hidden) { if (x.value < 0) { @@ -92,6 +75,7 @@ export const DetailedChart = ({ type: x.type, }) } + return } if (x.value < 0) { @@ -437,8 +421,6 @@ export const DetailedChart = ({ ) : null} - {/* ------------- */} - {!isLoading && !noValue && negativeTotal !== 0 ? ( ({ @@ -468,8 +450,6 @@ export const DetailedChart = ({ fill = undefined } - console.log('active', fill, active, entry.name, hoveredItem) - return ( ) : null} - {/* ------------- */} {!isLoading && noValue ? ( - - - - ) -} - -const HorizontalLineChart = ({ - data, - uncategorizedTotal, - netRevenue, - typeColorMapping, -}: { - data?: ChartData[] - uncategorizedTotal: number - netRevenue?: number - typeColorMapping: ColorsMapOption[] -}) => { - if (!data) { - return - } - - const total = - data.reduce((x, { value }) => (value < 0 ? x : x + value), 0) + - uncategorizedTotal - - const items = data - .filter(x => x.value >= 0 && x.type !== 'empty') - .map(x => ({ ...x, share: x.value / total })) - - if (uncategorizedTotal > 0) { - items.push({ - name: 'Uncategorized', - value: uncategorizedTotal, - type: 'uncategorized', - share: uncategorizedTotal / total, - }) - } - - return ( -
    -
    - - Net Revenue - - - {`$${formatMoney(netRevenue)}`} - -
    - {!items || items.length === 0 ? ( -
    - -
    - ) : ( -
    - {items.map((x, index) => { - if (x.type === 'uncategorized') { - return ( - - ) - } - - const { color, opacity } = - typeColorMapping.find(y => y.name === x.name) ?? - typeColorMapping[0] - return ( - - ) - })} -
    - )} -
    -
    - - Categorized - - - {`$${formatMoney(total)}`} - -
    -
    - - Uncategorized - - {`$${formatMoney(uncategorizedTotal)}`} -
    + {showHorizontalChart && ( + + )}
    ) diff --git a/src/components/ProfitAndLossDetailedCharts/HorizontalLineChart.tsx b/src/components/ProfitAndLossDetailedCharts/HorizontalLineChart.tsx new file mode 100644 index 000000000..e1a9abda2 --- /dev/null +++ b/src/components/ProfitAndLossDetailedCharts/HorizontalLineChart.tsx @@ -0,0 +1,120 @@ +import React from 'react' +import { centsToDollars as formatMoney } from '../../models/Money' +import { Text, TextSize, TextWeight } from '../Typography' +import { ChartData } from './DetailedChart' +import { ColorsMapOption } from './DetailedTable' + +export const HorizontalLineChart = ({ + data, + uncategorizedTotal, + netValue, + type, + typeColorMapping, +}: { + data?: ChartData[] + uncategorizedTotal: number + netValue?: number + type: string + typeColorMapping: ColorsMapOption[] +}) => { + if (!data) { + return + } + + const total = data.reduce((x, { value }) => (value < 0 ? x : x + value), 0) + + const items = data + .filter(x => x.value >= 0 && x.type !== 'empty') + .map(x => ({ ...x, share: x.value / total })) + + if (uncategorizedTotal > 0) { + items.push({ + name: 'Uncategorized', + value: uncategorizedTotal, + type: 'uncategorized', + share: uncategorizedTotal / total, + }) + } + + return ( +
    +
    + + Net {type} + + + {`$${formatMoney(netValue)}`} + +
    + {!items || items.length === 0 ? ( +
    + +
    + ) : ( +
    + {items.map((x, index) => { + if (x.type === 'uncategorized') { + return ( + + ) + } + + const { color, opacity } = + typeColorMapping.find(y => y.name === x.name) ?? + typeColorMapping[0] + return ( + + ) + })} +
    + )} +
    +
    + + Categorized + + + {`$${formatMoney(total)}`} + +
    +
    + + Uncategorized + + {`$${formatMoney(uncategorizedTotal)}`} +
    +
    +
    + ) +} diff --git a/src/components/ProfitAndLossDetailedCharts/ProfitAndLossDetailedCharts.tsx b/src/components/ProfitAndLossDetailedCharts/ProfitAndLossDetailedCharts.tsx index 28085af41..fd09be70e 100644 --- a/src/components/ProfitAndLossDetailedCharts/ProfitAndLossDetailedCharts.tsx +++ b/src/components/ProfitAndLossDetailedCharts/ProfitAndLossDetailedCharts.tsx @@ -97,6 +97,7 @@ export const ProfitAndLossDetailedCharts = ({ isLoading={isLoading} chartColorsList={chartColorsList} showDatePicker={showDatePicker} + showHorizontalChart />
    diff --git a/src/components/ProfitAndLossSummaries/ProfitAndLossSummaries.tsx b/src/components/ProfitAndLossSummaries/ProfitAndLossSummaries.tsx index 13435a9d4..71bbe1ce7 100644 --- a/src/components/ProfitAndLossSummaries/ProfitAndLossSummaries.tsx +++ b/src/components/ProfitAndLossSummaries/ProfitAndLossSummaries.tsx @@ -1,5 +1,6 @@ import React, { useContext, useMemo } from 'react' import { Scope } from '../../hooks/useProfitAndLoss/useProfitAndLoss' +import Check from '../../icons/Check' import { centsToDollars as formatMoney } from '../../models/Money' import { ProfitAndLoss } from '../../types' import { LineBaseItem } from '../../types/line_item' @@ -25,6 +26,7 @@ type Props = { actionable?: boolean revenueLabel?: string // deprecated stringOverrides?: ProfitAndLossSummariesStringOverrides + showUncategorized?: boolean } const CHART_PLACEHOLDER = [ @@ -68,11 +70,13 @@ export const ProfitAndLossSummaries = ({ actionable = false, revenueLabel, // deprecated stringOverrides, + showUncategorized = false, }: Props) => { const { data: storedData, uncategorizedTotalExpenses, uncategorizedTotalRevenue, + uncategorizedTransactions, isLoading, setSidebarScope, sidebarScope, @@ -92,8 +96,6 @@ export const ProfitAndLossSummaries = ({ const data = dataItem ? dataItem : { income: { value: NaN }, net_profit: NaN } - console.log('dataItem', data, storedData) - const incomeDirectionClass = (data.income.value ?? NaN) < 0 ? 'Layer__profit-and-loss-summaries__amount--negative' @@ -109,6 +111,8 @@ export const ProfitAndLossSummaries = ({ ? 'Layer__profit-and-loss-summaries__amount--negative' : 'Layer__profit-and-loss-summaries__amount--positive' + const expenses = Math.abs((data.income.value ?? 0) - data.net_profit) + return (
    - {!dataItem?.fully_categorized && ( + {showUncategorized && + !dataItem?.fully_categorized && + uncategorizedTotalRevenue ? (
    - Uncategorized + + Uncategorized + {`$${formatMoney( uncategorizedTotalRevenue, )}`} - + / {`$${formatMoney( uncategorizedTotalRevenue ?? @@ -163,7 +177,15 @@ export const ProfitAndLossSummaries = ({
    - )} + ) : null} + {showUncategorized && + !dataItem?.fully_categorized && + !uncategorizedTotalRevenue ? ( +
    + + All categorized +
    + ) : null}
    - {!dataItem?.fully_categorized && ( + {showUncategorized && + !dataItem?.fully_categorized && + uncategorizedTotalExpenses ? (
    - Uncategorized + + Uncategorized + {`$${formatMoney( uncategorizedTotalExpenses, )}`} - + / {`$${formatMoney( - uncategorizedTotalExpenses ?? - 0 + Math.abs((data.income.value ?? 0) - data.net_profit), + Math.abs(uncategorizedTotalExpenses ?? 0) + expenses, )}`}
    - )} + ) : null} + {showUncategorized && + !dataItem?.fully_categorized && + !uncategorizedTotalExpenses ? ( +
    + + All categorized +
    + ) : null}
    - {!dataItem?.fully_categorized && ( - - Uncategorized - {12} tbd transactions - - )} - {!dataItem?.fully_categorized && ( + {showUncategorized && !dataItem?.fully_categorized && (
    Uncategorized - {12} / 24 transactions + + {uncategorizedTransactions}{' '} + + transactions + +
    )} diff --git a/src/components/ProfitAndLossView/ProfitAndLossView.tsx b/src/components/ProfitAndLossView/ProfitAndLossView.tsx index c4835a791..fbd66a9a4 100644 --- a/src/components/ProfitAndLossView/ProfitAndLossView.tsx +++ b/src/components/ProfitAndLossView/ProfitAndLossView.tsx @@ -101,6 +101,7 @@ const Components = ({ vertical={true} actionable stringOverrides={stringOverrides?.profitAndLossSummaries} + showUncategorized />
    void + toReview: number + inBottomBar?: boolean +} + +export const Badges = ({ + loaded, + error, + refetch, + toReview, + inBottomBar, +}: BadgesProps) => { + if (loaded === 'initial' || loaded === 'loading') { + if (inBottomBar) { + return ( + } + wide + > + Checking... + + ) + } + + return + } + + if (loaded === 'complete' && error) { + return ( + } + onClick={() => refetch()} + wide + > + Refresh + + ) + } + + if (loaded === 'complete' && !error && toReview > 0) { + return ( + } + wide + > + {toReview} pending + + ) + } + + if (loaded === 'complete' && !error && toReview === 0) { + return ( + } + wide + > + All done + + ) + } + + return null +} diff --git a/src/components/TransactionToReviewCard/TransactionToReviewCard.tsx b/src/components/TransactionToReviewCard/TransactionToReviewCard.tsx index 2b06bf20a..f414fcf0f 100644 --- a/src/components/TransactionToReviewCard/TransactionToReviewCard.tsx +++ b/src/components/TransactionToReviewCard/TransactionToReviewCard.tsx @@ -1,24 +1,24 @@ import React, { useContext, useEffect, useState } from 'react' -import { Badge } from '../../components/Badge' -import { BadgeSize, BadgeVariant } from '../../components/Badge/Badge' import { Text, TextSize } from '../../components/Typography' import { useProfitAndLossLTM } from '../../hooks/useProfitAndLoss/useProfitAndLossLTM' -import BellIcon from '../../icons/Bell' -import CheckIcon from '../../icons/Check' -import RefreshCcw from '../../icons/RefreshCcw' -import { BadgeLoader } from '../BadgeLoader' import { NotificationCard } from '../NotificationCard' import { ProfitAndLoss } from '../ProfitAndLoss' +import { Badges } from './Badges' +import classNames from 'classnames' import { getMonth, getYear, startOfMonth } from 'date-fns' export interface TransactionToReviewCardProps { onClick?: () => void usePnlDateRange?: boolean + hideWhenNoTransactions?: boolean + size?: 'large' | 'medium' } export const TransactionToReviewCard = ({ onClick, usePnlDateRange, + size = 'medium', + hideWhenNoTransactions = false, }: TransactionToReviewCardProps) => { const { dateRange: contextDateRange } = useContext(ProfitAndLoss.Context) const dateRange = usePnlDateRange ? contextDateRange : undefined @@ -50,44 +50,38 @@ export const TransactionToReviewCard = ({ } } + if (toReview === 0 && hideWhenNoTransactions) { + return null + } + return ( onClick && onClick()} + bottomBar={ + size === 'large' && ( + + ) + } > Transactions to review - {loaded === 'initial' || loaded === 'loading' ? : null} - - {loaded === 'complete' && error ? ( - } - onClick={() => refetch()} - > - Refresh - - ) : null} - - {loaded === 'complete' && !error && toReview > 0 ? ( - } - > - {toReview} pending - - ) : null} - - {loaded === 'complete' && !error && toReview === 0 ? ( - } - > - All done - - ) : null} + {size !== 'large' && ( + + )} ) } diff --git a/src/hooks/useProfitAndLoss/useProfitAndLoss.tsx b/src/hooks/useProfitAndLoss/useProfitAndLoss.tsx index fc2b5fca6..884c31085 100644 --- a/src/hooks/useProfitAndLoss/useProfitAndLoss.tsx +++ b/src/hooks/useProfitAndLoss/useProfitAndLoss.tsx @@ -50,6 +50,7 @@ type UseProfitAndLoss = (props?: Props) => { filteredTotalExpenses?: number uncategorizedTotalRevenue?: number uncategorizedTotalExpenses?: number + uncategorizedTransactions?: number isLoading: boolean isValidating: boolean error: unknown @@ -286,6 +287,17 @@ export const useProfitAndLoss: UseProfitAndLoss = ( } }, [data, startDate, filters, sidebarScope, summaryData]) + const uncategorizedTransactions = useMemo(() => { + if (!summaryData) { + return + } + + const month = startDate.getMonth() + 1 + const year = startDate.getFullYear() + return summaryData.find(x => x.month === month && x.year === year) + ?.uncategorized_transactions + }, [summaryData, startDate]) + return { data, filteredDataRevenue, @@ -294,6 +306,7 @@ export const useProfitAndLoss: UseProfitAndLoss = ( filteredTotalExpenses, uncategorizedTotalRevenue, uncategorizedTotalExpenses, + uncategorizedTransactions, isLoading, isValidating, error: error, diff --git a/src/hooks/useProfitAndLoss/useProfitAndLossQuery.tsx b/src/hooks/useProfitAndLoss/useProfitAndLossQuery.tsx index 630329042..2ebb1363a 100644 --- a/src/hooks/useProfitAndLoss/useProfitAndLossQuery.tsx +++ b/src/hooks/useProfitAndLoss/useProfitAndLossQuery.tsx @@ -89,8 +89,8 @@ export const useProfitAndLossQuery: UseProfitAndLossQueryReturn = ( return { startDate, endDate, - data: rawData?.data, // @TODO - TOM - // data: testData(rawData?.data), + // data: rawData?.data, // @TODO - TOM + data: testData(rawData?.data), isLoading, isValidating, error: rawError, diff --git a/src/styles/accounting_overview.scss b/src/styles/accounting_overview.scss index 465339ba2..4fd969e6d 100644 --- a/src/styles/accounting_overview.scss +++ b/src/styles/accounting_overview.scss @@ -1,13 +1,13 @@ .Layer__accounting-overview__summaries-row { display: flex; - align-items: center; + align-items: stretch; gap: var(--spacing-md); max-width: 1406px; width: 100%; .Layer__notification-card { width: calc(25% - 12px); - height: 56px; + min-height: 56px; } } diff --git a/src/styles/badge.scss b/src/styles/badge.scss index 3547b80ea..f2fed4368 100644 --- a/src/styles/badge.scss +++ b/src/styles/badge.scss @@ -11,6 +11,14 @@ box-sizing: border-box; height: 27px; + &.Layer__badge--wide { + width: 100%; + } + + &.Layer__badge--roundings-small { + border-radius: var(--border-radius-2xs); + } + &.Layer__badge--small { font-size: var(--text-xs); padding: var(--spacing-3xs) var(--spacing-xs); @@ -48,6 +56,11 @@ color: var(--badge-fg-color-success); } + &.Layer__badge--light { + background-color: var(--color-base-50); + color: var(--text-color-primary); + } + &.Layer__badge--warning { background-color: var(--badge-bg-color-warning); color: var(--badge-fg-color-warning); diff --git a/src/styles/bookkeeping_overview.scss b/src/styles/bookkeeping_overview.scss index 9525f92f6..64fcf100e 100644 --- a/src/styles/bookkeeping_overview.scss +++ b/src/styles/bookkeeping_overview.scss @@ -26,14 +26,14 @@ .Layer__bookkeeping-overview__summaries-row { display: flex; - align-items: center; + align-items: stretch; gap: var(--spacing-md); max-width: 1406px; padding: var(--spacing-md); .Layer__notification-card { width: calc(25% - 12px); - height: 56px; + min-height: 56px; } } @@ -55,7 +55,6 @@ top: 16px !important; } - @container (max-width: 796px) { .Layer__bookkeeping-overview__summaries-row { flex-direction: column; diff --git a/src/styles/charts.scss b/src/styles/charts.scss index 12da46d2d..7260b4531 100644 --- a/src/styles/charts.scss +++ b/src/styles/charts.scss @@ -71,6 +71,11 @@ } } +.Layer__chart__tooltip-list--secondary-item { + opacity: 0.6; + margin-bottom: var(--spacing-sm); +} + .Layer__chart_y-axis-tick { tspan { font-size: var(--text-sm); diff --git a/src/styles/notification_card.scss b/src/styles/notification_card.scss index 391f116c5..04578ad11 100644 --- a/src/styles/notification_card.scss +++ b/src/styles/notification_card.scss @@ -2,12 +2,28 @@ box-sizing: border-box; box-shadow: 0px 0px 0px 1px var(--color-base-300); border-radius: var(--border-radius-xs); - padding: var(--spacing-2xs) var(--spacing-xs); + padding: var(--spacing-4xs); min-height: 52px; background-color: var(--color-base-0); display: flex; align-items: center; justify-content: space-between; + flex-direction: column; + + .Layer__notification-card__content { + display: flex; + width: 100%; + align-items: center; + justify-content: space-between; + flex: 1; + padding: var(--spacing-2xs); + box-sizing: border-box; + } + + .Layer__notification-card__bottom-bar { + display: flex; + width: 100%; + } .Layer__skeleton-loader { margin-top: 3px; @@ -38,4 +54,8 @@ background-color: var(--color-base-50); font-variation-settings: 'wght' var(--font-weight-bold); } + + .Layer__notification-card__bottom-bar .Layer__badge { + min-height: 25px; + } } diff --git a/src/styles/profit_and_loss.scss b/src/styles/profit_and_loss.scss index c76a37267..5829faba7 100644 --- a/src/styles/profit_and_loss.scss +++ b/src/styles/profit_and_loss.scss @@ -303,8 +303,22 @@ justify-content: space-between; width: 100%; color: var(--color-info-warning-fg); - padding: var(--spacing-2xs); + padding: var(--spacing-2xs) var(--spacing-xs); box-sizing: border-box; + height: 25px; + container-type: inline-size; + + &.Layer__profit-and-loss-summaries__info-banner--done { + justify-content: flex-start; + color: var(--color-info-success-fg); + } + + @container (max-width: 220px) { + .Layer__profit-and-loss-summaries-hide-xs { + display: none; + align-self: flex-end; + } + } } .Layer__profit-and-loss-summaries__info-banner__value { @@ -312,6 +326,10 @@ align-items: center; } +.Layer__profit-and-loss-summaries__info-banner__subvalue { + opacity: 0.6; +} + .Layer__profit-and-loss-summaries.flex-col { flex-direction: column; } @@ -320,7 +338,7 @@ display: flex; flex: 1; flex-direction: column; - gap: var(--spacing-xs); + gap: var(--spacing-4xs); align-items: center; box-shadow: 0px 0px 0px 1px var(--color-base-300); border-radius: var(--border-radius-xs); @@ -349,8 +367,11 @@ } &.net-profit { - padding-left: var(--spacing-xs); - min-height: 56px; + .Layer__profit-and-loss-summaries__summary__content { + min-height: 56px; + padding-left: var(--spacing-xs); + box-sizing: border-box; + } } &.Layer__actionable.net-profit { @@ -370,6 +391,8 @@ display: flex; flex: 1; width: 100%; + align-items: center; + gap: var(--spacing-sm); } .Layer__profit-and-loss-summaries__loader { @@ -384,6 +407,7 @@ .Layer__profit-and-loss-summaries__text { display: flex; + justify-content: center; flex-direction: column; gap: var(--spacing-4xs); flex: 1; @@ -411,7 +435,6 @@ .Layer__profit-and-loss-summaries__amount-wrapper { display: flex; - flex: 1; align-items: center; justify-content: space-between; padding-right: var(--spacing-2xs); diff --git a/src/views/AccountingOverview/AccountingOverview.tsx b/src/views/AccountingOverview/AccountingOverview.tsx index 0773d42dd..27ec3319a 100644 --- a/src/views/AccountingOverview/AccountingOverview.tsx +++ b/src/views/AccountingOverview/AccountingOverview.tsx @@ -49,10 +49,13 @@ export const AccountingOverview = ({
    From c7fa964eb77505e0a8d83d4846ac3fc535fad733 Mon Sep 17 00:00:00 2001 From: Tom Antas Date: Fri, 23 Aug 2024 13:24:09 +0200 Subject: [PATCH 6/9] use stripes --- .../DetailedChart.tsx | 2 + .../DetailedTable.tsx | 22 ++--- .../HorizontalLineChart.tsx | 80 ++++++++++++++++--- .../useProfitAndLossQuery.tsx | 35 +------- src/styles/profit_and_loss.scss | 12 +++ .../profit_and_loss_detailed_charts.scss | 4 + 6 files changed, 98 insertions(+), 57 deletions(-) diff --git a/src/components/ProfitAndLossDetailedCharts/DetailedChart.tsx b/src/components/ProfitAndLossDetailedCharts/DetailedChart.tsx index 8051d2d50..3d69725ad 100644 --- a/src/components/ProfitAndLossDetailedCharts/DetailedChart.tsx +++ b/src/components/ProfitAndLossDetailedCharts/DetailedChart.tsx @@ -663,6 +663,8 @@ export const DetailedChart = ({ netValue={filteredTotal} type={sidebarScope === 'expenses' ? 'Expenses' : 'Revenue'} typeColorMapping={typeColorMapping} + hoveredItem={hoveredItem} + setHoveredItem={setHoveredItem} /> )} diff --git a/src/components/ProfitAndLossDetailedCharts/DetailedTable.tsx b/src/components/ProfitAndLossDetailedCharts/DetailedTable.tsx index 0e621f83e..2f339b9d2 100644 --- a/src/components/ProfitAndLossDetailedCharts/DetailedTable.tsx +++ b/src/components/ProfitAndLossDetailedCharts/DetailedTable.tsx @@ -115,23 +115,25 @@ const ValueIcon = ({ > - + + + + - ) diff --git a/src/components/ProfitAndLossDetailedCharts/HorizontalLineChart.tsx b/src/components/ProfitAndLossDetailedCharts/HorizontalLineChart.tsx index e1a9abda2..237b39bba 100644 --- a/src/components/ProfitAndLossDetailedCharts/HorizontalLineChart.tsx +++ b/src/components/ProfitAndLossDetailedCharts/HorizontalLineChart.tsx @@ -3,6 +3,17 @@ import { centsToDollars as formatMoney } from '../../models/Money' import { Text, TextSize, TextWeight } from '../Typography' import { ChartData } from './DetailedChart' import { ColorsMapOption } from './DetailedTable' +import classNames from 'classnames' + +interface HorizontalLineChartProps { + data?: ChartData[] + uncategorizedTotal: number + netValue?: number + type: string + typeColorMapping: ColorsMapOption[] + hoveredItem?: string + setHoveredItem: (name?: string) => void +} export const HorizontalLineChart = ({ data, @@ -10,13 +21,9 @@ export const HorizontalLineChart = ({ netValue, type, typeColorMapping, -}: { - data?: ChartData[] - uncategorizedTotal: number - netValue?: number - type: string - typeColorMapping: ColorsMapOption[] -}) => { + hoveredItem, + setHoveredItem, +}: HorizontalLineChartProps) => { if (!data) { return } @@ -59,27 +66,74 @@ export const HorizontalLineChart = ({ ) : (
    - {items.map((x, index) => { + {items.map(x => { if (x.type === 'uncategorized') { return ( - + + + + + + + + + + + + + ) } const { color, opacity } = typeColorMapping.find(y => y.name === x.name) ?? typeColorMapping[0] + return ( setHoveredItem(x.name)} + onMouseLeave={() => setHoveredItem(undefined)} /> ) })} diff --git a/src/hooks/useProfitAndLoss/useProfitAndLossQuery.tsx b/src/hooks/useProfitAndLoss/useProfitAndLossQuery.tsx index 2ebb1363a..a449888e3 100644 --- a/src/hooks/useProfitAndLoss/useProfitAndLossQuery.tsx +++ b/src/hooks/useProfitAndLoss/useProfitAndLossQuery.tsx @@ -89,43 +89,10 @@ export const useProfitAndLossQuery: UseProfitAndLossQueryReturn = ( return { startDate, endDate, - // data: rawData?.data, // @TODO - TOM - data: testData(rawData?.data), + data: rawData?.data, isLoading, isValidating, error: rawError, refetch, } } - -const testData = (data?: ProfitAndLoss) => { - if (!data) { - return - } - - const a = { - ...data, - expenses: { - ...data.expenses, - line_items: data?.expenses?.line_items?.map(x => { - if (['FEES', 'MEALS'].includes(x.name ?? '')) { - return { ...x, value: -(x.value ?? 0) } - } - return x - }), - }, - cost_of_goods_sold: { - ...data.cost_of_goods_sold, - line_items: data?.cost_of_goods_sold?.line_items?.map(x => { - if (['LABOR_EXPENSE'].includes(x.name ?? '')) { - return { ...x, value: -(x.value ?? 0) } - } - return x - }), - }, - } as ProfitAndLoss - - console.log('datatest', a) - - return a -} diff --git a/src/styles/profit_and_loss.scss b/src/styles/profit_and_loss.scss index 5829faba7..3604f2bde 100644 --- a/src/styles/profit_and_loss.scss +++ b/src/styles/profit_and_loss.scss @@ -525,6 +525,10 @@ fill: var(--color-base-300); } +#layer-pie-stripe-pattern line { + stroke: var(--color-base-900); +} + #layer-dots-stripe-pattern rect { fill: var(--color-base-500); } @@ -545,6 +549,14 @@ fill: var(--color-base-100); } +#layer-pie-dots-pattern-line-chart rect { + fill: var(--color-base-500); +} + +#layer-pie-dots-pattern-bg-line-chart { + fill: var(--color-base-100); +} + @container (min-width: 1024px) { .Layer__profit-and-loss-row__label--depth-0 { padding-left: var(--spacing-xl); diff --git a/src/styles/profit_and_loss_detailed_charts.scss b/src/styles/profit_and_loss_detailed_charts.scss index 00d65c7cf..d4a3bd372 100644 --- a/src/styles/profit_and_loss_detailed_charts.scss +++ b/src/styles/profit_and_loss_detailed_charts.scss @@ -288,6 +288,10 @@ header.Layer__profit-and-loss-detailed-charts__header--tablet { } } +.Layer__profit-and-loss-horiztonal-line-chart__item--inactive { + background-color: var(--color-base-300) !important; +} + .Layer__profit-and-loss-detailed-charts .header--tablet { display: none; } From 95e03d579585f7eaa8b0976e064244a2000bc14d Mon Sep 17 00:00:00 2001 From: Tom Antas Date: Mon, 26 Aug 2024 16:38:30 +0200 Subject: [PATCH 7/9] clean --- src/utils/profitAndLossUtils.ts | 15 ++++++--------- 1 file changed, 6 insertions(+), 9 deletions(-) diff --git a/src/utils/profitAndLossUtils.ts b/src/utils/profitAndLossUtils.ts index 15833ac71..8b3be1905 100644 --- a/src/utils/profitAndLossUtils.ts +++ b/src/utils/profitAndLossUtils.ts @@ -4,15 +4,12 @@ import { ProfitAndLoss } from '../types/profit_and_loss' const doesLineItemQualifies = (item: LineItem) => { return !( - ( - item.is_contra || - item.value === undefined || - item.value === null || - isNaN(item.value) || - item.value === -Infinity || - item.value === Infinity - ) - // || item.value < 0 + item.is_contra || + item.value === undefined || + item.value === null || + isNaN(item.value) || + item.value === -Infinity || + item.value === Infinity ) } From 20b18fa44dad9ce5e04f63bdab2ab5f1ffe9b3e0 Mon Sep 17 00:00:00 2001 From: Tom Antas Date: Fri, 30 Aug 2024 12:43:45 +0200 Subject: [PATCH 8/9] use categorized --- .../ProfitAndLossSummaries.tsx | 5 +++- .../useProfitAndLoss/useProfitAndLoss.tsx | 24 +++++++++++++++---- .../useProfitAndLoss/useProfitAndLossLTM.tsx | 1 + src/types/profit_and_loss.ts | 1 + 4 files changed, 26 insertions(+), 5 deletions(-) diff --git a/src/components/ProfitAndLossSummaries/ProfitAndLossSummaries.tsx b/src/components/ProfitAndLossSummaries/ProfitAndLossSummaries.tsx index 71bbe1ce7..dc14454cb 100644 --- a/src/components/ProfitAndLossSummaries/ProfitAndLossSummaries.tsx +++ b/src/components/ProfitAndLossSummaries/ProfitAndLossSummaries.tsx @@ -77,6 +77,7 @@ export const ProfitAndLossSummaries = ({ uncategorizedTotalExpenses, uncategorizedTotalRevenue, uncategorizedTransactions, + categorizedTransactions, isLoading, setSidebarScope, sidebarScope, @@ -285,7 +286,9 @@ export const ProfitAndLossSummaries = ({ Uncategorized - {uncategorizedTransactions}{' '} + {uncategorizedTransactions ?? 0}/ + {(uncategorizedTransactions ?? 0) + + (categorizedTransactions ?? 0)}{' '} transactions diff --git a/src/hooks/useProfitAndLoss/useProfitAndLoss.tsx b/src/hooks/useProfitAndLoss/useProfitAndLoss.tsx index 884c31085..5dfb33e15 100644 --- a/src/hooks/useProfitAndLoss/useProfitAndLoss.tsx +++ b/src/hooks/useProfitAndLoss/useProfitAndLoss.tsx @@ -51,6 +51,7 @@ type UseProfitAndLoss = (props?: Props) => { uncategorizedTotalRevenue?: number uncategorizedTotalExpenses?: number uncategorizedTransactions?: number + categorizedTransactions?: number isLoading: boolean isValidating: boolean error: unknown @@ -287,15 +288,29 @@ export const useProfitAndLoss: UseProfitAndLoss = ( } }, [data, startDate, filters, sidebarScope, summaryData]) - const uncategorizedTransactions = useMemo(() => { + const { uncategorizedTransactions, categorizedTransactions } = useMemo(() => { if (!summaryData) { - return + return { + categorizedTransactions: undefined, + uncategorizedTransactions: undefined, + } } const month = startDate.getMonth() + 1 const year = startDate.getFullYear() - return summaryData.find(x => x.month === month && x.year === year) - ?.uncategorized_transactions + const record = summaryData.find(x => x.month === month && x.year === year) + + if (!record) { + return { + categorizedTransactions: undefined, + uncategorizedTransactions: undefined, + } + } + + return { + categorizedTransactions: record.categorized_transactions, + uncategorizedTransactions: record.uncategorized_transactions, + } }, [summaryData, startDate]) return { @@ -307,6 +322,7 @@ export const useProfitAndLoss: UseProfitAndLoss = ( uncategorizedTotalRevenue, uncategorizedTotalExpenses, uncategorizedTransactions, + categorizedTransactions, isLoading, isValidating, error: error, diff --git a/src/hooks/useProfitAndLoss/useProfitAndLossLTM.tsx b/src/hooks/useProfitAndLoss/useProfitAndLossLTM.tsx index 498946c1a..63296fd31 100644 --- a/src/hooks/useProfitAndLoss/useProfitAndLossLTM.tsx +++ b/src/hooks/useProfitAndLoss/useProfitAndLossLTM.tsx @@ -131,6 +131,7 @@ export const useProfitAndLossLTM: UseProfitAndLossLTMReturn = ( uncategorizedInflows: 0, uncategorizedOutflows: 0, uncategorized_transactions: 0, + categorized_transactions: 0, isLoading: true, } satisfies ProfitAndLossSummaryData) } diff --git a/src/types/profit_and_loss.ts b/src/types/profit_and_loss.ts index 360c26335..224a6be4e 100644 --- a/src/types/profit_and_loss.ts +++ b/src/types/profit_and_loss.ts @@ -70,6 +70,7 @@ export interface ProfitAndLossSummary { totalExpensesInverse?: number uncategorizedOutflowsInverse?: number uncategorized_transactions: number + categorized_transactions: number } export interface ProfitAndLossSummaries { From 0af6b892dfe99da2b02cbd490b7a1696a160d2dd Mon Sep 17 00:00:00 2001 From: Tom Antas Date: Thu, 5 Sep 2024 10:44:39 +0200 Subject: [PATCH 9/9] resolve --- src/components/ProfitAndLossDetailedCharts/DetailedChart.tsx | 1 + 1 file changed, 1 insertion(+) diff --git a/src/components/ProfitAndLossDetailedCharts/DetailedChart.tsx b/src/components/ProfitAndLossDetailedCharts/DetailedChart.tsx index 3d69725ad..28e18794e 100644 --- a/src/components/ProfitAndLossDetailedCharts/DetailedChart.tsx +++ b/src/components/ProfitAndLossDetailedCharts/DetailedChart.tsx @@ -45,6 +45,7 @@ export const DetailedChart = ({ isLoading, showDatePicker = true, showHorizontalChart = false, + sidebarScope, }: DetailedChartProps) => { const { chartData, negativeData, total, negativeTotal } = useMemo(() => { const chartData: ChartData[] = []