Refactor: Implement DashboardLayout, fix mobile nav, and resolve scroll issues
This commit is contained in:
@@ -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>
|
||||
)
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user