- Multi-tenant client management with unique API keys - Semaphore SMS integration (per-client key + sender name + credit balance) - DB-based SMS queue with 3-attempt retry and exponential backoff - Failed SMS dashboard with manual retry button - Reports page with date/client filter and CSV export - Admin auth via NextAuth (email/password) - Docker Compose setup (app + PostgreSQL 16) - Prisma 5 schema with SmsQueue, SmsLog, Client, User models Tech: Next.js 14 App Router + TypeScript + Tailwind CSS + Prisma + PostgreSQL
33 lines
919 B
TypeScript
33 lines
919 B
TypeScript
import { type ClassValue, clsx } from 'clsx'
|
|
import { twMerge } from 'tailwind-merge'
|
|
|
|
export function cn(...inputs: ClassValue[]) {
|
|
return twMerge(clsx(inputs))
|
|
}
|
|
|
|
export function generateApiKey(): string {
|
|
const chars = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789'
|
|
let result = 'nfc_'
|
|
for (let i = 0; i < 32; i++) {
|
|
result += chars.charAt(Math.floor(Math.random() * chars.length))
|
|
}
|
|
return result
|
|
}
|
|
|
|
export function slugify(text: string): string {
|
|
return text
|
|
.toLowerCase()
|
|
.replace(/[^a-z0-9]+/g, '-')
|
|
.replace(/(^-|-$)/g, '')
|
|
}
|
|
|
|
export function formatPhone(phone: string): string {
|
|
const cleaned = phone.replace(/\D/g, '')
|
|
if (cleaned.startsWith('0') && cleaned.length === 11) {
|
|
return '63' + cleaned.slice(1)
|
|
}
|
|
if (cleaned.startsWith('63') && cleaned.length === 12) return cleaned
|
|
if (cleaned.length === 10) return '63' + cleaned
|
|
return cleaned
|
|
}
|