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 (
@@ -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 = ({
|