-
-
Notifications
You must be signed in to change notification settings - Fork 410
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
- Loading branch information
1 parent
7ca4a8b
commit 20db272
Showing
47 changed files
with
3,029 additions
and
509 deletions.
There are no files selected for viewing
Large diffs are not rendered by default.
Oops, something went wrong.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,51 @@ | ||
import React from 'react'; | ||
import type { Column, ColumnDef, Row, TableMeta } from '@tanstack/react-table'; | ||
import clsx from 'clsx'; | ||
import { isNil, omit } from 'lodash'; | ||
import { ArrowDown10, ArrowDownZA, ArrowUp10, ArrowUpZA } from 'lucide-react'; | ||
import { FormattedMessage } from 'react-intl'; | ||
|
||
import type { | ||
AccountMetricsFragment, | ||
Currency, | ||
OverviewMetricsQueryVariables, | ||
} from '../../lib/graphql/types/v2/graphql'; | ||
import type { useQueryFilterReturnType } from '../../lib/hooks/useQueryFilter'; | ||
import { getCollectivePageRoute } from '../../lib/url-helpers'; | ||
|
||
import { AccountHoverCard } from '../AccountHoverCard'; | ||
import Avatar from '../Avatar'; | ||
import type { schema } from '../dashboard/sections/overview/CollectiveOverview'; | ||
import type { MetricProps } from '../dashboard/sections/overview/Metric'; | ||
import { ChangeBadge, getPercentageDifference } from '../dashboard/sections/overview/Metric'; | ||
import FormattedMoneyAmount from '../FormattedMoneyAmount'; | ||
import Link from '../Link'; | ||
import { DataTable } from '../table/DataTable'; | ||
import { Badge } from '../ui/Badge'; | ||
import { Button } from '../ui/Button'; | ||
import { Checkbox } from '../ui/Checkbox'; | ||
import { AccountsSublist } from './AccountsSublist'; | ||
|
||
export default function AccountsList({ data, queryFilter, loading, metric }) { | ||
const currency = data?.account?.[metric.id]?.current?.currency; | ||
|
||
const meta = { | ||
queryFilter, | ||
currency: currency, | ||
isAmount: !!metric.amount, | ||
metric, | ||
}; | ||
|
||
// if (error) { | ||
// return <MessageBoxGraphqlError error={error} />; | ||
// } | ||
|
||
return ( | ||
<div className="space-y-8"> | ||
<AccountsSublist label="Main account" type="COLLECTIVE" data={data} metric={metric} meta={meta} /> | ||
<AccountsSublist label="Projects" type="PROJECT" data={data} metric={metric} meta={meta} /> | ||
|
||
<AccountsSublist label="Events" type="EVENT" data={data} metric={metric} meta={meta} /> | ||
</div> | ||
); | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,222 @@ | ||
import React from 'react'; | ||
import type { Column, ColumnDef, Row, TableMeta } from '@tanstack/react-table'; | ||
import clsx from 'clsx'; | ||
import { isNil, omit } from 'lodash'; | ||
import { ArrowDown10, ArrowDownZA, ArrowUp10, ArrowUpZA, ChevronRight } from 'lucide-react'; | ||
import { FormattedMessage } from 'react-intl'; | ||
|
||
import type { | ||
AccountMetricsFragment, | ||
Currency, | ||
OverviewMetricsQueryVariables, | ||
} from '../../lib/graphql/types/v2/graphql'; | ||
import type { useQueryFilterReturnType } from '../../lib/hooks/useQueryFilter'; | ||
import { getCollectivePageRoute } from '../../lib/url-helpers'; | ||
|
||
import { AccountHoverCard } from '../AccountHoverCard'; | ||
import Avatar from '../Avatar'; | ||
import type { schema } from '../dashboard/sections/overview/CollectiveOverview'; | ||
import type { MetricProps } from '../dashboard/sections/overview/Metric'; | ||
import { ChangeBadge, getPercentageDifference } from '../dashboard/sections/overview/Metric'; | ||
import FormattedMoneyAmount from '../FormattedMoneyAmount'; | ||
import Link from '../Link'; | ||
import { DataTable } from '../table/DataTable'; | ||
import { Badge } from '../ui/Badge'; | ||
import { Button } from '../ui/Button'; | ||
import { Checkbox } from '../ui/Checkbox'; | ||
import { useRouter } from 'next/router'; | ||
type AccountMetricsRow = AccountMetricsFragment & { | ||
current: number; | ||
comparison?: number; | ||
percentageDifference?: number; | ||
}; | ||
|
||
interface AccountMetricsMeta extends TableMeta<AccountMetricsRow> { | ||
currency: Currency; | ||
isAmount: boolean; | ||
queryFilter: useQueryFilterReturnType<typeof schema, OverviewMetricsQueryVariables>; | ||
metric: MetricProps; | ||
} | ||
|
||
const SortableHeader = ({ | ||
column, | ||
label, | ||
type, | ||
align, | ||
}: { | ||
column: Column<AccountMetricsRow, unknown>; | ||
label: React.ReactNode; | ||
type?: 'alphabetic' | 'numerical'; | ||
align?: 'left' | 'right'; | ||
}) => { | ||
const isSorted = column.getIsSorted(); | ||
const isSortedDesc = isSorted === 'desc'; | ||
const UpIcon = type === 'alphabetic' ? ArrowUpZA : ArrowUp10; | ||
const DownIcon = type === 'alphabetic' ? ArrowDownZA : ArrowDown10; | ||
const SortIcon = isSortedDesc || !isSorted ? UpIcon : DownIcon; | ||
return ( | ||
<div className={clsx('flex items-center', align === 'right' && 'justify-end')}> | ||
<Button | ||
variant="ghost" | ||
size="xs" | ||
className={clsx('group/btn -m-2 gap-2', isSorted && 'text-foreground')} | ||
onClick={() => column.toggleSorting(!isSortedDesc)} | ||
> | ||
<SortIcon | ||
className={clsx( | ||
'h-4 w-4 transition-colors', | ||
isSorted ? 'text-muted-foreground' : 'text-transparent group-hover/btn:text-muted-foreground', | ||
)} | ||
/> | ||
<span className={clsx(align === 'left' && '-order-1')}>{label}</span> | ||
</Button> | ||
</div> | ||
); | ||
}; | ||
const columns: ColumnDef<AccountMetricsRow>[] = [ | ||
{ | ||
id: 'name', | ||
accessorKey: 'name', | ||
header: ({ column }) => ( | ||
<SortableHeader | ||
column={column} | ||
type="alphabetic" | ||
label={<FormattedMessage defaultMessage="Name" id="HAlOn1" />} | ||
align="left" | ||
/> | ||
), | ||
meta: { className: 'min-w-0 max-w-[300px]' }, | ||
|
||
cell: ({ row, table }) => { | ||
const account = row.original; | ||
// const { queryFilter } = table.options.meta as AccountMetricsMeta; | ||
// const selectedAccountSlug = queryFilter.values.account; | ||
return ( | ||
<div className="flex items-center gap-3 text-base"> | ||
<div className="flex items-center gap-1.5 overflow-hidden"> | ||
<AccountHoverCard | ||
account={account} | ||
trigger={ | ||
<div className="max-w-[400px] truncate"> | ||
<Link | ||
href={getCollectivePageRoute(account)} | ||
className={clsx('truncate hover:underline group-hover/row:text-foreground')} | ||
> | ||
{account.name} | ||
</Link> | ||
</div> | ||
} | ||
/> | ||
|
||
{account.isArchived && ( | ||
<Badge size="xs" className="capitalize"> | ||
Archived {account.type.toLowerCase()} | ||
</Badge> | ||
)} | ||
</div> | ||
</div> | ||
); | ||
}, | ||
}, | ||
{ | ||
id: 'current', | ||
accessorKey: 'current', | ||
meta: { className: 'text-right' }, | ||
header: ({ column, table }) => { | ||
const meta = table.options.meta as AccountMetricsMeta; | ||
return <SortableHeader align="right" column={column} label={meta.metric.label} />; | ||
}, | ||
sortingFn: (rowA: Row<AccountMetricsRow>, rowB: Row<AccountMetricsRow>): number => { | ||
const a = rowA.original.current; | ||
const b = rowB.original.current; | ||
|
||
const diff = a - b; | ||
|
||
// sort by comparison value if current is the same | ||
if (diff === 0) { | ||
const rowAPrevious = rowA.original.comparison; | ||
const rowBPrevious = rowB.original.comparison; | ||
return rowAPrevious - rowBPrevious; | ||
} | ||
return a - b; | ||
}, | ||
cell: ({ cell, table }) => { | ||
const current = cell.getValue() as number; | ||
const meta = table.options.meta as AccountMetricsMeta; | ||
|
||
return ( | ||
<div className="flex items-center justify-end gap-2"> | ||
<span className="text-base font-medium"> | ||
{meta.isAmount ? ( | ||
<FormattedMoneyAmount amount={current} currency={meta.currency} precision={2} showCurrencyCode={false} /> | ||
) : ( | ||
current.toLocaleString() | ||
)} | ||
</span> | ||
<ChevronRight size={20} className="text-muted-foreground" /> | ||
</div> | ||
); | ||
}, | ||
}, | ||
]; | ||
|
||
export function AccountsSublist({ label, type, data, metric, meta }) { | ||
const router = useRouter(); | ||
const columnData: AccountMetricsRow[] = React.useMemo(() => { | ||
const nodes = data | ||
? [omit(data?.account, 'childrenAccounts'), ...(data?.account.childrenAccounts.nodes ?? [])] | ||
: []; | ||
const filteredNodes = nodes.filter(node => node.type === type); | ||
|
||
return filteredNodes.map(node => { | ||
const current = node[metric.id].current.valueInCents ?? node[metric.id].current; | ||
const comparison = node[metric.id].comparison?.valueInCents ?? node[metric.id].comparison; | ||
return { | ||
...node, | ||
current: Math.abs(current), | ||
comparison: !isNil(comparison) ? Math.abs(comparison) : undefined, | ||
percentageDifference: getPercentageDifference(current, comparison), | ||
}; | ||
}); | ||
}, [metric.id, data]); | ||
return ( | ||
<div className=""> | ||
<h2 className="mb-3 px-2 text-lg font-semibold text-slate-800">{label}</h2> | ||
<div className="flex flex-col divide-y overflow-hidden rounded-xl border bg-background"> | ||
{columnData | ||
.sort((a, b) => b.current - a.current) | ||
.map(account => ( | ||
<Link | ||
key={account.id} | ||
className="flex items-center justify-between px-4 py-4 hover:bg-muted" | ||
href={`/preview/${router.query.collectiveSlug}/finances/${account.slug}`} | ||
> | ||
<div>{account.name}</div> | ||
<div className="flex items-center gap-2"> | ||
<div className="font-medium"> | ||
<FormattedMoneyAmount | ||
amount={account.current} | ||
currency={meta.currency} | ||
precision={2} | ||
showCurrencyCode={false} | ||
/> | ||
</div> | ||
<ChevronRight size={20} className="text-muted-foreground" /> | ||
</div> | ||
</Link> | ||
))} | ||
</div> | ||
|
||
{/* <DataTable | ||
hideHeader | ||
className="bg-background" | ||
columns={columns} | ||
data={columnData} | ||
initialSort={[{ id: 'current', desc: true }]} | ||
nbPlaceholders={nbPlaceholders} | ||
loading={loading} | ||
meta={meta} | ||
/> */} | ||
</div> | ||
); | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,59 @@ | ||
import { useRouter } from 'next/router'; | ||
import React from 'react'; | ||
import Link from '../Link'; | ||
import { Slash } from 'lucide-react'; | ||
|
||
const getPathdata = (router, collective, account) => { | ||
switch (router.pathname) { | ||
case '/preview/[collectiveSlug]/finances/[accountSlug]': | ||
return [{ href: `/preview/${router.query.collectiveSlug}/finances`, label: 'Finances' }]; | ||
case '/preview/[collectiveSlug]/transactions/[groupId]': | ||
return [ | ||
{ href: `/preview/${router.query.collectiveSlug}/finances`, label: 'Finances' }, | ||
{ | ||
href: `/preview/${router.query.collectiveSlug}/finances/${router.query.collectiveSlug}`, | ||
label: collective?.name, | ||
}, | ||
]; | ||
case '/preview/[collectiveSlug]/[accountSlug]/transactions/[groupId]': | ||
return [ | ||
{ href: `/preview/${router.query.collectiveSlug}/finances`, label: 'Finances' }, | ||
{ | ||
href: `/preview/${router.query.collectiveSlug}/finances/${router.query.accountSlug}`, | ||
label: account?.name, | ||
}, | ||
]; | ||
case '/preview/[collectiveSlug]/projects/[accountSlug]': | ||
return [{ href: `/preview/${router.query.collectiveSlug}/projects`, label: 'Projects' }]; | ||
case '/preview/[collectiveSlug]/events/[accountSlug]': | ||
return [{ href: `/preview/${router.query.collectiveSlug}/events`, label: 'Events' }]; | ||
default: | ||
return [{ href: '', label: '' }]; | ||
} | ||
}; | ||
|
||
export function Breadcrumb({ breadcrumbs }) { | ||
return ( | ||
<div className="flex items-center gap-2 text-sm font-medium text-muted-foreground"> | ||
<Slash size={20} strokeWidth={1} /> | ||
|
||
{breadcrumbs?.map(({ href, label }, i, a) => { | ||
if (i === a.length - 1) { | ||
return ( | ||
<span key={href} className="p-1 text-foreground"> | ||
{label} | ||
</span> | ||
); | ||
} | ||
return ( | ||
<React.Fragment key={href}> | ||
<Link href={href} className="rounded p-1 hover:bg-muted hover:text-foreground"> | ||
<span className="text-muted-foreground">{label}</span> | ||
</Link> | ||
<Slash size={20} strokeWidth={1} /> | ||
</React.Fragment> | ||
); | ||
})} | ||
</div> | ||
); | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,40 @@ | ||
import React from 'react'; | ||
import { gql, useQuery } from '@apollo/client'; | ||
import { useRouter } from 'next/router'; | ||
import { API_V2_CONTEXT } from '../../lib/graphql/helpers'; | ||
import Link from '../Link'; | ||
import Avatar from '../Avatar'; | ||
|
||
const collectiveHeaderQuery = gql` | ||
query CollectiveHeader($slug: String!) { | ||
account(slug: $slug) { | ||
id | ||
slug | ||
name | ||
type | ||
... on AccountWithParent { | ||
parent { | ||
id | ||
slug | ||
name | ||
} | ||
} | ||
} | ||
} | ||
`; | ||
export function CollectiveHeader() { | ||
const router = useRouter(); | ||
const { data, loading } = useQuery(collectiveHeaderQuery, { | ||
variables: { slug: router.query.accountSlug }, | ||
context: API_V2_CONTEXT, | ||
}); | ||
return ( | ||
<div className="border-b bg-background"> | ||
<div className="mx-auto flex h-16 max-w-screen-xl items-center justify-between px-6"> | ||
<Link href={`/preview/${data?.account.parent?.slug || data?.account.slug}`} className="flex items-center gap-2"> | ||
<Avatar className="" collective={data?.account.parent || data?.account} /> <span>{data?.account.name}</span> | ||
</Link> | ||
</div> | ||
</div> | ||
); | ||
} |
Oops, something went wrong.