Commit 160edf33 authored by Hendrik Garske's avatar Hendrik Garske

feat: Prisma entfernt, PostgreSQL mit pg implementiert, Favicon übernommen

parent 6951ce44
Pipeline #20 failed with stages
in 26 seconds
......@@ -18,7 +18,6 @@ before_script:
build:
stage: build
script:
- npx prisma generate
- npm run build
artifacts:
paths:
......
......@@ -15,11 +15,8 @@ WORKDIR /app
COPY --from=deps /app/node_modules ./node_modules
COPY . .
# Generate Prisma Client
ENV NEXT_TELEMETRY_DISABLED 1
RUN npx prisma generate
# Build Next.js
ENV NEXT_TELEMETRY_DISABLED 1
RUN npm run build
# Production image, copy all the files and run next
......@@ -39,10 +36,6 @@ COPY --from=builder /app/public ./public
COPY --from=builder --chown=nextjs:nodejs /app/.next/standalone ./
COPY --from=builder --chown=nextjs:nodejs /app/.next/static ./.next/static
# Copy Prisma Client files (required for database access)
COPY --from=builder --chown=nextjs:nodejs /app/node_modules/.prisma ./node_modules/.prisma
COPY --from=builder --chown=nextjs:nodejs /app/node_modules/@prisma ./node_modules/@prisma
USER nextjs
EXPOSE 3000
......
......@@ -6,7 +6,7 @@
[![Next.js](https://img.shields.io/badge/Next.js-16.1.1-black)](https://nextjs.org/)
[![TypeScript](https://img.shields.io/badge/TypeScript-5.0-blue)](https://www.typescriptlang.org/)
[![Prisma](https://img.shields.io/badge/Prisma-6.19-2D3748)](https://www.prisma.io/)
[![PostgreSQL](https://img.shields.io/badge/PostgreSQL-15+-336791)](https://www.postgresql.org/)
[![PostgreSQL](https://img.shields.io/badge/PostgreSQL-Latest-336791)](https://www.postgresql.org/)
[![Docker](https://img.shields.io/badge/Docker-Ready-2496ED)](https://www.docker.com/)
......@@ -58,7 +58,7 @@ Das CoreX Management Dashboard ist eine interne Webanwendung zur Verwaltung von
- **Sprache**: [TypeScript 5.0](https://www.typescriptlang.org/)
- **Styling**: [Tailwind CSS v4](https://tailwindcss.com/)
- **Datenbank**: [PostgreSQL](https://www.postgresql.org/)
- **ORM**: [Prisma 6.19](https://www.prisma.io/)
- **Database**: [PostgreSQL](https://www.postgresql.org/) mit [node-postgres (pg)](https://node-postgres.com/)
- **UI Components**:
- [Radix UI](https://www.radix-ui.com/)
- [Lucide Icons](https://lucide.dev/)
......@@ -195,23 +195,22 @@ GRANT ALL PRIVILEGES ON DATABASE corex_dashboard TO corex_user;
DATABASE_URL="postgresql://corex_user:ihr_sicheres_passwort@localhost:5432/corex_dashboard?schema=public"
```
### Migrationen ausführen
### Datenbank-Schema initialisieren
```bash
# Initiale Migration
npx prisma migrate dev --name init
# Weitere Migrationen (nach Schema-Änderungen)
npx prisma migrate dev --name beschreibung_der_aenderung
```
### Prisma Studio (Datenbank-Viewer)
Führen Sie das SQL-Skript aus, um die Tabellen zu erstellen:
```bash
npx prisma studio
# Mit psql
psql -U corex_user -d corex_dashboard -f scripts/init-db.sql
# Oder manuell die SQL-Datei ausführen
```
Öffnet einen Browser-basierten Datenbank-Viewer auf [http://localhost:5555](http://localhost:5555)
Die SQL-Datei befindet sich in `scripts/init-db.sql` und erstellt:
- `users` Tabelle
- `customers` Tabelle
- `time_entries` Tabelle
- Indizes und Trigger für automatische `updatedAt` Updates
## 💻 Entwicklung
......@@ -230,17 +229,8 @@ npm run start
# ESLint ausführen
npm run lint
# Prisma Client generieren
npx prisma generate
# Neue Migration erstellen
npx prisma migrate dev --name migration_name
# Datenbank zurücksetzen (⚠️ löscht alle Daten)
npx prisma migrate reset
# Prisma Studio öffnen
npx prisma studio
# Datenbank-Verbindung testen
# Die Verbindung wird automatisch beim Start der App getestet
```
### Entwicklungsworkflow
......@@ -301,11 +291,13 @@ corex-dashboard/
│ ├── theme-provider.tsx # Theme-Provider
│ └── theme-toggle.tsx # Theme-Toggle Button
├── lib/ # Utility-Funktionen
│ ├── prisma.ts # Prisma Client Singleton
│ ├── db.ts # PostgreSQL Connection Pool
│ ├── utils/ # Helper-Funktionen
│ │ ├── db-helpers.ts # Datenbank-Helper
│ │ └── ...
│ └── utils.ts # Helper-Funktionen (cn, etc.)
├── prisma/ # Prisma Konfiguration
│ ├── migrations/ # Datenbank-Migrationen
│ └── schema.prisma # Datenbank-Schema
├── scripts/ # Skripte
│ └── init-db.sql # Datenbank-Initialisierung
├── public/ # Statische Assets
│ ├── branding/ # Logo-Varianten
│ └── logo.png # Hauptlogo
......@@ -503,7 +495,7 @@ CMD ["node", "server.js"]
Das Projekt verwendet GitLab CI/CD für automatische Builds und Tests.
**Pipeline Stages:**
1. **Build**: Kompiliert TypeScript, generiert Prisma Client
1. **Build**: Kompiliert TypeScript und erstellt Production Build
2. **Test**: Führt ESLint aus
> ⚠️ **Privates Repository**: CI/CD ist nur für autorisierte GitLab-Nutzer verfügbar.
......
import { NextRequest, NextResponse } from "next/server"
import { prisma } from "@/lib/prisma"
import { query } from "@/lib/db"
import { rowToCustomer } from "@/lib/utils/db-helpers"
export async function GET(
request: NextRequest,
......@@ -7,25 +8,49 @@ export async function GET(
) {
try {
const { id } = await params
const customer = await prisma.customer.findUnique({
where: { id },
include: {
timeEntries: {
include: {
user: {
select: { name: true, email: true },
},
},
orderBy: { createdAt: "desc" },
},
},
})
if (!customer) {
const customerResult = await query(
`SELECT * FROM customers WHERE id = $1`,
[id]
)
if (customerResult.rows.length === 0) {
return NextResponse.json({ error: "Customer not found" }, { status: 404 })
}
return NextResponse.json(customer)
const customer = rowToCustomer(customerResult.rows[0])
// Get time entries with user info
const timeEntriesResult = await query(
`SELECT te.*,
json_build_object(
'name', u.name,
'email', u.email
) as user
FROM time_entries te
LEFT JOIN users u ON u.id = te."userId"
WHERE te."customerId" = $1
ORDER BY te."createdAt" DESC`,
[id]
)
const timeEntries = timeEntriesResult.rows.map((row: { user: unknown }) => ({
id: row.id,
description: row.description,
startTime: row.startTime,
endTime: row.endTime,
duration: row.duration,
createdAt: row.createdAt,
updatedAt: row.updatedAt,
customerId: row.customerId,
userId: row.userId,
user: typeof row.user === 'string' ? JSON.parse(row.user) : row.user,
}))
return NextResponse.json({
...customer,
timeEntries,
})
} catch (error) {
console.error("Error fetching customer:", error)
return NextResponse.json({ error: "Failed to fetch customer" }, { status: 500 })
......@@ -45,18 +70,28 @@ export async function PUT(
return NextResponse.json({ error: "Name is required" }, { status: 400 })
}
const customer = await prisma.customer.update({
where: { id },
data: {
const result = await query(
`UPDATE customers
SET name = $1, email = $2, phone = $3, company = $4, address = $5, notes = $6, "updatedAt" = $7
WHERE id = $8
RETURNING *`,
[
name,
email: email || null,
phone: phone || null,
company: company || null,
address: address || null,
notes: notes || null,
},
})
email || null,
phone || null,
company || null,
address || null,
notes || null,
new Date(),
id,
]
)
if (result.rows.length === 0) {
return NextResponse.json({ error: "Customer not found" }, { status: 404 })
}
const customer = rowToCustomer(result.rows[0])
return NextResponse.json(customer)
} catch (error) {
console.error("Error updating customer:", error)
......@@ -70,9 +105,11 @@ export async function DELETE(
) {
try {
const { id } = await params
await prisma.customer.delete({
where: { id },
})
await query(
`DELETE FROM customers WHERE id = $1`,
[id]
)
return NextResponse.json({ success: true })
} catch (error) {
......@@ -80,4 +117,3 @@ export async function DELETE(
return NextResponse.json({ error: "Failed to delete customer" }, { status: 500 })
}
}
import { NextRequest, NextResponse } from "next/server"
import { prisma } from "@/lib/prisma"
import { query } from "@/lib/db"
import { generateId, rowToCustomer } from "@/lib/utils/db-helpers"
export async function GET(request: NextRequest) {
try {
const searchParams = request.nextUrl.searchParams
const search = searchParams.get("search")
const customers = await prisma.customer.findMany({
where: search
? {
OR: [
{ name: { contains: search } },
{ email: { contains: search } },
{ company: { contains: search } },
],
let customersQuery = `
SELECT c.*,
COALESCE(
json_agg(
json_build_object(
'id', te.id,
'description', te.description,
'startTime', te."startTime",
'endTime', te."endTime",
'duration', te.duration,
'createdAt', te."createdAt",
'updatedAt', te."updatedAt",
'customerId', te."customerId",
'userId', te."userId",
'user', json_build_object(
'name', u.name,
'email', u.email
)
)
) FILTER (WHERE te.id IS NOT NULL),
'[]'::json
) as "timeEntries"
FROM customers c
LEFT JOIN time_entries te ON te."customerId" = c.id
LEFT JOIN users u ON u.id = te."userId"
`
const params: string[] = []
if (search) {
customersQuery += `
WHERE c.name ILIKE $1 OR c.email ILIKE $1 OR c.company ILIKE $1
`
params.push(`%${search}%`)
}
customersQuery += `
GROUP BY c.id
ORDER BY c."createdAt" DESC
`
const result = await query(customersQuery, params.length > 0 ? params : undefined)
const customers = result.rows.map((row: { timeEntries: string | unknown[] }) => {
const customer = rowToCustomer(row)
return {
...customer,
timeEntries: typeof row.timeEntries === 'string'
? JSON.parse(row.timeEntries)
: (Array.isArray(row.timeEntries) ? row.timeEntries : []),
}
: undefined,
orderBy: { createdAt: "desc" },
include: {
timeEntries: {
include: {
user: {
select: { name: true, email: true },
},
},
orderBy: { createdAt: "desc" },
},
},
})
return NextResponse.json(customers)
......@@ -46,17 +77,27 @@ export async function POST(request: NextRequest) {
}
try {
const customer = await prisma.customer.create({
data: {
name: typeof name === 'string' ? name.trim() : name,
email: email && typeof email === 'string' && email.trim() !== "" ? email.trim() : null,
phone: phone && typeof phone === 'string' && phone.trim() !== "" ? phone.trim() : null,
company: company && typeof company === 'string' && company.trim() !== "" ? company.trim() : null,
address: address && typeof address === 'string' && address.trim() !== "" ? address.trim() : null,
notes: notes && typeof notes === 'string' && notes.trim() !== "" ? notes.trim() : null,
},
})
const id = generateId()
const now = new Date()
const result = await query(
`INSERT INTO customers (id, name, email, phone, company, address, notes, "createdAt", "updatedAt")
VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9)
RETURNING *`,
[
id,
typeof name === 'string' ? name.trim() : name,
email && typeof email === 'string' && email.trim() !== "" ? email.trim() : null,
phone && typeof phone === 'string' && phone.trim() !== "" ? phone.trim() : null,
company && typeof company === 'string' && company.trim() !== "" ? company.trim() : null,
address && typeof address === 'string' && address.trim() !== "" ? address.trim() : null,
notes && typeof notes === 'string' && notes.trim() !== "" ? notes.trim() : null,
now,
now,
]
)
const customer = rowToCustomer(result.rows[0])
return NextResponse.json(customer, { status: 201 })
} catch (dbError: unknown) {
console.error("Database error creating customer:", dbError)
......@@ -83,4 +124,3 @@ export async function POST(request: NextRequest) {
)
}
}
import { NextResponse } from "next/server"
import { prisma } from "@/lib/prisma"
export async function GET() {
try {
// Test Prisma connection
await prisma.$connect()
const customerCount = await prisma.customer.count()
return NextResponse.json({
success: true,
message: "Prisma connection successful",
customerCount
})
} catch (error: unknown) {
console.error("Test error:", error)
const errorMessage = error instanceof Error ? error.message : "Unknown error"
const errorStack = process.env.NODE_ENV === 'development' && error instanceof Error ? error.stack : undefined
return NextResponse.json({
success: false,
error: errorMessage,
stack: errorStack
}, { status: 500 })
}
}
import { NextRequest, NextResponse } from "next/server"
import { prisma } from "@/lib/prisma"
import { query } from "@/lib/db"
export async function DELETE(
request: NextRequest,
......@@ -7,9 +7,11 @@ export async function DELETE(
) {
try {
const { id } = await params
await prisma.timeEntry.delete({
where: { id },
})
await query(
`DELETE FROM time_entries WHERE id = $1`,
[id]
)
return NextResponse.json({ success: true })
} catch (error) {
......@@ -17,4 +19,3 @@ export async function DELETE(
return NextResponse.json({ error: "Failed to delete time entry" }, { status: 500 })
}
}
import { NextRequest, NextResponse } from "next/server"
import { prisma } from "@/lib/prisma"
import { query } from "@/lib/db"
import { generateId, rowToTimeEntry } from "@/lib/utils/db-helpers"
export async function GET(request: NextRequest) {
try {
......@@ -7,21 +8,51 @@ export async function GET(request: NextRequest) {
const customerId = searchParams.get("customerId")
const userId = searchParams.get("userId")
const timeEntries = await prisma.timeEntry.findMany({
where: {
...(customerId ? { customerId } : {}),
...(userId ? { userId } : {}),
},
include: {
customer: {
select: { id: true, name: true, company: true },
},
user: {
select: { id: true, name: true, email: true },
},
},
orderBy: { createdAt: "desc" },
})
let timeEntriesQuery = `
SELECT te.*,
json_build_object(
'id', c.id,
'name', c.name,
'company', c.company
) as customer,
json_build_object(
'id', u.id,
'name', u.name,
'email', u.email
) as user
FROM time_entries te
LEFT JOIN customers c ON c.id = te."customerId"
LEFT JOIN users u ON u.id = te."userId"
WHERE 1=1
`
const params: string[] = []
let paramCount = 0
if (customerId) {
paramCount++
timeEntriesQuery += ` AND te."customerId" = $${paramCount}`
params.push(customerId)
}
if (userId) {
paramCount++
timeEntriesQuery += ` AND te."userId" = $${paramCount}`
params.push(userId)
}
timeEntriesQuery += ` ORDER BY te."createdAt" DESC`
const result = await query(
timeEntriesQuery,
params.length > 0 ? params : undefined
)
const timeEntries = result.rows.map((row: { customer: unknown; user: unknown }) => ({
...rowToTimeEntry(row),
customer: typeof row.customer === 'string' ? JSON.parse(row.customer) : row.customer,
user: typeof row.user === 'string' ? JSON.parse(row.user) : row.user,
}))
return NextResponse.json(timeEntries)
} catch (error) {
......@@ -42,29 +73,45 @@ export async function POST(request: NextRequest) {
)
}
const timeEntry = await prisma.timeEntry.create({
data: {
const id = generateId()
const now = new Date()
const result = await query(
`INSERT INTO time_entries (id, description, "startTime", "endTime", duration, "customerId", "userId", "createdAt", "updatedAt")
VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9)
RETURNING *`,
[
id,
description,
startTime: new Date(startTime),
endTime: new Date(endTime),
duration: parseInt(duration),
new Date(startTime),
new Date(endTime),
parseInt(String(duration), 10),
customerId,
userId,
},
include: {
customer: {
select: { id: true, name: true, company: true },
},
user: {
select: { id: true, name: true, email: true },
},
},
})
return NextResponse.json(timeEntry, { status: 201 })
now,
now,
]
)
const timeEntry = rowToTimeEntry(result.rows[0])
// Get related customer and user
const customerResult = await query(
`SELECT id, name, company FROM customers WHERE id = $1`,
[customerId]
)
const userResult = await query(
`SELECT id, name, email FROM users WHERE id = $1`,
[userId]
)
return NextResponse.json({
...timeEntry,
customer: customerResult.rows[0] || null,
user: userResult.rows[0] || null,
}, { status: 201 })
} catch (error) {
console.error("Error creating time entry:", error)
return NextResponse.json({ error: "Failed to create time entry" }, { status: 500 })
}
}
import { NextRequest, NextResponse } from "next/server"
import { prisma } from "@/lib/prisma"
import { query } from "@/lib/db"
import { generateId, rowToUser } from "@/lib/utils/db-helpers"
export async function GET() {
try {
const users = await prisma.user.findMany({
orderBy: { createdAt: "desc" },
})
const result = await query(
`SELECT * FROM users ORDER BY "createdAt" DESC`
)
const users = result.rows.map(rowToUser)
return NextResponse.json(users)
} catch (error) {
console.error("Error fetching users:", error)
......@@ -24,26 +26,29 @@ export async function POST(request: NextRequest) {
}
// Check if user already exists
const existingUser = await prisma.user.findUnique({
where: { email },
})
const existingResult = await query(
`SELECT * FROM users WHERE email = $1`,
[email]
)
if (existingUser) {
return NextResponse.json(existingUser)
if (existingResult.rows.length > 0) {
return NextResponse.json(rowToUser(existingResult.rows[0]))
}
const user = await prisma.user.create({
data: {
email,
name,
image: image || null,
},
})
const id = generateId()
const now = new Date()
const result = await query(
`INSERT INTO users (id, email, name, image, "createdAt", "updatedAt")
VALUES ($1, $2, $3, $4, $5, $6)
RETURNING *`,
[id, email, name, image || null, now, now]
)
const user = rowToUser(result.rows[0])
return NextResponse.json(user, { status: 201 })
} catch (error) {
console.error("Error creating user:", error)
return NextResponse.json({ error: "Failed to create user" }, { status: 500 })
}
}
<svg width="64" height="64" viewBox="0 0 64 64" fill="none" xmlns="http://www.w3.org/2000/svg">
<defs>
<linearGradient id="bgGradient" x1="0%" y1="0%" x2="100%" y2="100%">
<stop offset="0%" style="stop-color:#0F172A;stop-opacity:1" />
<stop offset="100%" style="stop-color:#1E293B;stop-opacity:1" />
</linearGradient>
<linearGradient id="xGradient" x1="0%" y1="0%" x2="100%" y2="100%">
<stop offset="0%" style="stop-color:#3B82F6;stop-opacity:1" />
<stop offset="100%" style="stop-color:#2563EB;stop-opacity:1" />
</linearGradient>
<filter id="glow">
<feGaussianBlur stdDeviation="1.5" result="coloredBlur"/>
<feMerge>
<feMergeNode in="coloredBlur"/>
<feMergeNode in="SourceGraphic"/>
</feMerge>
</filter>
</defs>
<!-- Background with gradient -->
<rect width="64" height="64" rx="12" fill="url(#bgGradient)"/>
<!-- X with gradient and glow -->
<g transform="translate(14, 14)">
<path d="M 4 4 L 32 32 M 32 4 L 4 32"
stroke="url(#xGradient)"
stroke-width="5"
stroke-linecap="round"
stroke-linejoin="round"
filter="url(#glow)"/>
</g>
</svg>
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 100 100">
<rect width="100" height="100" fill="#000000"/>
<text x="50" y="70" font-family="Arial, sans-serif" font-size="60" font-weight="bold" fill="#ffffff" text-anchor="middle">CX</text>
</svg>
import { Pool } from 'pg'
const connectionString = process.env.DATABASE_URL
if (!connectionString) {
throw new Error('DATABASE_URL environment variable is not set')
}
// Create a singleton pool instance
const pool = new Pool({
connectionString,
ssl: process.env.NODE_ENV === 'production' ? { rejectUnauthorized: false } : false,
})
// Handle pool errors
pool.on('error', (err) => {
console.error('Unexpected error on idle client', err)
process.exit(-1)
})
export { pool as db }
// Helper function to query the database
export async function query(text: string, params?: unknown[]) {
const start = Date.now()
const res = await pool.query(text, params)
const duration = Date.now() - start
if (process.env.NODE_ENV === 'development') {
console.log('Executed query', { text, duration, rows: res.rowCount })
}
return res
}
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
import { nanoid } from 'nanoid'
// Generate CUID-like ID
export function generateId(): string {
return nanoid()
}
// Helper to convert database rows to objects
export function rowToCustomer(row: {
id: string
name: string
email: string | null
phone: string | null
company: string | null
address: string | null
notes: string | null
createdAt: Date
updatedAt: Date
}) {
return {
id: row.id,
name: row.name,
email: row.email,
phone: row.phone,
company: row.company,
address: row.address,
notes: row.notes,
createdAt: row.createdAt,
updatedAt: row.updatedAt,
}
}
export function rowToUser(row: {
id: string
email: string
name: string
image: string | null
createdAt: Date
updatedAt: Date
}) {
return {
id: row.id,
email: row.email,
name: row.name,
image: row.image,
createdAt: row.createdAt,
updatedAt: row.updatedAt,
}
}
export function rowToTimeEntry(row: {
id: string
description: string
startTime: Date
endTime: Date
duration: number
createdAt: Date
updatedAt: Date
customerId: string
userId: string
}) {
return {
id: row.id,
description: row.description,
startTime: row.startTime,
endTime: row.endTime,
duration: row.duration,
createdAt: row.createdAt,
updatedAt: row.updatedAt,
customerId: row.customerId,
userId: row.userId,
}
}
......@@ -8,15 +8,15 @@
"name": "corex-dashboard",
"version": "0.1.0",
"dependencies": {
"@prisma/client": "^6.19.1",
"@radix-ui/react-label": "^2.1.8",
"@radix-ui/react-slot": "^1.2.4",
"class-variance-authority": "^0.7.1",
"clsx": "^2.1.1",
"lucide-react": "^0.562.0",
"nanoid": "^5.1.6",
"next": "16.1.1",
"next-themes": "^0.4.6",
"prisma": "^6.19.1",
"pg": "^8.16.3",
"react": "19.2.3",
"react-dom": "19.2.3",
"tailwind-merge": "^3.4.0",
......@@ -25,6 +25,7 @@
"devDependencies": {
"@tailwindcss/postcss": "^4",
"@types/node": "^20",
"@types/pg": "^8.16.0",
"@types/react": "^19",
"@types/react-dom": "^19",
"eslint": "^9",
......@@ -1237,85 +1238,6 @@
"node": ">=12.4.0"
}
},
"node_modules/@prisma/client": {
"version": "6.19.1",
"resolved": "https://registry.npmjs.org/@prisma/client/-/client-6.19.1.tgz",
"integrity": "sha512-4SXj4Oo6HyQkLUWT8Ke5R0PTAfVOKip5Roo+6+b2EDTkFg5be0FnBWiuRJc0BC0sRQIWGMLKW1XguhVfW/z3/A==",
"hasInstallScript": true,
"license": "Apache-2.0",
"engines": {
"node": ">=18.18"
},
"peerDependencies": {
"prisma": "*",
"typescript": ">=5.1.0"
},
"peerDependenciesMeta": {
"prisma": {
"optional": true
},
"typescript": {
"optional": true
}
}
},
"node_modules/@prisma/config": {
"version": "6.19.1",
"resolved": "https://registry.npmjs.org/@prisma/config/-/config-6.19.1.tgz",
"integrity": "sha512-bUL/aYkGXLwxVGhJmQMtslLT7KPEfUqmRa919fKI4wQFX4bIFUKiY8Jmio/2waAjjPYrtuDHa7EsNCnJTXxiOw==",
"license": "Apache-2.0",
"dependencies": {
"c12": "3.1.0",
"deepmerge-ts": "7.1.5",
"effect": "3.18.4",
"empathic": "2.0.0"
}
},
"node_modules/@prisma/debug": {
"version": "6.19.1",
"resolved": "https://registry.npmjs.org/@prisma/debug/-/debug-6.19.1.tgz",
"integrity": "sha512-h1JImhlAd/s5nhY/e9qkAzausWldbeT+e4nZF7A4zjDYBF4BZmKDt4y0jK7EZapqOm1kW7V0e9agV/iFDy3fWw==",
"license": "Apache-2.0"
},
"node_modules/@prisma/engines": {
"version": "6.19.1",
"resolved": "https://registry.npmjs.org/@prisma/engines/-/engines-6.19.1.tgz",
"integrity": "sha512-xy95dNJ7DiPf9IJ3oaVfX785nbFl7oNDzclUF+DIiJw6WdWCvPl0LPU0YqQLsrwv8N64uOQkH391ujo3wSo+Nw==",
"hasInstallScript": true,
"license": "Apache-2.0",
"dependencies": {
"@prisma/debug": "6.19.1",
"@prisma/engines-version": "7.1.1-3.c2990dca591cba766e3b7ef5d9e8a84796e47ab7",
"@prisma/fetch-engine": "6.19.1",
"@prisma/get-platform": "6.19.1"
}
},
"node_modules/@prisma/engines-version": {
"version": "7.1.1-3.c2990dca591cba766e3b7ef5d9e8a84796e47ab7",
"resolved": "https://registry.npmjs.org/@prisma/engines-version/-/engines-version-7.1.1-3.c2990dca591cba766e3b7ef5d9e8a84796e47ab7.tgz",
"integrity": "sha512-03bgb1VD5gvuumNf+7fVGBzfpJPjmqV423l/WxsWk2cNQ42JD0/SsFBPhN6z8iAvdHs07/7ei77SKu7aZfq8bA==",
"license": "Apache-2.0"
},
"node_modules/@prisma/fetch-engine": {
"version": "6.19.1",
"resolved": "https://registry.npmjs.org/@prisma/fetch-engine/-/fetch-engine-6.19.1.tgz",
"integrity": "sha512-mmgcotdaq4VtAHO6keov3db+hqlBzQS6X7tR7dFCbvXjLVTxBYdSJFRWz+dq7F9p6dvWyy1X0v8BlfRixyQK6g==",
"license": "Apache-2.0",
"dependencies": {
"@prisma/debug": "6.19.1",
"@prisma/engines-version": "7.1.1-3.c2990dca591cba766e3b7ef5d9e8a84796e47ab7",
"@prisma/get-platform": "6.19.1"
}
},
"node_modules/@prisma/get-platform": {
"version": "6.19.1",
"resolved": "https://registry.npmjs.org/@prisma/get-platform/-/get-platform-6.19.1.tgz",
"integrity": "sha512-zsg44QUiQAnFUyh6Fbt7c9HjMXHwFTqtrgcX7DAZmRgnkPyYT7Sh8Mn8D5PuuDYNtMOYcpLGg576MLfIORsBYw==",
"license": "Apache-2.0",
"dependencies": {
"@prisma/debug": "6.19.1"
}
},
"node_modules/@radix-ui/react-compose-refs": {
"version": "1.1.2",
"resolved": "https://registry.npmjs.org/@radix-ui/react-compose-refs/-/react-compose-refs-1.1.2.tgz",
......@@ -1402,12 +1324,6 @@
"dev": true,
"license": "MIT"
},
"node_modules/@standard-schema/spec": {
"version": "1.1.0",
"resolved": "https://registry.npmjs.org/@standard-schema/spec/-/spec-1.1.0.tgz",
"integrity": "sha512-l2aFy5jALhniG5HgqrD6jXLi/rUWrKvqN/qJx6yoJsgKhblVd+iqqU4RCXavm/jPityDo5TCvKMnpjKnOriy0w==",
"license": "MIT"
},
"node_modules/@swc/helpers": {
"version": "0.5.15",
"resolved": "https://registry.npmjs.org/@swc/helpers/-/helpers-0.5.15.tgz",
......@@ -1730,6 +1646,18 @@
"undici-types": "~6.21.0"
}
},
"node_modules/@types/pg": {
"version": "8.16.0",
"resolved": "https://registry.npmjs.org/@types/pg/-/pg-8.16.0.tgz",
"integrity": "sha512-RmhMd/wD+CF8Dfo+cVIy3RR5cl8CyfXQ0tGgW6XBL8L4LM/UTEbNXYRbLwU6w+CgrKBNbrQWt4FUtTfaU5jSYQ==",
"dev": true,
"license": "MIT",
"dependencies": {
"@types/node": "*",
"pg-protocol": "*",
"pg-types": "^2.2.0"
}
},
"node_modules/@types/react": {
"version": "19.2.7",
"resolved": "https://registry.npmjs.org/@types/react/-/react-19.2.7.tgz",
......@@ -2653,34 +2581,6 @@
"node": "^6 || ^7 || ^8 || ^9 || ^10 || ^11 || ^12 || >=13.7"
}
},
"node_modules/c12": {
"version": "3.1.0",
"resolved": "https://registry.npmjs.org/c12/-/c12-3.1.0.tgz",
"integrity": "sha512-uWoS8OU1MEIsOv8p/5a82c3H31LsWVR5qiyXVfBNOzfffjUWtPnhAb4BYI2uG2HfGmZmFjCtui5XNWaps+iFuw==",
"license": "MIT",
"dependencies": {
"chokidar": "^4.0.3",
"confbox": "^0.2.2",
"defu": "^6.1.4",
"dotenv": "^16.6.1",
"exsolve": "^1.0.7",
"giget": "^2.0.0",
"jiti": "^2.4.2",
"ohash": "^2.0.11",
"pathe": "^2.0.3",
"perfect-debounce": "^1.0.0",
"pkg-types": "^2.2.0",
"rc9": "^2.1.2"
},
"peerDependencies": {
"magicast": "^0.3.5"
},
"peerDependenciesMeta": {
"magicast": {
"optional": true
}
}
},
"node_modules/call-bind": {
"version": "1.0.8",
"resolved": "https://registry.npmjs.org/call-bind/-/call-bind-1.0.8.tgz",
......@@ -2778,30 +2678,6 @@
"url": "https://github.com/chalk/chalk?sponsor=1"
}
},
"node_modules/chokidar": {
"version": "4.0.3",
"resolved": "https://registry.npmjs.org/chokidar/-/chokidar-4.0.3.tgz",
"integrity": "sha512-Qgzu8kfBvo+cA4962jnP1KkS6Dop5NS6g7R5LFYJr4b8Ub94PPQXUksCw9PvXoeXPRRddRNC5C1JQUR2SMGtnA==",
"license": "MIT",
"dependencies": {
"readdirp": "^4.0.1"
},
"engines": {
"node": ">= 14.16.0"
},
"funding": {
"url": "https://paulmillr.com/funding/"
}
},
"node_modules/citty": {
"version": "0.1.6",
"resolved": "https://registry.npmjs.org/citty/-/citty-0.1.6.tgz",
"integrity": "sha512-tskPPKEs8D2KPafUypv2gxwJP8h/OaJmC82QQGGDQcHvXX43xF2VDACcJVmZ0EuSxkpO9Kc4MlrA3q0+FG58AQ==",
"license": "MIT",
"dependencies": {
"consola": "^3.2.3"
}
},
"node_modules/class-variance-authority": {
"version": "0.7.1",
"resolved": "https://registry.npmjs.org/class-variance-authority/-/class-variance-authority-0.7.1.tgz",
......@@ -2856,21 +2732,6 @@
"dev": true,
"license": "MIT"
},
"node_modules/confbox": {
"version": "0.2.2",
"resolved": "https://registry.npmjs.org/confbox/-/confbox-0.2.2.tgz",
"integrity": "sha512-1NB+BKqhtNipMsov4xI/NnhCKp9XG9NamYp5PVm9klAT0fsrNPjaFICsCFhNhwZJKNh7zB/3q8qXz0E9oaMNtQ==",
"license": "MIT"
},
"node_modules/consola": {
"version": "3.4.2",
"resolved": "https://registry.npmjs.org/consola/-/consola-3.4.2.tgz",
"integrity": "sha512-5IKcdX0nnYavi6G7TtOhwkYzyjfJlatbjMjuLSfE2kYT5pMDOilZ4OvMhi637CcDICTmz3wARPoyhqyX1Y+XvA==",
"license": "MIT",
"engines": {
"node": "^14.18.0 || >=16.10.0"
}
},
"node_modules/convert-source-map": {
"version": "2.0.0",
"resolved": "https://registry.npmjs.org/convert-source-map/-/convert-source-map-2.0.0.tgz",
......@@ -2986,15 +2847,6 @@
"dev": true,
"license": "MIT"
},
"node_modules/deepmerge-ts": {
"version": "7.1.5",
"resolved": "https://registry.npmjs.org/deepmerge-ts/-/deepmerge-ts-7.1.5.tgz",
"integrity": "sha512-HOJkrhaYsweh+W+e74Yn7YStZOilkoPb6fycpwNLKzSPtruFs48nYis0zy5yJz1+ktUhHxoRDJ27RQAWLIJVJw==",
"license": "BSD-3-Clause",
"engines": {
"node": ">=16.0.0"
}
},
"node_modules/define-data-property": {
"version": "1.1.4",
"resolved": "https://registry.npmjs.org/define-data-property/-/define-data-property-1.1.4.tgz",
......@@ -3031,18 +2883,6 @@
"url": "https://github.com/sponsors/ljharb"
}
},
"node_modules/defu": {
"version": "6.1.4",
"resolved": "https://registry.npmjs.org/defu/-/defu-6.1.4.tgz",
"integrity": "sha512-mEQCMmwJu317oSz8CwdIOdwf3xMif1ttiM8LTufzc3g6kR+9Pe236twL8j3IYT1F7GfRgGcW6MWxzZjLIkuHIg==",
"license": "MIT"
},
"node_modules/destr": {
"version": "2.0.5",
"resolved": "https://registry.npmjs.org/destr/-/destr-2.0.5.tgz",
"integrity": "sha512-ugFTXCtDZunbzasqBxrK93Ik/DRYsO6S/fedkWEMKqt04xZ4csmnmwGDBAb07QWNaGMAmnTIemsYZCksjATwsA==",
"license": "MIT"
},
"node_modules/detect-libc": {
"version": "2.1.2",
"resolved": "https://registry.npmjs.org/detect-libc/-/detect-libc-2.1.2.tgz",
......@@ -3066,18 +2906,6 @@
"node": ">=0.10.0"
}
},
"node_modules/dotenv": {
"version": "16.6.1",
"resolved": "https://registry.npmjs.org/dotenv/-/dotenv-16.6.1.tgz",
"integrity": "sha512-uBq4egWHTcTt33a72vpSG0z3HnPuIl6NqYcTrKEg2azoEyl2hpW0zqlxysq2pK9HlDIHyHyakeYaYnSAwd8bow==",
"license": "BSD-2-Clause",
"engines": {
"node": ">=12"
},
"funding": {
"url": "https://dotenvx.com"
}
},
"node_modules/dunder-proto": {
"version": "1.0.1",
"resolved": "https://registry.npmjs.org/dunder-proto/-/dunder-proto-1.0.1.tgz",
......@@ -3093,16 +2921,6 @@
"node": ">= 0.4"
}
},
"node_modules/effect": {
"version": "3.18.4",
"resolved": "https://registry.npmjs.org/effect/-/effect-3.18.4.tgz",
"integrity": "sha512-b1LXQJLe9D11wfnOKAk3PKxuqYshQ0Heez+y5pnkd3jLj1yx9QhM72zZ9uUrOQyNvrs2GZZd/3maL0ZV18YuDA==",
"license": "MIT",
"dependencies": {
"@standard-schema/spec": "^1.0.0",
"fast-check": "^3.23.1"
}
},
"node_modules/electron-to-chromium": {
"version": "1.5.267",
"resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.5.267.tgz",
......@@ -3117,15 +2935,6 @@
"dev": true,
"license": "MIT"
},
"node_modules/empathic": {
"version": "2.0.0",
"resolved": "https://registry.npmjs.org/empathic/-/empathic-2.0.0.tgz",
"integrity": "sha512-i6UzDscO/XfAcNYD75CfICkmfLedpyPDdozrLMmQc5ORaQcdMoc21OnlEylMIqI7U8eniKrPMxxtj8k0vhmJhA==",
"license": "MIT",
"engines": {
"node": ">=14"
}
},
"node_modules/enhanced-resolve": {
"version": "5.18.4",
"resolved": "https://registry.npmjs.org/enhanced-resolve/-/enhanced-resolve-5.18.4.tgz",
......@@ -3766,34 +3575,6 @@
"node": ">=0.10.0"
}
},
"node_modules/exsolve": {
"version": "1.0.8",
"resolved": "https://registry.npmjs.org/exsolve/-/exsolve-1.0.8.tgz",
"integrity": "sha512-LmDxfWXwcTArk8fUEnOfSZpHOJ6zOMUJKOtFLFqJLoKJetuQG874Uc7/Kki7zFLzYybmZhp1M7+98pfMqeX8yA==",
"license": "MIT"
},
"node_modules/fast-check": {
"version": "3.23.2",
"resolved": "https://registry.npmjs.org/fast-check/-/fast-check-3.23.2.tgz",
"integrity": "sha512-h5+1OzzfCC3Ef7VbtKdcv7zsstUQwUDlYpUTvjeUsJAssPgLn7QzbboPtL5ro04Mq0rPOsMzl7q5hIbRs2wD1A==",
"funding": [
{
"type": "individual",
"url": "https://github.com/sponsors/dubzzz"
},
{
"type": "opencollective",
"url": "https://opencollective.com/fast-check"
}
],
"license": "MIT",
"dependencies": {
"pure-rand": "^6.1.0"
},
"engines": {
"node": ">=8.0.0"
}
},
"node_modules/fast-deep-equal": {
"version": "3.1.3",
"resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz",
......@@ -4066,23 +3847,6 @@
"url": "https://github.com/privatenumber/get-tsconfig?sponsor=1"
}
},
"node_modules/giget": {
"version": "2.0.0",
"resolved": "https://registry.npmjs.org/giget/-/giget-2.0.0.tgz",
"integrity": "sha512-L5bGsVkxJbJgdnwyuheIunkGatUF/zssUoxxjACCseZYAVbaqdh9Tsmmlkl8vYan09H7sbvKt4pS8GqKLBrEzA==",
"license": "MIT",
"dependencies": {
"citty": "^0.1.6",
"consola": "^3.4.0",
"defu": "^6.1.4",
"node-fetch-native": "^1.6.6",
"nypm": "^0.6.0",
"pathe": "^2.0.3"
},
"bin": {
"giget": "dist/cli.mjs"
}
},
"node_modules/glob-parent": {
"version": "6.0.2",
"resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-6.0.2.tgz",
......@@ -4760,6 +4524,7 @@
"version": "2.6.1",
"resolved": "https://registry.npmjs.org/jiti/-/jiti-2.6.1.tgz",
"integrity": "sha512-ekilCSN1jwRvIbgeg/57YFh8qQDNbwDb9xT/qu2DAHbFFZUicIl4ygVaAvzveMhMVr3LnpSKTNnwt8PoOfmKhQ==",
"dev": true,
"license": "MIT",
"bin": {
"jiti": "lib/jiti-cli.mjs"
......@@ -5283,9 +5048,9 @@
"license": "MIT"
},
"node_modules/nanoid": {
"version": "3.3.11",
"resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.11.tgz",
"integrity": "sha512-N8SpfPUnUp1bK+PMYW8qSWdl9U+wwNWI4QKxOYDy9JAro3WMX7p2OeVRF9v+347pnakNevPmiHhNmZ2HbFA76w==",
"version": "5.1.6",
"resolved": "https://registry.npmjs.org/nanoid/-/nanoid-5.1.6.tgz",
"integrity": "sha512-c7+7RQ+dMB5dPwwCp4ee1/iV/q2P6aK1mTZcfr1BTuVlyW9hJYiMPybJCcnBlQtuSmTIWNeazm/zqNoZSSElBg==",
"funding": [
{
"type": "github",
......@@ -5294,10 +5059,10 @@
],
"license": "MIT",
"bin": {
"nanoid": "bin/nanoid.cjs"
"nanoid": "bin/nanoid.js"
},
"engines": {
"node": "^10 || ^12 || ^13.7 || ^14 || >=15.0.1"
"node": "^18 || >=20"
}
},
"node_modules/napi-postinstall": {
......@@ -5386,6 +5151,24 @@
"react-dom": "^16.8 || ^17 || ^18 || ^19 || ^19.0.0-rc"
}
},
"node_modules/next/node_modules/nanoid": {
"version": "3.3.11",
"resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.11.tgz",
"integrity": "sha512-N8SpfPUnUp1bK+PMYW8qSWdl9U+wwNWI4QKxOYDy9JAro3WMX7p2OeVRF9v+347pnakNevPmiHhNmZ2HbFA76w==",
"funding": [
{
"type": "github",
"url": "https://github.com/sponsors/ai"
}
],
"license": "MIT",
"bin": {
"nanoid": "bin/nanoid.cjs"
},
"engines": {
"node": "^10 || ^12 || ^13.7 || ^14 || >=15.0.1"
}
},
"node_modules/next/node_modules/postcss": {
"version": "8.4.31",
"resolved": "https://registry.npmjs.org/postcss/-/postcss-8.4.31.tgz",
......@@ -5414,12 +5197,6 @@
"node": "^10 || ^12 || >=14"
}
},
"node_modules/node-fetch-native": {
"version": "1.6.7",
"resolved": "https://registry.npmjs.org/node-fetch-native/-/node-fetch-native-1.6.7.tgz",
"integrity": "sha512-g9yhqoedzIUm0nTnTqAQvueMPVOuIY16bqgAJJC8XOOubYFNwz6IER9qs0Gq2Xd0+CecCKFjtdDTMA4u4xG06Q==",
"license": "MIT"
},
"node_modules/node-releases": {
"version": "2.0.27",
"resolved": "https://registry.npmjs.org/node-releases/-/node-releases-2.0.27.tgz",
......@@ -5427,25 +5204,6 @@
"dev": true,
"license": "MIT"
},
"node_modules/nypm": {
"version": "0.6.2",
"resolved": "https://registry.npmjs.org/nypm/-/nypm-0.6.2.tgz",
"integrity": "sha512-7eM+hpOtrKrBDCh7Ypu2lJ9Z7PNZBdi/8AT3AX8xoCj43BBVHD0hPSTEvMtkMpfs8FCqBGhxB+uToIQimA111g==",
"license": "MIT",
"dependencies": {
"citty": "^0.1.6",
"consola": "^3.4.2",
"pathe": "^2.0.3",
"pkg-types": "^2.3.0",
"tinyexec": "^1.0.1"
},
"bin": {
"nypm": "dist/cli.mjs"
},
"engines": {
"node": "^14.16.0 || >=16.10.0"
}
},
"node_modules/object-assign": {
"version": "4.1.1",
"resolved": "https://registry.npmjs.org/object-assign/-/object-assign-4.1.1.tgz",
......@@ -5569,12 +5327,6 @@
"url": "https://github.com/sponsors/ljharb"
}
},
"node_modules/ohash": {
"version": "2.0.11",
"resolved": "https://registry.npmjs.org/ohash/-/ohash-2.0.11.tgz",
"integrity": "sha512-RdR9FQrFwNBNXAr4GixM8YaRZRJ5PUWbKYbE5eOsrwAjJW0q2REGcf79oYPsLyskQCZG1PLN+S/K1V00joZAoQ==",
"license": "MIT"
},
"node_modules/optionator": {
"version": "0.9.4",
"resolved": "https://registry.npmjs.org/optionator/-/optionator-0.9.4.tgz",
......@@ -5683,18 +5435,96 @@
"dev": true,
"license": "MIT"
},
"node_modules/pathe": {
"version": "2.0.3",
"resolved": "https://registry.npmjs.org/pathe/-/pathe-2.0.3.tgz",
"integrity": "sha512-WUjGcAqP1gQacoQe+OBJsFA7Ld4DyXuUIjZ5cc75cLHvJ7dtNsTugphxIADwspS+AraAUePCKrSVtPLFj/F88w==",
"node_modules/pg": {
"version": "8.16.3",
"resolved": "https://registry.npmjs.org/pg/-/pg-8.16.3.tgz",
"integrity": "sha512-enxc1h0jA/aq5oSDMvqyW3q89ra6XIIDZgCX9vkMrnz5DFTw/Ny3Li2lFQ+pt3L6MCgm/5o2o8HW9hiJji+xvw==",
"license": "MIT",
"peer": true,
"dependencies": {
"pg-connection-string": "^2.9.1",
"pg-pool": "^3.10.1",
"pg-protocol": "^1.10.3",
"pg-types": "2.2.0",
"pgpass": "1.0.5"
},
"engines": {
"node": ">= 16.0.0"
},
"optionalDependencies": {
"pg-cloudflare": "^1.2.7"
},
"peerDependencies": {
"pg-native": ">=3.0.1"
},
"peerDependenciesMeta": {
"pg-native": {
"optional": true
}
}
},
"node_modules/pg-cloudflare": {
"version": "1.2.7",
"resolved": "https://registry.npmjs.org/pg-cloudflare/-/pg-cloudflare-1.2.7.tgz",
"integrity": "sha512-YgCtzMH0ptvZJslLM1ffsY4EuGaU0cx4XSdXLRFae8bPP4dS5xL1tNB3k2o/N64cHJpwU7dxKli/nZ2lUa5fLg==",
"license": "MIT",
"optional": true
},
"node_modules/pg-connection-string": {
"version": "2.9.1",
"resolved": "https://registry.npmjs.org/pg-connection-string/-/pg-connection-string-2.9.1.tgz",
"integrity": "sha512-nkc6NpDcvPVpZXxrreI/FOtX3XemeLl8E0qFr6F2Lrm/I8WOnaWNhIPK2Z7OHpw7gh5XJThi6j6ppgNoaT1w4w==",
"license": "MIT"
},
"node_modules/perfect-debounce": {
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/perfect-debounce/-/perfect-debounce-1.0.0.tgz",
"integrity": "sha512-xCy9V055GLEqoFaHoC1SoLIaLmWctgCUaBaWxDZ7/Zx4CTyX7cJQLJOok/orfjZAh9kEYpjJa4d0KcJmCbctZA==",
"node_modules/pg-int8": {
"version": "1.0.1",
"resolved": "https://registry.npmjs.org/pg-int8/-/pg-int8-1.0.1.tgz",
"integrity": "sha512-WCtabS6t3c8SkpDBUlb1kjOs7l66xsGdKpIPZsg4wR+B3+u9UAum2odSsF9tnvxg80h4ZxLWMy4pRjOsFIqQpw==",
"license": "ISC",
"engines": {
"node": ">=4.0.0"
}
},
"node_modules/pg-pool": {
"version": "3.10.1",
"resolved": "https://registry.npmjs.org/pg-pool/-/pg-pool-3.10.1.tgz",
"integrity": "sha512-Tu8jMlcX+9d8+QVzKIvM/uJtp07PKr82IUOYEphaWcoBhIYkoHpLXN3qO59nAI11ripznDsEzEv8nUxBVWajGg==",
"license": "MIT",
"peerDependencies": {
"pg": ">=8.0"
}
},
"node_modules/pg-protocol": {
"version": "1.10.3",
"resolved": "https://registry.npmjs.org/pg-protocol/-/pg-protocol-1.10.3.tgz",
"integrity": "sha512-6DIBgBQaTKDJyxnXaLiLR8wBpQQcGWuAESkRBX/t6OwA8YsqP+iVSiond2EDy6Y/dsGk8rh/jtax3js5NeV7JQ==",
"license": "MIT"
},
"node_modules/pg-types": {
"version": "2.2.0",
"resolved": "https://registry.npmjs.org/pg-types/-/pg-types-2.2.0.tgz",
"integrity": "sha512-qTAAlrEsl8s4OiEQY69wDvcMIdQN6wdz5ojQiOy6YRMuynxenON0O5oCpJI6lshc6scgAY8qvJ2On/p+CXY0GA==",
"license": "MIT",
"dependencies": {
"pg-int8": "1.0.1",
"postgres-array": "~2.0.0",
"postgres-bytea": "~1.0.0",
"postgres-date": "~1.0.4",
"postgres-interval": "^1.1.0"
},
"engines": {
"node": ">=4"
}
},
"node_modules/pgpass": {
"version": "1.0.5",
"resolved": "https://registry.npmjs.org/pgpass/-/pgpass-1.0.5.tgz",
"integrity": "sha512-FdW9r/jQZhSeohs1Z3sI1yxFQNFvMcnmfuj4WBMUTxOrAyLMaTcE1aAMBiTlbMNaXvBCQuVi0R7hd8udDSP7ug==",
"license": "MIT",
"dependencies": {
"split2": "^4.1.0"
}
},
"node_modules/picocolors": {
"version": "1.1.1",
"resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.1.1.tgz",
......@@ -5714,17 +5544,6 @@
"url": "https://github.com/sponsors/jonschlinkert"
}
},
"node_modules/pkg-types": {
"version": "2.3.0",
"resolved": "https://registry.npmjs.org/pkg-types/-/pkg-types-2.3.0.tgz",
"integrity": "sha512-SIqCzDRg0s9npO5XQ3tNZioRY1uK06lA41ynBC1YmFTmnY6FjUjVt6s4LoADmwoig1qqD0oK8h1p/8mlMx8Oig==",
"license": "MIT",
"dependencies": {
"confbox": "^0.2.2",
"exsolve": "^1.0.7",
"pathe": "^2.0.3"
}
},
"node_modules/possible-typed-array-names": {
"version": "1.1.0",
"resolved": "https://registry.npmjs.org/possible-typed-array-names/-/possible-typed-array-names-1.1.0.tgz",
......@@ -5764,40 +5583,72 @@
"node": "^10 || ^12 || >=14"
}
},
"node_modules/prelude-ls": {
"version": "1.2.1",
"resolved": "https://registry.npmjs.org/prelude-ls/-/prelude-ls-1.2.1.tgz",
"integrity": "sha512-vkcDPrRZo1QZLbn5RLGPpg/WmIQ65qoWWhcGKf/b5eplkkarX0m9z8ppCat4mlOqUsWpyNuYgO3VRyrYHSzX5g==",
"node_modules/postcss/node_modules/nanoid": {
"version": "3.3.11",
"resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.11.tgz",
"integrity": "sha512-N8SpfPUnUp1bK+PMYW8qSWdl9U+wwNWI4QKxOYDy9JAro3WMX7p2OeVRF9v+347pnakNevPmiHhNmZ2HbFA76w==",
"dev": true,
"funding": [
{
"type": "github",
"url": "https://github.com/sponsors/ai"
}
],
"license": "MIT",
"bin": {
"nanoid": "bin/nanoid.cjs"
},
"engines": {
"node": ">= 0.8.0"
"node": "^10 || ^12 || ^13.7 || ^14 || >=15.0.1"
}
},
"node_modules/prisma": {
"version": "6.19.1",
"resolved": "https://registry.npmjs.org/prisma/-/prisma-6.19.1.tgz",
"integrity": "sha512-XRfmGzh6gtkc/Vq3LqZJcS2884dQQW3UhPo6jNRoiTW95FFQkXFg8vkYEy6og+Pyv0aY7zRQ7Wn1Cvr56XjhQQ==",
"hasInstallScript": true,
"license": "Apache-2.0",
"peer": true,
"dependencies": {
"@prisma/config": "6.19.1",
"@prisma/engines": "6.19.1"
"node_modules/postgres-array": {
"version": "2.0.0",
"resolved": "https://registry.npmjs.org/postgres-array/-/postgres-array-2.0.0.tgz",
"integrity": "sha512-VpZrUqU5A69eQyW2c5CA1jtLecCsN2U/bD6VilrFDWq5+5UIEVO7nazS3TEcHf1zuPYO/sqGvUvW62g86RXZuA==",
"license": "MIT",
"engines": {
"node": ">=4"
}
},
"bin": {
"prisma": "build/index.js"
"node_modules/postgres-bytea": {
"version": "1.0.1",
"resolved": "https://registry.npmjs.org/postgres-bytea/-/postgres-bytea-1.0.1.tgz",
"integrity": "sha512-5+5HqXnsZPE65IJZSMkZtURARZelel2oXUEO8rH83VS/hxH5vv1uHquPg5wZs8yMAfdv971IU+kcPUczi7NVBQ==",
"license": "MIT",
"engines": {
"node": ">=0.10.0"
}
},
"node_modules/postgres-date": {
"version": "1.0.7",
"resolved": "https://registry.npmjs.org/postgres-date/-/postgres-date-1.0.7.tgz",
"integrity": "sha512-suDmjLVQg78nMK2UZ454hAG+OAW+HQPZ6n++TNDUX+L0+uUlLywnoxJKDou51Zm+zTCjrCl0Nq6J9C5hP9vK/Q==",
"license": "MIT",
"engines": {
"node": ">=18.18"
"node": ">=0.10.0"
}
},
"peerDependencies": {
"typescript": ">=5.1.0"
"node_modules/postgres-interval": {
"version": "1.2.0",
"resolved": "https://registry.npmjs.org/postgres-interval/-/postgres-interval-1.2.0.tgz",
"integrity": "sha512-9ZhXKM/rw350N1ovuWHbGxnGh/SNJ4cnxHiM0rxE4VN41wsg8P8zWn9hv/buK00RP4WvlOyr/RBDiptyxVbkZQ==",
"license": "MIT",
"dependencies": {
"xtend": "^4.0.0"
},
"peerDependenciesMeta": {
"typescript": {
"optional": true
"engines": {
"node": ">=0.10.0"
}
},
"node_modules/prelude-ls": {
"version": "1.2.1",
"resolved": "https://registry.npmjs.org/prelude-ls/-/prelude-ls-1.2.1.tgz",
"integrity": "sha512-vkcDPrRZo1QZLbn5RLGPpg/WmIQ65qoWWhcGKf/b5eplkkarX0m9z8ppCat4mlOqUsWpyNuYgO3VRyrYHSzX5g==",
"dev": true,
"license": "MIT",
"engines": {
"node": ">= 0.8.0"
}
},
"node_modules/prop-types": {
......@@ -5822,22 +5673,6 @@
"node": ">=6"
}
},
"node_modules/pure-rand": {
"version": "6.1.0",
"resolved": "https://registry.npmjs.org/pure-rand/-/pure-rand-6.1.0.tgz",
"integrity": "sha512-bVWawvoZoBYpp6yIoQtQXHZjmz35RSVHnUOTefl8Vcjr8snTPY1wnpSPMWekcFwbxI6gtmT7rSYPFvz71ldiOA==",
"funding": [
{
"type": "individual",
"url": "https://github.com/sponsors/dubzzz"
},
{
"type": "opencollective",
"url": "https://opencollective.com/fast-check"
}
],
"license": "MIT"
},
"node_modules/queue-microtask": {
"version": "1.2.3",
"resolved": "https://registry.npmjs.org/queue-microtask/-/queue-microtask-1.2.3.tgz",
......@@ -5859,16 +5694,6 @@
],
"license": "MIT"
},
"node_modules/rc9": {
"version": "2.1.2",
"resolved": "https://registry.npmjs.org/rc9/-/rc9-2.1.2.tgz",
"integrity": "sha512-btXCnMmRIBINM2LDZoEmOogIZU7Qe7zn4BpomSKZ/ykbLObuBdvG+mFq11DL6fjH1DRwHhrlgtYWG96bJiC7Cg==",
"license": "MIT",
"dependencies": {
"defu": "^6.1.4",
"destr": "^2.0.3"
}
},
"node_modules/react": {
"version": "19.2.3",
"resolved": "https://registry.npmjs.org/react/-/react-19.2.3.tgz",
......@@ -5899,19 +5724,6 @@
"dev": true,
"license": "MIT"
},
"node_modules/readdirp": {
"version": "4.1.2",
"resolved": "https://registry.npmjs.org/readdirp/-/readdirp-4.1.2.tgz",
"integrity": "sha512-GDhwkLfywWL2s6vEjyhri+eXmfH6j1L7JE27WhqLeYzoh/A3DBaYGEj2H/HFZCn/kMfim73FXxEJTw06WtxQwg==",
"license": "MIT",
"engines": {
"node": ">= 14.18.0"
},
"funding": {
"type": "individual",
"url": "https://paulmillr.com/funding/"
}
},
"node_modules/reflect.getprototypeof": {
"version": "1.0.10",
"resolved": "https://registry.npmjs.org/reflect.getprototypeof/-/reflect.getprototypeof-1.0.10.tgz",
......@@ -6318,6 +6130,15 @@
"node": ">=0.10.0"
}
},
"node_modules/split2": {
"version": "4.2.0",
"resolved": "https://registry.npmjs.org/split2/-/split2-4.2.0.tgz",
"integrity": "sha512-UcjcJOWknrNkF6PLX83qcHM6KHgVKNkV62Y8a5uYDVv9ydGQVwAHMKqHdJje1VTWpljG0WYpCDhrCdAOYH4TWg==",
"license": "ISC",
"engines": {
"node": ">= 10.x"
}
},
"node_modules/stable-hash": {
"version": "0.0.5",
"resolved": "https://registry.npmjs.org/stable-hash/-/stable-hash-0.0.5.tgz",
......@@ -6555,15 +6376,6 @@
"url": "https://opencollective.com/webpack"
}
},
"node_modules/tinyexec": {
"version": "1.0.2",
"resolved": "https://registry.npmjs.org/tinyexec/-/tinyexec-1.0.2.tgz",
"integrity": "sha512-W/KYk+NFhkmsYpuHq5JykngiOCnxeVL8v8dFnqxSD8qEEdRfXk1SDM6JzNqcERbcGYj9tMrDQBYV9cjgnunFIg==",
"license": "MIT",
"engines": {
"node": ">=18"
}
},
"node_modules/tinyglobby": {
"version": "0.2.15",
"resolved": "https://registry.npmjs.org/tinyglobby/-/tinyglobby-0.2.15.tgz",
......@@ -6775,7 +6587,7 @@
"version": "5.9.3",
"resolved": "https://registry.npmjs.org/typescript/-/typescript-5.9.3.tgz",
"integrity": "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==",
"devOptional": true,
"dev": true,
"license": "Apache-2.0",
"peer": true,
"bin": {
......@@ -7027,6 +6839,15 @@
"node": ">=0.10.0"
}
},
"node_modules/xtend": {
"version": "4.0.2",
"resolved": "https://registry.npmjs.org/xtend/-/xtend-4.0.2.tgz",
"integrity": "sha512-LKYU1iAXJXUgAXn9URjiu+MWhyUXHsvfp7mcuYm9dSUKK0/CjtrUwFAxD82/mCWbtLsGjFIad0wIsod4zrTAEQ==",
"license": "MIT",
"engines": {
"node": ">=0.4"
}
},
"node_modules/yallist": {
"version": "3.1.1",
"resolved": "https://registry.npmjs.org/yallist/-/yallist-3.1.1.tgz",
......
......@@ -9,15 +9,15 @@
"lint": "eslint"
},
"dependencies": {
"@prisma/client": "^6.19.1",
"@radix-ui/react-label": "^2.1.8",
"@radix-ui/react-slot": "^1.2.4",
"class-variance-authority": "^0.7.1",
"clsx": "^2.1.1",
"lucide-react": "^0.562.0",
"nanoid": "^5.1.6",
"next": "16.1.1",
"next-themes": "^0.4.6",
"prisma": "^6.19.1",
"pg": "^8.16.3",
"react": "19.2.3",
"react-dom": "19.2.3",
"tailwind-merge": "^3.4.0",
......@@ -26,6 +26,7 @@
"devDependencies": {
"@tailwindcss/postcss": "^4",
"@types/node": "^20",
"@types/pg": "^8.16.0",
"@types/react": "^19",
"@types/react-dom": "^19",
"eslint": "^9",
......
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 100 100">
<rect width="100" height="100" fill="#000000"/>
<text x="50" y="70" font-family="Arial, sans-serif" font-size="60" font-weight="bold" fill="#ffffff" text-anchor="middle">CX</text>
</svg>
-- Create users table
CREATE TABLE IF NOT EXISTS users (
id TEXT PRIMARY KEY,
email TEXT NOT NULL UNIQUE,
name TEXT NOT NULL,
image TEXT,
"createdAt" TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,
"updatedAt" TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP
);
-- Create customers table
CREATE TABLE IF NOT EXISTS customers (
id TEXT PRIMARY KEY,
name TEXT NOT NULL,
email TEXT,
phone TEXT,
company TEXT,
address TEXT,
notes TEXT,
"createdAt" TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,
"updatedAt" TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP
);
-- Create time_entries table
CREATE TABLE IF NOT EXISTS time_entries (
id TEXT PRIMARY KEY,
description TEXT NOT NULL,
"startTime" TIMESTAMP NOT NULL,
"endTime" TIMESTAMP NOT NULL,
duration INTEGER NOT NULL,
"createdAt" TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,
"updatedAt" TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,
"customerId" TEXT NOT NULL REFERENCES customers(id) ON DELETE CASCADE ON UPDATE CASCADE,
"userId" TEXT NOT NULL REFERENCES users(id) ON DELETE CASCADE ON UPDATE CASCADE
);
-- Create indexes
CREATE INDEX IF NOT EXISTS idx_time_entries_customer_id ON time_entries("customerId");
CREATE INDEX IF NOT EXISTS idx_time_entries_user_id ON time_entries("userId");
CREATE INDEX IF NOT EXISTS idx_time_entries_created_at ON time_entries("createdAt");
-- Create function to update updatedAt timestamp
CREATE OR REPLACE FUNCTION update_updated_at_column()
RETURNS TRIGGER AS $$
BEGIN
NEW."updatedAt" = CURRENT_TIMESTAMP;
RETURN NEW;
END;
$$ language 'plpgsql';
-- Create triggers to auto-update updatedAt
CREATE TRIGGER update_users_updated_at BEFORE UPDATE ON users
FOR EACH ROW EXECUTE FUNCTION update_updated_at_column();
CREATE TRIGGER update_customers_updated_at BEFORE UPDATE ON customers
FOR EACH ROW EXECUTE FUNCTION update_updated_at_column();
CREATE TRIGGER update_time_entries_updated_at BEFORE UPDATE ON time_entries
FOR EACH ROW EXECUTE FUNCTION update_updated_at_column();
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment