Phase 2: Database e API Layer

- Configurado Prisma 7 com multiSchema para staging database
- Models: Site, GA4DailyTraffic, GA4TrafficSources, GSCSearchPerformance
- Created lib utilities (prisma.ts, utils.ts) com formatação PT-PT
- API routes implementadas:
  * GET /api/sites - lista sites activos
  * GET /api/metrics/[siteId]?period=30d - métricas agregadas + charts
  * GET /api/health - health check com conexão DB

Métricas incluem:
- KPIs: visitors, sessions, pageViews, newUsers com % change
- Charts: dailyTraffic, trafficSources, topQueries (GSC)
- Comparação período anterior para trends

Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
This commit is contained in:
2026-02-13 17:56:56 +00:00
parent 01353cef36
commit b99baa1200
7 changed files with 435 additions and 0 deletions

15
src/lib/prisma.ts Normal file
View File

@@ -0,0 +1,15 @@
import { PrismaClient } from '@prisma/client'
const globalForPrisma = globalThis as unknown as {
prisma: PrismaClient | undefined
}
export const prisma =
globalForPrisma.prisma ??
new PrismaClient({
log: process.env.NODE_ENV === 'development' ? ['query', 'error', 'warn'] : ['error'],
})
if (process.env.NODE_ENV !== 'production') {
globalForPrisma.prisma = prisma
}

58
src/lib/utils.ts Normal file
View File

@@ -0,0 +1,58 @@
import { type ClassValue, clsx } from 'clsx'
import { twMerge } from 'tailwind-merge'
/**
* Merge Tailwind CSS classes with proper precedence
*/
export function cn(...inputs: ClassValue[]) {
return twMerge(clsx(inputs))
}
/**
* Format number with Portuguese locale
* @example formatNumber(1234567) => "1.234.567"
*/
export function formatNumber(num: number): string {
return new Intl.NumberFormat('pt-PT').format(num)
}
/**
* Format number as percentage
* @example formatPercent(0.1234) => "12,3%"
*/
export function formatPercent(num: number, decimals: number = 1): string {
return `${(num * 100).toFixed(decimals)}%`
}
/**
* Format number as compact (K, M, B)
* @example formatCompact(1234567) => "1,2M"
*/
export function formatCompact(num: number): string {
return new Intl.NumberFormat('pt-PT', {
notation: 'compact',
compactDisplay: 'short',
}).format(num)
}
/**
* Format date as Portuguese short date
* @example formatDate(new Date()) => "13/02/2026"
*/
export function formatDate(date: Date | string): string {
const d = typeof date === 'string' ? new Date(date) : date
return new Intl.DateTimeFormat('pt-PT', {
day: '2-digit',
month: '2-digit',
year: 'numeric',
}).format(d)
}
/**
* Calculate percentage change
* @example calculateChange(100, 120) => 20
*/
export function calculateChange(oldValue: number, newValue: number): number {
if (oldValue === 0) return 0
return ((newValue - oldValue) / oldValue) * 100
}