feat: add services module with AI predictions
Added comprehensive services management with intelligent predictions: - New Services page (/services) with Luz, Agua, Gas, Internet tracking - AI-powered bill prediction based on historical data - Trend analysis (up/down percentage) for consumption patterns - Interactive service cards with icons and visual indicators - Complete payment history with period tracking - AddServiceModal for registering new bills - ServiceBill type definition with period tracking (YYYY-MM) - Services slice in Zustand store - Predictions engine using historical data analysis 🤖 Generated with [Claude Code](https://claude.com/claude-code)
This commit is contained in:
61
lib/predictions.ts
Normal file
61
lib/predictions.ts
Normal file
@@ -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
|
||||
}
|
||||
@@ -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<FinanzasState>()(
|
||||
persist(
|
||||
@@ -17,6 +19,7 @@ export const useFinanzasStore = create<FinanzasState>()(
|
||||
...createCardsSlice(...a),
|
||||
...createBudgetSlice(...a),
|
||||
...createAlertsSlice(...a),
|
||||
...createServicesSlice(...a),
|
||||
}),
|
||||
{
|
||||
name: 'finanzas-storage',
|
||||
|
||||
35
lib/store/slices/servicesSlice.ts
Normal file
35
lib/store/slices/servicesSlice.ts
Normal file
@@ -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<ServiceBill, 'id' | 'isPaid'>) => void
|
||||
deleteServiceBill: (id: string) => void
|
||||
toggleServiceBillPaid: (id: string) => void
|
||||
}
|
||||
|
||||
export const createServicesSlice: StateCreator<ServicesSlice> = (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
|
||||
),
|
||||
})),
|
||||
})
|
||||
11
lib/types.ts
11
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
|
||||
|
||||
Reference in New Issue
Block a user