feat: initial NFC Attendance Hub implementation

- 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
This commit is contained in:
Nemo
2026-03-12 08:49:13 +00:00
parent bb3f8d0ef2
commit b273f1a573
53 changed files with 6000 additions and 2 deletions

32
lib/utils.ts Normal file
View File

@@ -0,0 +1,32 @@
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
}