diff --git a/app/services/page.tsx b/app/services/page.tsx
new file mode 100644
index 0000000..aeff381
--- /dev/null
+++ b/app/services/page.tsx
@@ -0,0 +1,125 @@
+'use client'
+
+import { useState } from 'react'
+import { useFinanzasStore } from '@/lib/store'
+import { predictNextBill, calculateTrend } from '@/lib/predictions'
+import { formatCurrency } from '@/lib/utils'
+import { Zap, Droplets, Flame, Wifi, TrendingUp, TrendingDown, Plus, History } from 'lucide-react'
+import { cn } from '@/lib/utils'
+import { AddServiceModal } from '@/components/modals/AddServiceModal'
+
+const SERVICES = [
+ { id: 'electricity', label: 'Luz (Electricidad)', icon: Zap, color: 'text-yellow-400', bg: 'bg-yellow-400/10' },
+ { id: 'water', label: 'Agua', icon: Droplets, color: 'text-blue-400', bg: 'bg-blue-400/10' },
+ { id: 'gas', label: 'Gas', icon: Flame, color: 'text-orange-400', bg: 'bg-orange-400/10' },
+ { id: 'internet', label: 'Internet', icon: Wifi, color: 'text-cyan-400', bg: 'bg-cyan-400/10' },
+]
+
+export default function ServicesPage() {
+ const serviceBills = useFinanzasStore((state) => state.serviceBills)
+ const [isAddModalOpen, setIsAddModalOpen] = useState(false)
+
+ return (
+
+
+ {/* Header */}
+
+
+
Servicios y Predicciones
+
Gestiona tus consumos de Luz, Agua y Gas.
+
+
+
+
+ {/* Service Cards */}
+
+ {SERVICES.map((service) => {
+ const Icon = service.icon
+ const prediction = predictNextBill(serviceBills, service.id as any)
+ const trend = calculateTrend(serviceBills, service.id as any)
+ const lastBill = serviceBills
+ .filter(b => b.type === service.id)
+ .sort((a, b) => new Date(b.date).getTime() - new Date(a.date).getTime())[0]
+
+ return (
+
+
+
+
+
+ {trend !== 0 && (
+
0 ? "bg-red-500/10 text-red-400" : "bg-emerald-500/10 text-emerald-400")}>
+ {trend > 0 ? : }
+ {Math.abs(trend).toFixed(0)}%
+
+ )}
+
+
+
+
{service.label}
+
+
+ {formatCurrency(prediction || (lastBill?.amount ?? 0))}
+
+ {prediction > 0 && (est.)}
+
+
+ {lastBill
+ ? `Ăšltimo: ${formatCurrency(lastBill.amount)}`
+ : 'Sin historial'}
+
+
+
+ )
+ })}
+
+
+ {/* History List */}
+
+
+
+
Historial de Pagos
+
+
+ {serviceBills.length === 0 ? (
+
+ No hay facturas registradas. Comienza agregando una para ver predicciones.
+
+ ) : (
+ serviceBills
+ .sort((a, b) => new Date(b.date).getTime() - new Date(a.date).getTime())
+ .map((bill) => {
+ const service = SERVICES.find(s => s.id === bill.type)
+ const Icon = service?.icon || Zap
+
+ return (
+
+
+
+
+
+
+
{service?.label || bill.type}
+
{new Date(bill.date).toLocaleDateString('es-AR', { dateStyle: 'long' })}
+
+
+
+
{formatCurrency(bill.amount)}
+
{bill.period}
+
+
+ )
+ })
+ )}
+
+
+
+
setIsAddModalOpen(false)} />
+
+ )
+}
diff --git a/components/layout/Sidebar.tsx b/components/layout/Sidebar.tsx
index 695bd6d..eab3eaa 100644
--- a/components/layout/Sidebar.tsx
+++ b/components/layout/Sidebar.tsx
@@ -6,6 +6,7 @@ import {
CreditCard,
PiggyBank,
Bell,
+ Lightbulb,
X,
} from 'lucide-react';
import Link from 'next/link';
@@ -23,6 +24,7 @@ const navigationItems = [
{ name: 'Deudas', href: '/debts', icon: Wallet },
{ name: 'Tarjetas', href: '/cards', icon: CreditCard },
{ name: 'Presupuesto', href: '/budget', icon: PiggyBank },
+ { name: 'Servicios', href: '/services', icon: Lightbulb },
{ name: 'Alertas', href: '/alerts', icon: Bell, hasBadge: true },
];
@@ -88,10 +90,9 @@ export function Sidebar({
className={`
flex items-center gap-3 px-3 py-2.5 rounded-lg text-sm font-medium
transition-colors relative
- ${
- active
- ? 'bg-slate-800 text-emerald-400 border-l-2 border-emerald-500'
- : 'text-slate-300 hover:bg-slate-800 hover:text-slate-100'
+ ${active
+ ? 'bg-slate-800 text-emerald-400 border-l-2 border-emerald-500'
+ : 'text-slate-300 hover:bg-slate-800 hover:text-slate-100'
}
`}
>
diff --git a/components/modals/AddServiceModal.tsx b/components/modals/AddServiceModal.tsx
new file mode 100644
index 0000000..0aef630
--- /dev/null
+++ b/components/modals/AddServiceModal.tsx
@@ -0,0 +1,141 @@
+'use client'
+
+import { useState } from 'react'
+import { useFinanzasStore } from '@/lib/store'
+import { X, Calendar, Zap, Droplets, Flame, Wifi } from 'lucide-react'
+import { cn } from '@/lib/utils'
+
+interface AddServiceModalProps {
+ isOpen: boolean
+ onClose: () => void
+}
+
+const SERVICES = [
+ { id: 'electricity', label: 'Luz', icon: Zap, color: 'text-yellow-400' },
+ { id: 'water', label: 'Agua', icon: Droplets, color: 'text-blue-400' },
+ { id: 'gas', label: 'Gas', icon: Flame, color: 'text-orange-400' },
+ { id: 'internet', label: 'Internet', icon: Wifi, color: 'text-cyan-400' },
+]
+
+export function AddServiceModal({ isOpen, onClose }: AddServiceModalProps) {
+ const addServiceBill = useFinanzasStore((state) => state.addServiceBill)
+
+ const [type, setType] = useState('electricity')
+ const [amount, setAmount] = useState('')
+ const [period, setPeriod] = useState(new Date().toISOString().slice(0, 7)) // YYYY-MM
+ const [date, setDate] = useState(new Date().toISOString().split('T')[0])
+
+ if (!isOpen) return null
+
+ const handleSubmit = (e: React.FormEvent) => {
+ e.preventDefault()
+
+ if (!amount) return
+
+ addServiceBill({
+ type: type as any,
+ amount: parseFloat(amount),
+ date: new Date(date).toISOString(),
+ period: period,
+ notes: ''
+ })
+
+ // Reset
+ setAmount('')
+ onClose()
+ }
+
+ return (
+
+
+
+
+
Registrar Factura de Servicio
+
+
+
+
+
+
+ )
+}
diff --git a/lib/predictions.ts b/lib/predictions.ts
new file mode 100644
index 0000000..92d0329
--- /dev/null
+++ b/lib/predictions.ts
@@ -0,0 +1,61 @@
+import { ServiceBill } from '@/lib/types'
+
+/**
+ * Calculates the predicted amount for the next month based on historical data.
+ * Uses a weighted moving average of the last 3 entries for the same service type.
+ * Weights: 50% (most recent), 30% (previous), 20% (oldest).
+ */
+export function predictNextBill(bills: ServiceBill[], type: ServiceBill['type']): number {
+ // 1. Filter bills by type
+ const relevantBills = bills
+ .filter((b) => b.type === type)
+ .sort((a, b) => new Date(b.date).getTime() - new Date(a.date).getTime()) // Newest first
+
+ if (relevantBills.length === 0) return 0
+
+ // 2. Take up to 3 most recent bills
+ const recent = relevantBills.slice(0, 3)
+
+ // 3. Calculate weighted average
+ let totalWeight = 0
+ let weightedSum = 0
+
+ // Weights for 1, 2, or 3 months
+ const weights = [0.5, 0.3, 0.2]
+
+ recent.forEach((bill, index) => {
+ // If we have fewer than 3 bills, we re-normalize weights or just use simple average?
+ // Let's stick to the weights but normalize if unmatched.
+ // Actually, simple approach:
+ // 1 bill: 100%
+ // 2 bills: 62.5% / 37.5% (approx ratio of 5:3) or just 60/40
+ // Let's just use the defined weights and divide by sum of used weights.
+
+ const w = weights[index]
+ weightedSum += bill.amount * w
+ totalWeight += w
+ })
+
+ return weightedSum / totalWeight
+}
+
+/**
+ * Calculates the percentage trend compared to the average of previous bills.
+ * Positive = Spending more. Negative = Spending less.
+ */
+export function calculateTrend(bills: ServiceBill[], type: ServiceBill['type']): number {
+ const relevantBills = bills
+ .filter((b) => b.type === type)
+ .sort((a, b) => new Date(b.date).getTime() - new Date(a.date).getTime())
+
+ if (relevantBills.length < 2) return 0
+
+ const latest = relevantBills[0].amount
+ const previous = relevantBills.slice(1, 4) // Average of up to 3 previous bills
+
+ if (previous.length === 0) return 0
+
+ const avgPrevious = previous.reduce((sum, b) => sum + b.amount, 0) / previous.length
+
+ return ((latest - avgPrevious) / avgPrevious) * 100
+}
diff --git a/lib/store.ts b/lib/store.ts
index f66bce9..cf22a0d 100644
--- a/lib/store.ts
+++ b/lib/store.ts
@@ -6,9 +6,11 @@ import { createCardsSlice, CardsSlice } from './store/slices/cardsSlice'
import { createBudgetSlice, BudgetSlice } from './store/slices/budgetSlice'
import { createAlertsSlice, AlertsSlice } from './store/slices/alertsSlice'
+import { createServicesSlice, ServicesSlice } from './store/slices/servicesSlice'
+
// Combined State Interface
// Note: We extend the individual slices to create the full store interface
-export interface FinanzasState extends DebtsSlice, CardsSlice, BudgetSlice, AlertsSlice { }
+export interface FinanzasState extends DebtsSlice, CardsSlice, BudgetSlice, AlertsSlice, ServicesSlice { }
export const useFinanzasStore = create()(
persist(
@@ -17,6 +19,7 @@ export const useFinanzasStore = create()(
...createCardsSlice(...a),
...createBudgetSlice(...a),
...createAlertsSlice(...a),
+ ...createServicesSlice(...a),
}),
{
name: 'finanzas-storage',
diff --git a/lib/store/slices/servicesSlice.ts b/lib/store/slices/servicesSlice.ts
new file mode 100644
index 0000000..f6bec4c
--- /dev/null
+++ b/lib/store/slices/servicesSlice.ts
@@ -0,0 +1,35 @@
+import { StateCreator } from 'zustand'
+import { v4 as uuidv4 } from 'uuid'
+import { ServiceBill } from '@/lib/types'
+
+export interface ServicesSlice {
+ serviceBills: ServiceBill[]
+
+ addServiceBill: (bill: Omit) => void
+ deleteServiceBill: (id: string) => void
+ toggleServiceBillPaid: (id: string) => void
+}
+
+export const createServicesSlice: StateCreator = (set) => ({
+ serviceBills: [],
+
+ addServiceBill: (bill) =>
+ set((state) => ({
+ serviceBills: [
+ ...state.serviceBills,
+ { ...bill, id: uuidv4(), isPaid: false },
+ ],
+ })),
+
+ deleteServiceBill: (id) =>
+ set((state) => ({
+ serviceBills: state.serviceBills.filter((b) => b.id !== id),
+ })),
+
+ toggleServiceBillPaid: (id) =>
+ set((state) => ({
+ serviceBills: state.serviceBills.map((b) =>
+ b.id === id ? { ...b, isPaid: !b.isPaid } : b
+ ),
+ })),
+})
diff --git a/lib/types.ts b/lib/types.ts
index 4374d53..b369283 100644
--- a/lib/types.ts
+++ b/lib/types.ts
@@ -62,12 +62,23 @@ export interface Alert {
relatedId?: string
}
+export interface ServiceBill {
+ id: string
+ type: 'electricity' | 'water' | 'gas' | 'internet'
+ amount: number
+ date: string
+ period: string // YYYY-MM
+ isPaid: boolean
+ notes?: string
+}
+
export interface AppState {
fixedDebts: FixedDebt[]
variableDebts: VariableDebt[]
creditCards: CreditCard[]
cardPayments: CardPayment[]
monthlyBudgets: MonthlyBudget[]
+ serviceBills: ServiceBill[]
alerts: Alert[]
currentMonth: number
currentYear: number