Refactor: Implement DashboardLayout, fix mobile nav, and resolve scroll issues

This commit is contained in:
ren
2026-01-29 14:41:46 +01:00
parent 0a04e0817d
commit 811c78ffa5
171 changed files with 1678 additions and 23983 deletions

View File

@@ -1,142 +1,145 @@
'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 (
<div className="space-y-6">
{/* Header */}
<div className="flex flex-col sm:flex-row sm:items-center justify-between gap-4">
<div>
<h1 className="text-2xl font-bold text-white">Servicios y Predicciones</h1>
<p className="text-slate-400 text-sm">Gestiona tus consumos de Luz, Agua y Gas.</p>
</div>
<button
onClick={() => setIsAddModalOpen(true)}
className="flex items-center gap-2 px-4 py-2 bg-cyan-500 hover:bg-cyan-400 text-white rounded-lg transition shadow-lg shadow-cyan-500/20 font-medium self-start sm:self-auto"
>
<Plus size={18} /> Nuevo Pago
</button>
</div>
{/* Service Cards */}
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-4 gap-4">
{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 (
<div key={service.id} className="bg-slate-900 border border-slate-800 rounded-xl p-5 space-y-4">
<div className="flex items-center justify-between">
<div className={cn("p-2 rounded-lg", service.bg)}>
<Icon className={cn("w-6 h-6", service.color)} />
</div>
{trend !== 0 && (
<div className={cn("flex items-center gap-1 text-xs font-medium px-2 py-1 rounded-full", trend > 0 ? "bg-red-500/10 text-red-400" : "bg-emerald-500/10 text-emerald-400")}>
{trend > 0 ? <TrendingUp size={12} /> : <TrendingDown size={12} />}
{Math.abs(trend).toFixed(0)}%
</div>
)}
</div>
<div>
<p className="text-slate-400 text-sm font-medium">{service.label}</p>
<div className="flex items-baseline gap-2">
<h3 className="text-2xl font-bold text-white mt-1">
{formatCurrency(prediction || (lastBill?.amount ?? 0))}
</h3>
{prediction > 0 && <span className="text-xs text-slate-500 font-mono">(est.)</span>}
</div>
<p className="text-xs text-slate-500 mt-1">
{lastBill
? `Último: ${formatCurrency(lastBill.amount)}`
: 'Sin historial'}
</p>
</div>
</div>
)
})}
</div>
{/* History List */}
<div className="bg-slate-900 border border-slate-800 rounded-xl overflow-hidden">
<div className="p-5 border-b border-slate-800 flex items-center gap-2">
<History size={18} className="text-slate-400" />
<h3 className="text-lg font-semibold text-white">Historial de Pagos</h3>
</div>
<div className="divide-y divide-slate-800">
{serviceBills.length === 0 ? (
<div className="p-8 text-center text-slate-500 text-sm">
No hay facturas registradas. Comienza agregando una para ver predicciones.
</div>
) : (
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 (
<div key={bill.id} className="p-4 flex items-center justify-between hover:bg-slate-800/50 transition-colors">
<div className="flex items-center gap-4">
<div className={cn("p-2 rounded-lg", service?.bg || 'bg-slate-800')}>
<Icon className={cn("w-5 h-5", service?.color || 'text-slate-400')} />
</div>
<div>
<p className="text-white font-medium capitalize">{service?.label || bill.type}</p>
<div className="flex flex-col sm:flex-row sm:items-center gap-1 sm:gap-2">
<p className="text-xs text-slate-500 capitalize">{new Date(bill.date).toLocaleDateString('es-AR', { dateStyle: 'long' })}</p>
{bill.usage && (
<>
<span className="hidden sm:inline text-slate-700"></span>
<p className="text-xs text-slate-400">
Consumo: <span className="text-slate-300 font-medium">{bill.usage} {bill.unit}</span>
</p>
</>
)}
</div>
</div>
</div>
<div className="text-right">
<p className="text-white font-mono font-medium">{formatCurrency(bill.amount)}</p>
<div className="flex flex-col items-end">
<p className="text-xs text-slate-500 uppercase">{bill.period}</p>
{bill.usage && bill.amount && (
<p className="text-[10px] text-cyan-500/80 font-mono">
{formatCurrency(bill.amount / bill.usage)} / {bill.unit}
</p>
)}
</div>
</div>
</div>
)
})
)}
</div>
</div>
<AddServiceModal isOpen={isAddModalOpen} onClose={() => setIsAddModalOpen(false)} />
</div>
)
}
'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'
import { DashboardLayout } from '@/components/layout/DashboardLayout'
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 (
<DashboardLayout title="Servicios">
<div className="space-y-6">
{/* Header */}
<div className="flex flex-col sm:flex-row sm:items-center justify-between gap-4">
<div>
<h2 className="text-2xl font-bold text-white">Servicios y Predicciones</h2>
<p className="text-slate-400 text-sm">Gestiona tus consumos de Luz, Agua y Gas.</p>
</div>
<button
onClick={() => setIsAddModalOpen(true)}
className="flex items-center gap-2 px-4 py-2 bg-cyan-500 hover:bg-cyan-400 text-white rounded-lg transition shadow-lg shadow-cyan-500/20 font-medium self-start sm:self-auto"
>
<Plus size={18} /> Nuevo Pago
</button>
</div>
{/* Service Cards */}
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-4 gap-4">
{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 (
<div key={service.id} className="bg-slate-900 border border-slate-800 rounded-xl p-5 space-y-4">
<div className="flex items-center justify-between">
<div className={cn("p-2 rounded-lg", service.bg)}>
<Icon className={cn("w-6 h-6", service.color)} />
</div>
{trend !== 0 && (
<div className={cn("flex items-center gap-1 text-xs font-medium px-2 py-1 rounded-full", trend > 0 ? "bg-red-500/10 text-red-400" : "bg-emerald-500/10 text-emerald-400")}>
{trend > 0 ? <TrendingUp size={12} /> : <TrendingDown size={12} />}
{Math.abs(trend).toFixed(0)}%
</div>
)}
</div>
<div>
<p className="text-slate-400 text-sm font-medium">{service.label}</p>
<div className="flex items-baseline gap-2">
<h3 className="text-2xl font-bold text-white mt-1">
{formatCurrency(prediction || (lastBill?.amount ?? 0))}
</h3>
{prediction > 0 && <span className="text-xs text-slate-500 font-mono">(est.)</span>}
</div>
<p className="text-xs text-slate-500 mt-1">
{lastBill
? `Último: ${formatCurrency(lastBill.amount)}`
: 'Sin historial'}
</p>
</div>
</div>
)
})}
</div>
{/* History List */}
<div className="bg-slate-900 border border-slate-800 rounded-xl overflow-hidden">
<div className="p-5 border-b border-slate-800 flex items-center gap-2">
<History size={18} className="text-slate-400" />
<h3 className="text-lg font-semibold text-white">Historial de Pagos</h3>
</div>
<div className="divide-y divide-slate-800">
{serviceBills.length === 0 ? (
<div className="p-8 text-center text-slate-500 text-sm">
No hay facturas registradas. Comienza agregando una para ver predicciones.
</div>
) : (
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 (
<div key={bill.id} className="p-4 flex items-center justify-between hover:bg-slate-800/50 transition-colors">
<div className="flex items-center gap-4">
<div className={cn("p-2 rounded-lg", service?.bg || 'bg-slate-800')}>
<Icon className={cn("w-5 h-5", service?.color || 'text-slate-400')} />
</div>
<div>
<p className="text-white font-medium capitalize">{service?.label || bill.type}</p>
<div className="flex flex-col sm:flex-row sm:items-center gap-1 sm:gap-2">
<p className="text-xs text-slate-500 capitalize">{new Date(bill.date).toLocaleDateString('es-AR', { dateStyle: 'long' })}</p>
{bill.usage && (
<>
<span className="hidden sm:inline text-slate-700"></span>
<p className="text-xs text-slate-400">
Consumo: <span className="text-slate-300 font-medium">{bill.usage} {bill.unit}</span>
</p>
</>
)}
</div>
</div>
</div>
<div className="text-right">
<p className="text-white font-mono font-medium">{formatCurrency(bill.amount)}</p>
<div className="flex flex-col items-end">
<p className="text-xs text-slate-500 uppercase">{bill.period}</p>
{bill.usage && bill.amount && (
<p className="text-[10px] text-cyan-500/80 font-mono">
{formatCurrency(bill.amount / bill.usage)} / {bill.unit}
</p>
)}
</div>
</div>
</div>
)
})
)}
</div>
</div>
<AddServiceModal isOpen={isAddModalOpen} onClose={() => setIsAddModalOpen(false)} />
</div>
</DashboardLayout>
)
}