282 lines
9.5 KiB
React
282 lines
9.5 KiB
React
import React, { useState } from 'react';
|
|
import { motion } from 'framer-motion';
|
|
import {
|
|
Wallet,
|
|
TrendingDown,
|
|
PiggyBank,
|
|
AlertTriangle,
|
|
Pencil,
|
|
Check,
|
|
Plus,
|
|
ArrowUpRight,
|
|
} from 'lucide-react';
|
|
import ProgressBar from '../ui/ProgressBar';
|
|
import Button from '../ui/Button';
|
|
import { formatCurrency } from '../../utils/formatCurrency';
|
|
|
|
const BudgetOverview = ({
|
|
totalBudget = 0,
|
|
totalSpent = 0,
|
|
remainingBudget = 0,
|
|
percentUsed = 0,
|
|
isOverBudget = false,
|
|
currency = 'IDR',
|
|
onUpdateTotalBudget,
|
|
onOpenAddExpense,
|
|
}) => {
|
|
const [isEditingBudget, setIsEditingBudget] = useState(false);
|
|
const [budgetInput, setBudgetInput] = useState(totalBudget.toString());
|
|
|
|
const handleSaveBudget = (e) => {
|
|
e.preventDefault();
|
|
const val = Number(budgetInput) || 0;
|
|
onUpdateTotalBudget(val);
|
|
setIsEditingBudget(false);
|
|
};
|
|
|
|
const getRemainingStatus = () => {
|
|
if (isOverBudget) {
|
|
return { text: 'Over Budget!', color: 'var(--color-danger)', bg: 'var(--color-danger-bg)' };
|
|
}
|
|
if (percentUsed >= 80) {
|
|
return { text: 'Mendekati Batas', color: 'var(--color-warning)', bg: 'var(--color-warning-bg)' };
|
|
}
|
|
return { text: 'Aman & Terkendali', color: 'var(--color-success)', bg: 'var(--color-success-bg)' };
|
|
};
|
|
|
|
const remainingStatus = getRemainingStatus();
|
|
|
|
return (
|
|
<div style={{ marginBottom: 'var(--space-8)' }}>
|
|
{/* 3 Overview Stat Cards Grid */}
|
|
<div
|
|
style={{
|
|
display: 'grid',
|
|
gridTemplateColumns: 'repeat(auto-fit, minmax(260px, 1fr))',
|
|
gap: 'var(--space-4)',
|
|
marginBottom: 'var(--space-6)',
|
|
}}
|
|
>
|
|
{/* Card 1: Total Budget */}
|
|
<div
|
|
className="glass"
|
|
style={{
|
|
padding: 'var(--space-5) var(--space-6)',
|
|
borderRadius: 'var(--radius-xl)',
|
|
border: '1px solid var(--color-border)',
|
|
position: 'relative',
|
|
}}
|
|
>
|
|
<div style={{ display: 'flex', alignItems: 'center', justifyContent: 'space-between', marginBottom: 'var(--space-2)' }}>
|
|
<div style={{ fontSize: 'var(--text-xs)', textTransform: 'uppercase', color: 'var(--color-text-muted)', fontWeight: 700, letterSpacing: '0.04em' }}>
|
|
Target Total Budget
|
|
</div>
|
|
<button
|
|
onClick={() => {
|
|
setBudgetInput(totalBudget.toString());
|
|
setIsEditingBudget(!isEditingBudget);
|
|
}}
|
|
style={{
|
|
color: 'var(--color-primary-light)',
|
|
backgroundColor: 'hsla(220, 90%, 56%, 0.15)',
|
|
padding: '4px 8px',
|
|
borderRadius: 'var(--radius-md)',
|
|
fontSize: '11px',
|
|
fontWeight: 600,
|
|
display: 'flex',
|
|
alignItems: 'center',
|
|
gap: '4px',
|
|
}}
|
|
>
|
|
<Pencil size={12} /> {isEditingBudget ? 'Tutup' : 'Ubah'}
|
|
</button>
|
|
</div>
|
|
|
|
{isEditingBudget ? (
|
|
<form onSubmit={handleSaveBudget} style={{ display: 'flex', gap: 'var(--space-2)', marginTop: 'var(--space-2)' }}>
|
|
<input
|
|
type="number"
|
|
min="0"
|
|
step="100000"
|
|
className="input-field"
|
|
value={budgetInput}
|
|
onChange={(e) => setBudgetInput(e.target.value)}
|
|
autoFocus
|
|
style={{ padding: 'var(--space-2) var(--space-3)', fontSize: 'var(--text-sm)' }}
|
|
/>
|
|
<Button type="submit" variant="primary" size="sm" icon={Check}>
|
|
Simpan
|
|
</Button>
|
|
</form>
|
|
) : (
|
|
<div
|
|
style={{
|
|
fontSize: 'var(--text-2xl)',
|
|
fontWeight: 800,
|
|
color: '#ffffff',
|
|
fontFamily: 'var(--font-display)',
|
|
letterSpacing: '-0.02em',
|
|
}}
|
|
>
|
|
{formatCurrency(totalBudget, currency)}
|
|
</div>
|
|
)}
|
|
|
|
<div style={{ fontSize: 'var(--text-xs)', color: 'var(--color-text-secondary)', marginTop: 'var(--space-2)' }}>
|
|
Alokasi dana keseluruhan trip
|
|
</div>
|
|
</div>
|
|
|
|
{/* Card 2: Total Spent */}
|
|
<div
|
|
className="glass"
|
|
style={{
|
|
padding: 'var(--space-5) var(--space-6)',
|
|
borderRadius: 'var(--radius-xl)',
|
|
border: '1px solid var(--color-border)',
|
|
}}
|
|
>
|
|
<div style={{ display: 'flex', alignItems: 'center', justifyContent: 'space-between', marginBottom: 'var(--space-2)' }}>
|
|
<div style={{ fontSize: 'var(--text-xs)', textTransform: 'uppercase', color: 'var(--color-text-muted)', fontWeight: 700, letterSpacing: '0.04em' }}>
|
|
Total Terpakai
|
|
</div>
|
|
<div
|
|
style={{
|
|
backgroundColor: 'hsla(38, 95%, 55%, 0.15)',
|
|
color: 'var(--color-accent-light)',
|
|
padding: '2px 8px',
|
|
borderRadius: 'var(--radius-full)',
|
|
fontSize: '11px',
|
|
fontWeight: 700,
|
|
}}
|
|
>
|
|
{percentUsed}% Terpakai
|
|
</div>
|
|
</div>
|
|
|
|
<div
|
|
style={{
|
|
fontSize: 'var(--text-2xl)',
|
|
fontWeight: 800,
|
|
color: 'var(--color-accent-light)',
|
|
fontFamily: 'var(--font-display)',
|
|
letterSpacing: '-0.02em',
|
|
}}
|
|
>
|
|
{formatCurrency(totalSpent, currency)}
|
|
</div>
|
|
|
|
<div style={{ fontSize: 'var(--text-xs)', color: 'var(--color-text-secondary)', marginTop: 'var(--space-2)' }}>
|
|
Pengeluaran yang tercatat saat ini
|
|
</div>
|
|
</div>
|
|
|
|
{/* Card 3: Remaining Budget */}
|
|
<div
|
|
className="glass"
|
|
style={{
|
|
padding: 'var(--space-5) var(--space-6)',
|
|
borderRadius: 'var(--radius-xl)',
|
|
border: isOverBudget ? '1px solid var(--color-danger)' : '1px solid var(--color-border)',
|
|
boxShadow: isOverBudget ? '0 0 20px hsla(0, 75%, 55%, 0.25)' : 'none',
|
|
}}
|
|
>
|
|
<div style={{ display: 'flex', alignItems: 'center', justifyContent: 'space-between', marginBottom: 'var(--space-2)' }}>
|
|
<div style={{ fontSize: 'var(--text-xs)', textTransform: 'uppercase', color: 'var(--color-text-muted)', fontWeight: 700, letterSpacing: '0.04em' }}>
|
|
Sisa Budget
|
|
</div>
|
|
<div
|
|
style={{
|
|
backgroundColor: remainingStatus.bg,
|
|
color: remainingStatus.color,
|
|
padding: '2px 8px',
|
|
borderRadius: 'var(--radius-full)',
|
|
fontSize: '11px',
|
|
fontWeight: 700,
|
|
}}
|
|
>
|
|
{remainingStatus.text}
|
|
</div>
|
|
</div>
|
|
|
|
<div
|
|
style={{
|
|
fontSize: 'var(--text-2xl)',
|
|
fontWeight: 800,
|
|
color: isOverBudget ? 'var(--color-danger)' : 'var(--color-success)',
|
|
fontFamily: 'var(--font-display)',
|
|
letterSpacing: '-0.02em',
|
|
}}
|
|
>
|
|
{formatCurrency(remainingBudget, currency)}
|
|
</div>
|
|
|
|
<div style={{ fontSize: 'var(--text-xs)', color: 'var(--color-text-secondary)', marginTop: 'var(--space-2)' }}>
|
|
{isOverBudget ? 'Melebihi alokasi anggaran!' : 'Sisa saldo yang dapat dibelanjakan'}
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
{/* Progress & Alert Bar */}
|
|
<div
|
|
className="glass-dark"
|
|
style={{
|
|
padding: 'var(--space-5) var(--space-6)',
|
|
borderRadius: 'var(--radius-xl)',
|
|
border: '1px solid var(--color-border)',
|
|
}}
|
|
>
|
|
<div style={{ display: 'flex', alignItems: 'center', justifyContent: 'space-between', marginBottom: 'var(--space-3)' }}>
|
|
<div style={{ fontSize: 'var(--text-sm)', fontWeight: 600, color: 'var(--color-text-primary)' }}>
|
|
Persentase Anggaran Terpakai
|
|
</div>
|
|
<Button
|
|
variant="primary"
|
|
size="sm"
|
|
icon={Plus}
|
|
onClick={onOpenAddExpense}
|
|
>
|
|
Tambah Pengeluaran
|
|
</Button>
|
|
</div>
|
|
|
|
<ProgressBar
|
|
value={percentUsed}
|
|
showPercent={false}
|
|
height={10}
|
|
variant={isOverBudget ? 'danger' : percentUsed >= 80 ? 'warning' : 'primary'}
|
|
/>
|
|
|
|
{/* Warning Banner if Over Budget */}
|
|
{isOverBudget && (
|
|
<motion.div
|
|
initial={{ opacity: 0, y: 5 }}
|
|
animate={{ opacity: 1, y: 0 }}
|
|
style={{
|
|
marginTop: 'var(--space-4)',
|
|
padding: 'var(--space-3) var(--space-4)',
|
|
borderRadius: 'var(--radius-md)',
|
|
backgroundColor: 'var(--color-danger-bg)',
|
|
border: '1px solid hsla(0, 75%, 55%, 0.3)',
|
|
display: 'flex',
|
|
alignItems: 'center',
|
|
gap: 'var(--space-3)',
|
|
color: 'var(--color-danger)',
|
|
fontSize: 'var(--text-xs)',
|
|
fontWeight: 600,
|
|
}}
|
|
>
|
|
<AlertTriangle size={16} flexShrink={0} />
|
|
<span>
|
|
Perhatian: Pengeluaran Anda melebihi target anggaran sebesar{' '}
|
|
<strong>{formatCurrency(Math.abs(remainingBudget), currency)}</strong>. Evaluasi kembali pos belanja Anda.
|
|
</span>
|
|
</motion.div>
|
|
)}
|
|
</div>
|
|
</div>
|
|
);
|
|
};
|
|
|
|
export default BudgetOverview;
|