This takes part of the work being done in #96 that was reverted but still useful. Note Tailwind and Fresh weren't upgraded because there's no security vulnerability in either, and I have found the new versions to be worse in performance. Thos will likely stay at those fixed versions going forward.
139 lines
5.0 KiB
TypeScript
139 lines
5.0 KiB
TypeScript
import { useSignal } from '@preact/signals';
|
|
import { useEffect, useRef } from 'preact/hooks';
|
|
import { Chart } from 'chart.js/auto';
|
|
|
|
import { formatNumber } from '/lib/utils/misc.ts';
|
|
import { Budget, SupportedCurrencySymbol } from '/lib/types.ts';
|
|
|
|
interface ListBudgetsProps {
|
|
budgets: Budget[];
|
|
month: string;
|
|
currency: SupportedCurrencySymbol;
|
|
onClickEditBudget: (budgetId: string) => void;
|
|
}
|
|
|
|
export default function ListBudgets(
|
|
{
|
|
budgets,
|
|
month,
|
|
currency,
|
|
onClickEditBudget,
|
|
}: ListBudgetsProps,
|
|
) {
|
|
const view = useSignal<'list' | 'chart'>('list');
|
|
const chartRef = useRef<HTMLCanvasElement>(null);
|
|
|
|
// Calculate a total budget to show before all others
|
|
const totalBudget: Omit<Budget, 'user_id' | 'created_at'> = {
|
|
id: 'total',
|
|
name: 'Total',
|
|
month,
|
|
value: budgets.reduce((accumulatedValue, budget) => accumulatedValue + budget.value, 0),
|
|
extra: {
|
|
usedValue: budgets.reduce((accumulatedValue, budget) => accumulatedValue + budget.extra.usedValue, 0),
|
|
availableValue: budgets.reduce((accumulatedValue, budget) => accumulatedValue + budget.extra.availableValue, 0),
|
|
},
|
|
};
|
|
|
|
function swapView(newView: 'list' | 'chart') {
|
|
view.value = view.value === newView ? 'list' : newView;
|
|
}
|
|
|
|
useEffect(() => {
|
|
if (view.value === 'chart') {
|
|
const budgetColors = [totalBudget, ...budgets].map((_, index) =>
|
|
index === 0
|
|
? 'rgba(59, 130, 246, 0.8)'
|
|
: `hsl(${(index - 1) * (360 / budgets.length)}, ${index % 2 ? 85 : 70}%, ${index % 2 ? 55 : 65}%, 0.8)`
|
|
);
|
|
|
|
new Chart(chartRef.current as HTMLCanvasElement, {
|
|
type: 'doughnut',
|
|
data: {
|
|
labels: [{ name: 'Available' }, ...budgets].map((budget) => budget.name),
|
|
datasets: [{
|
|
label: '',
|
|
data: [totalBudget, ...budgets].map((budget) =>
|
|
budget.id === 'total' ? budget.extra.availableValue : budget.extra.usedValue
|
|
),
|
|
backgroundColor: budgetColors,
|
|
borderWidth: 1,
|
|
borderColor: '#222',
|
|
}],
|
|
},
|
|
options: {
|
|
backgroundColor: '#222',
|
|
plugins: {
|
|
legend: {
|
|
position: 'bottom',
|
|
fullSize: true,
|
|
labels: {
|
|
usePointStyle: true,
|
|
pointStyle: 'circle',
|
|
padding: 10,
|
|
},
|
|
},
|
|
},
|
|
},
|
|
});
|
|
}
|
|
}, [view.value]);
|
|
|
|
return (
|
|
<section class='mx-auto max-w-7xl my-8'>
|
|
{budgets.length === 0
|
|
? (
|
|
<article class='px-6 py-4 font-normal text-center w-full'>
|
|
<div class='font-medium text-slate-400 text-md'>No budgets to show for {month}</div>
|
|
</article>
|
|
)
|
|
: (
|
|
<section class='w-full flex flex-wrap gap-4 justify-center items-center'>
|
|
{view.value === 'list'
|
|
? [totalBudget, ...budgets].map((budget) => {
|
|
let backgroundColorClass = 'bg-green-600';
|
|
let usedValuePercentage = Math.ceil(100 * budget.extra.usedValue / budget.value);
|
|
|
|
if (usedValuePercentage >= 100) {
|
|
usedValuePercentage = 100;
|
|
backgroundColorClass = 'bg-red-600';
|
|
}
|
|
|
|
return (
|
|
<div
|
|
onClick={() => budget.id === 'total' ? swapView('chart') : onClickEditBudget(budget.id)}
|
|
class='flex w-full md:w-auto max-w-sm gap-y-4 gap-x-4 rounded shadow-md bg-slate-700 relative cursor-pointer py-4 px-6 hover:opacity-80'
|
|
>
|
|
<article class='order-first tracking-tight flex flex-col text-base mr-4'>
|
|
<span class='font-bold text-lg' title='Amount used from budgeted amount'>
|
|
{formatNumber(currency, budget.extra.usedValue)} of {formatNumber(currency, budget.value)}
|
|
</span>
|
|
<span
|
|
class='bg-gray-600 h-1.5 w-full block rounded-full mt-2 mx-0'
|
|
title={`${usedValuePercentage}% of budget used`}
|
|
>
|
|
<span
|
|
class={`${backgroundColorClass} w-0 block h-1.5 rounded-full`}
|
|
style={{ width: `${usedValuePercentage}%` }}
|
|
>
|
|
</span>
|
|
</span>
|
|
<span class='mt-2 font-normal text-gray-400'>{budget.name}</span>
|
|
</article>
|
|
<span class='text-lg text-right text-gray-200' title='Amount available from budgeted amount'>
|
|
{formatNumber(currency, budget.extra.availableValue)}
|
|
</span>
|
|
</div>
|
|
);
|
|
})
|
|
: (
|
|
<section class='p-4 rounded-lg shadow-sm cursor-pointer' onClick={() => swapView('list')}>
|
|
<canvas ref={chartRef}></canvas>
|
|
</section>
|
|
)}
|
|
</section>
|
|
)}
|
|
</section>
|
|
);
|
|
}
|