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 56c9a1733..0bdc35cd5 100644 --- a/src/components/ProfitAndLossChart/ProfitAndLossChart.tsx +++ b/src/components/ProfitAndLossChart/ProfitAndLossChart.tsx @@ -378,6 +378,10 @@ 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 return (
@@ -393,12 +397,29 @@ export const ProfitAndLossChart = ({ ${centsToDollars(revenue)} +
  • + + + ${centsToDollars(revenueUncategorized)} + +
  • ${centsToDollars(Math.abs(expenses))}
  • +
  • + + + {expensesUncategorized && + `$${centsToDollars(expensesUncategorized * -1)}`} + +
  • ) diff --git a/src/components/ProfitAndLossDetailedCharts/DetailedTable.tsx b/src/components/ProfitAndLossDetailedCharts/DetailedTable.tsx index 89078213a..2f339b9d2 100644 --- a/src/components/ProfitAndLossDetailedCharts/DetailedTable.tsx +++ b/src/components/ProfitAndLossDetailedCharts/DetailedTable.tsx @@ -1,5 +1,8 @@ import React from 'react' -import { DEFAULT_CHART_COLOR_TYPE } from '../../config/charts' +import { + DEFAULT_CHART_COLOR_TYPE, + DEFAULT_CHART_NEGATIVE_COLOR, +} from '../../config/charts' import { Scope, SidebarScope, @@ -29,25 +32,51 @@ export interface DetailedTableProps { stringOverrides?: DetailedTableStringOverrides } +export interface ColorsMapOption { + color: string + name: string + opacity: number + type: string +} + export const mapTypesToColors = ( data: any[], colorList: string[] = DEFAULT_CHART_COLOR_TYPE, -) => { + negativeColor = DEFAULT_CHART_NEGATIVE_COLOR, +): ColorsMapOption[] => { const typeToColor: any = {} const typeToLastOpacity: any = {} let colorIndex = 0 + let colorNegativeIndex = 0 return data.map(obj => { - const type = obj.type + const type = obj.value < 0 ? 'negative' : obj.type if (type === 'Uncategorized') { return { color: '#EEEEF0', opacity: 1, + type: 'Uncategorized', + name: 'Uncategorized', + } + } + + if (type === 'empty') { + return { + color: '#fff', + opacity: 0, + type: 'empty', + name: 'empty', } } - if (!typeToColor[type]) { + if (type === 'negative' && !typeToColor['negative']) { + typeToColor['negative'] = negativeColor + colorNegativeIndex++ + typeToLastOpacity[type] = 1 + } else if (type === 'negative' && typeToColor[type]) { + typeToLastOpacity[type] -= 0.1 + } else if (!typeToColor[type]) { typeToColor[type] = colorList[colorIndex % colorList.length] colorIndex++ typeToLastOpacity[type] = 1 @@ -58,6 +87,8 @@ export const mapTypesToColors = ( const opacity = typeToLastOpacity[type] return { + type: type, + name: obj.name, color: typeToColor[type], opacity: opacity, } @@ -70,7 +101,7 @@ const ValueIcon = ({ idx, }: { item: LineBaseItem - typeColorMapping: any + typeColorMapping: ColorsMapOption[] idx: number }) => { if (item.type === 'Uncategorized') { @@ -84,34 +115,41 @@ const ValueIcon = ({ > - + + + + - ) } + const colorMapping = typeColorMapping.find(x => x.name === item.name) ?? { + color: '#f2f2f2', + opacity: 1, + } + return (
    ) @@ -138,7 +176,7 @@ export const DetailedTable = ({ ) } - const typeColorMapping: any = mapTypesToColors(filteredData, chartColorsList) + const typeColorMapping = mapTypesToColors(filteredData, chartColorsList) return (
    @@ -191,7 +229,9 @@ export const DetailedTable = ({ ${formatMoney(item.value)} - {formatPercent(item.share)}% + {item.share !== undefined + ? `${formatPercent(item.share)}%` + : ''} void +} + +export const HorizontalLineChart = ({ + data, + uncategorizedTotal, + netValue, + type, + typeColorMapping, + hoveredItem, + setHoveredItem, +}: HorizontalLineChartProps) => { + 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 => { + if (x.type === 'uncategorized') { + return ( + + + + + + + + + + + + + + ) + } + + const { color, opacity } = + typeColorMapping.find(y => y.name === x.name) ?? + typeColorMapping[0] + + return ( + setHoveredItem(x.name)} + onMouseLeave={() => setHoveredItem(undefined)} + /> + ) + })} +
    + )} +
    +
    + + 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 8d42f001a..dc14454cb 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' @@ -7,8 +8,10 @@ import { collectExpensesItems, collectRevenueItems, } from '../../utils/profitAndLossUtils' +import { Badge, BadgeVariant } from '../Badge' import { ProfitAndLoss as PNL } from '../ProfitAndLoss' import { SkeletonLoader } from '../SkeletonLoader' +import { Text, TextSize } from '../Typography' import { MiniChart } from './MiniChart' import classNames from 'classnames' @@ -23,6 +26,7 @@ type Props = { actionable?: boolean revenueLabel?: string // deprecated stringOverrides?: ProfitAndLossSummariesStringOverrides + showUncategorized?: boolean } const CHART_PLACEHOLDER = [ @@ -66,9 +70,14 @@ export const ProfitAndLossSummaries = ({ actionable = false, revenueLabel, // deprecated stringOverrides, + showUncategorized = false, }: Props) => { const { data: storedData, + uncategorizedTotalExpenses, + uncategorizedTotalRevenue, + uncategorizedTransactions, + categorizedTransactions, isLoading, setSidebarScope, sidebarScope, @@ -103,6 +112,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 (
    - -
    - - {stringOverrides?.revenueLabel || revenueLabel || 'Revenue'} - - {isLoading || storedData === undefined ? ( -
    - -
    - ) : ( - - {formatMoney(Math.abs(data?.income?.value ?? NaN))} +
    + +
    + + {stringOverrides?.revenueLabel || revenueLabel || 'Revenue'} - )} + {isLoading || storedData === undefined ? ( +
    + +
    + ) : ( +
    + + {formatMoney(Math.abs(data?.income?.value ?? NaN))} + +
    + )} +
    + {showUncategorized && + !dataItem?.fully_categorized && + uncategorizedTotalRevenue ? ( +
    + + Uncategorized + + + {`$${formatMoney( + uncategorizedTotalRevenue, + )}`} + + / + {`$${formatMoney( + uncategorizedTotalRevenue ?? + 0 + Math.abs(data?.income?.value ?? 0), + )}`} + + +
    + ) : null} + {showUncategorized && + !dataItem?.fully_categorized && + !uncategorizedTotalRevenue ? ( +
    + + All categorized +
    + ) : null}
    - -
    - - {stringOverrides?.expensesLabel || 'Expenses'} - - {isLoading || storedData === undefined ? ( -
    - -
    - ) : ( - - {formatMoney( - Math.abs((data.income.value ?? 0) - data.net_profit), - )} +
    + +
    + + {stringOverrides?.expensesLabel || 'Expenses'} - )} + {isLoading || storedData === undefined ? ( +
    + +
    + ) : ( +
    + + {formatMoney( + Math.abs((data.income.value ?? 0) - data.net_profit), + )} + +
    + )} +
    + {showUncategorized && + !dataItem?.fully_categorized && + uncategorizedTotalExpenses ? ( +
    + + Uncategorized + + + {`$${formatMoney( + uncategorizedTotalExpenses, + )}`} + + / + {`$${formatMoney( + Math.abs(uncategorizedTotalExpenses ?? 0) + expenses, + )}`} + + +
    + ) : null} + {showUncategorized && + !dataItem?.fully_categorized && + !uncategorizedTotalExpenses ? ( +
    + + All categorized +
    + ) : null}
    -
    - - {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))} + + )} +
    + {showUncategorized && !dataItem?.fully_categorized && ( +
    + Uncategorized + + + {uncategorizedTransactions ?? 0}/ + {(uncategorizedTransactions ?? 0) + + (categorizedTransactions ?? 0)}{' '} + + 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/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/useProfitAndLoss.tsx b/src/hooks/useProfitAndLoss/useProfitAndLoss.tsx index 66824bc36..5dfb33e15 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,10 @@ type UseProfitAndLoss = (props?: Props) => { filteredTotalRevenue?: number filteredDataExpenses: LineBaseItem[] filteredTotalExpenses?: number + uncategorizedTotalRevenue?: number + uncategorizedTotalExpenses?: number + uncategorizedTransactions?: number + categorizedTransactions?: number isLoading: boolean isValidating: boolean error: unknown @@ -127,9 +133,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 +164,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 +199,28 @@ export const useProfitAndLoss: UseProfitAndLoss = ( } }) const total = sorted - .filter(x => !x.hidden) - .reduce((x, { value }) => x + value, 0) - const withShare = applyShare(sorted, total) + .filter(x => !x.hidden && !EXCLUDE_FROM_TOTAL.includes(x.type)) + .reduce((x, { value }) => (value < 0 ? x : x + value), 0) + 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 +241,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,20 +275,54 @@ export const useProfitAndLoss: UseProfitAndLoss = ( return b.value - a.value } }) + const total = sorted - .filter(x => !x.hidden) - .reduce((x, { value }) => x + value, 0) - const withShare = applyShare(sorted, total) + .filter(x => !x.hidden && !EXCLUDE_FROM_TOTAL.includes(x.type)) + .reduce((x, { value }) => (value < 0 ? x : x + value), 0) + 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]) + const { uncategorizedTransactions, categorizedTransactions } = useMemo(() => { + if (!summaryData) { + return { + categorizedTransactions: undefined, + uncategorizedTransactions: undefined, + } + } + + const month = startDate.getMonth() + 1 + const year = startDate.getFullYear() + 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 { data, filteredDataRevenue, filteredTotalRevenue, filteredDataExpenses, filteredTotalExpenses, + 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/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 b1ec0aba5..3604f2bde 100644 --- a/src/styles/profit_and_loss.scss +++ b/src/styles/profit_and_loss.scss @@ -295,6 +295,41 @@ 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) 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 { + display: flex; + align-items: center; +} + +.Layer__profit-and-loss-summaries__info-banner__subvalue { + opacity: 0.6; +} + .Layer__profit-and-loss-summaries.flex-col { flex-direction: column; } @@ -302,7 +337,8 @@ .Layer__profit-and-loss-summaries__summary { display: flex; flex: 1; - gap: var(--spacing-xs); + flex-direction: column; + gap: var(--spacing-4xs); align-items: center; box-shadow: 0px 0px 0px 1px var(--color-base-300); border-radius: var(--border-radius-xs); @@ -331,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 { @@ -340,6 +379,20 @@ 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%; + align-items: center; + gap: var(--spacing-sm); } .Layer__profit-and-loss-summaries__loader { @@ -354,8 +407,10 @@ .Layer__profit-and-loss-summaries__text { display: flex; + justify-content: center; flex-direction: column; gap: var(--spacing-4xs); + flex: 1; } .Layer__profit-and-loss-summaries__title { @@ -378,6 +433,18 @@ } } +.Layer__profit-and-loss-summaries__amount-wrapper { + display: flex; + 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: '-$'; @@ -458,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); } @@ -478,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 177ca4d48..d4a3bd372 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; @@ -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; } @@ -378,3 +382,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: 2px; + overflow: hidden; + margin: var(--spacing-2xs) 0; +} + +.Layer__profit-and-loss-horiztonal-line-chart__item { + height: 9px; + 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/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 { diff --git a/src/utils/profitAndLossUtils.ts b/src/utils/profitAndLossUtils.ts index 70fb7417f..8b3be1905 100644 --- a/src/utils/profitAndLossUtils.ts +++ b/src/utils/profitAndLossUtils.ts @@ -9,8 +9,7 @@ const doesLineItemQualifies = (item: LineItem) => { item.value === null || isNaN(item.value) || item.value === -Infinity || - item.value === Infinity || - item.value < 0 + item.value === Infinity ) } @@ -63,12 +62,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)) || item.value < 0) { + return { + ...item, + share: undefined, + } + } + return { ...item, share: item.value / total, 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 = ({