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: ...@@ -18,7 +18,6 @@ before_script:
build: build:
stage: build stage: build
script: script:
- npx prisma generate
- npm run build - npm run build
artifacts: artifacts:
paths: paths:
......
...@@ -15,11 +15,8 @@ WORKDIR /app ...@@ -15,11 +15,8 @@ WORKDIR /app
COPY --from=deps /app/node_modules ./node_modules COPY --from=deps /app/node_modules ./node_modules
COPY . . COPY . .
# Generate Prisma Client
ENV NEXT_TELEMETRY_DISABLED 1
RUN npx prisma generate
# Build Next.js # Build Next.js
ENV NEXT_TELEMETRY_DISABLED 1
RUN npm run build RUN npm run build
# Production image, copy all the files and run next # Production image, copy all the files and run next
...@@ -39,10 +36,6 @@ COPY --from=builder /app/public ./public ...@@ -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/standalone ./
COPY --from=builder --chown=nextjs:nodejs /app/.next/static ./.next/static 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 USER nextjs
EXPOSE 3000 EXPOSE 3000
......
...@@ -6,7 +6,7 @@ ...@@ -6,7 +6,7 @@
[![Next.js](https://img.shields.io/badge/Next.js-16.1.1-black)](https://nextjs.org/) [![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/) [![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/) [![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/) [![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 ...@@ -58,7 +58,7 @@ Das CoreX Management Dashboard ist eine interne Webanwendung zur Verwaltung von
- **Sprache**: [TypeScript 5.0](https://www.typescriptlang.org/) - **Sprache**: [TypeScript 5.0](https://www.typescriptlang.org/)
- **Styling**: [Tailwind CSS v4](https://tailwindcss.com/) - **Styling**: [Tailwind CSS v4](https://tailwindcss.com/)
- **Datenbank**: [PostgreSQL](https://www.postgresql.org/) - **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**: - **UI Components**:
- [Radix UI](https://www.radix-ui.com/) - [Radix UI](https://www.radix-ui.com/)
- [Lucide Icons](https://lucide.dev/) - [Lucide Icons](https://lucide.dev/)
...@@ -195,23 +195,22 @@ GRANT ALL PRIVILEGES ON DATABASE corex_dashboard TO corex_user; ...@@ -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" DATABASE_URL="postgresql://corex_user:ihr_sicheres_passwort@localhost:5432/corex_dashboard?schema=public"
``` ```
### Migrationen ausführen ### Datenbank-Schema initialisieren
```bash Führen Sie das SQL-Skript aus, um die Tabellen zu erstellen:
# 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)
```bash ```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 ## 💻 Entwicklung
...@@ -230,17 +229,8 @@ npm run start ...@@ -230,17 +229,8 @@ npm run start
# ESLint ausführen # ESLint ausführen
npm run lint npm run lint
# Prisma Client generieren # Datenbank-Verbindung testen
npx prisma generate # Die Verbindung wird automatisch beim Start der App getestet
# 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
``` ```
### Entwicklungsworkflow ### Entwicklungsworkflow
...@@ -301,11 +291,13 @@ corex-dashboard/ ...@@ -301,11 +291,13 @@ corex-dashboard/
│ ├── theme-provider.tsx # Theme-Provider │ ├── theme-provider.tsx # Theme-Provider
│ └── theme-toggle.tsx # Theme-Toggle Button │ └── theme-toggle.tsx # Theme-Toggle Button
├── lib/ # Utility-Funktionen ├── 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.) │ └── utils.ts # Helper-Funktionen (cn, etc.)
├── prisma/ # Prisma Konfiguration ├── scripts/ # Skripte
│ ├── migrations/ # Datenbank-Migrationen │ └── init-db.sql # Datenbank-Initialisierung
│ └── schema.prisma # Datenbank-Schema
├── public/ # Statische Assets ├── public/ # Statische Assets
│ ├── branding/ # Logo-Varianten │ ├── branding/ # Logo-Varianten
│ └── logo.png # Hauptlogo │ └── logo.png # Hauptlogo
...@@ -503,7 +495,7 @@ CMD ["node", "server.js"] ...@@ -503,7 +495,7 @@ CMD ["node", "server.js"]
Das Projekt verwendet GitLab CI/CD für automatische Builds und Tests. Das Projekt verwendet GitLab CI/CD für automatische Builds und Tests.
**Pipeline Stages:** **Pipeline Stages:**
1. **Build**: Kompiliert TypeScript, generiert Prisma Client 1. **Build**: Kompiliert TypeScript und erstellt Production Build
2. **Test**: Führt ESLint aus 2. **Test**: Führt ESLint aus
> ⚠️ **Privates Repository**: CI/CD ist nur für autorisierte GitLab-Nutzer verfügbar. > ⚠️ **Privates Repository**: CI/CD ist nur für autorisierte GitLab-Nutzer verfügbar.
......
import { NextRequest, NextResponse } from "next/server" 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( export async function GET(
request: NextRequest, request: NextRequest,
...@@ -7,25 +8,49 @@ export async function GET( ...@@ -7,25 +8,49 @@ export async function GET(
) { ) {
try { try {
const { id } = await params 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({ 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) { } catch (error) {
console.error("Error fetching customer:", error) console.error("Error fetching customer:", error)
return NextResponse.json({ error: "Failed to fetch customer" }, { status: 500 }) return NextResponse.json({ error: "Failed to fetch customer" }, { status: 500 })
...@@ -45,18 +70,28 @@ export async function PUT( ...@@ -45,18 +70,28 @@ export async function PUT(
return NextResponse.json({ error: "Name is required" }, { status: 400 }) return NextResponse.json({ error: "Name is required" }, { status: 400 })
} }
const customer = await prisma.customer.update({ const result = await query(
where: { id }, `UPDATE customers
data: { SET name = $1, email = $2, phone = $3, company = $4, address = $5, notes = $6, "updatedAt" = $7
WHERE id = $8
RETURNING *`,
[
name, name,
email: email || null, email || null,
phone: phone || null, phone || null,
company: company || null, company || null,
address: address || null, address || null,
notes: notes || 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) return NextResponse.json(customer)
} catch (error) { } catch (error) {
console.error("Error updating customer:", error) console.error("Error updating customer:", error)
...@@ -70,9 +105,11 @@ export async function DELETE( ...@@ -70,9 +105,11 @@ export async function DELETE(
) { ) {
try { try {
const { id } = await params const { id } = await params
await prisma.customer.delete({
where: { id }, await query(
}) `DELETE FROM customers WHERE id = $1`,
[id]
)
return NextResponse.json({ success: true }) return NextResponse.json({ success: true })
} catch (error) { } catch (error) {
...@@ -80,4 +117,3 @@ export async function DELETE( ...@@ -80,4 +117,3 @@ export async function DELETE(
return NextResponse.json({ error: "Failed to delete customer" }, { status: 500 }) return NextResponse.json({ error: "Failed to delete customer" }, { status: 500 })
} }
} }
import { NextRequest, NextResponse } from "next/server" 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) { export async function GET(request: NextRequest) {
try { try {
const searchParams = request.nextUrl.searchParams const searchParams = request.nextUrl.searchParams
const search = searchParams.get("search") const search = searchParams.get("search")
const customers = await prisma.customer.findMany({ let customersQuery = `
where: search SELECT c.*,
? { COALESCE(
OR: [ json_agg(
{ name: { contains: search } }, json_build_object(
{ email: { contains: search } }, 'id', te.id,
{ company: { contains: search } }, 'description', te.description,
], 'startTime', te."startTime",
} 'endTime', te."endTime",
: undefined, 'duration', te.duration,
orderBy: { createdAt: "desc" }, 'createdAt', te."createdAt",
include: { 'updatedAt', te."updatedAt",
timeEntries: { 'customerId', te."customerId",
include: { 'userId', te."userId",
user: { 'user', json_build_object(
select: { name: true, email: true }, 'name', u.name,
}, 'email', u.email
}, )
orderBy: { createdAt: "desc" }, )
}, ) 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 : []),
}
}) })
return NextResponse.json(customers) return NextResponse.json(customers)
...@@ -46,17 +77,27 @@ export async function POST(request: NextRequest) { ...@@ -46,17 +77,27 @@ export async function POST(request: NextRequest) {
} }
try { try {
const customer = await prisma.customer.create({ const id = generateId()
data: { const now = new Date()
name: typeof name === 'string' ? name.trim() : name,
email: email && typeof email === 'string' && email.trim() !== "" ? email.trim() : null, const result = await query(
phone: phone && typeof phone === 'string' && phone.trim() !== "" ? phone.trim() : null, `INSERT INTO customers (id, name, email, phone, company, address, notes, "createdAt", "updatedAt")
company: company && typeof company === 'string' && company.trim() !== "" ? company.trim() : null, VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9)
address: address && typeof address === 'string' && address.trim() !== "" ? address.trim() : null, RETURNING *`,
notes: notes && typeof notes === 'string' && notes.trim() !== "" ? notes.trim() : null, [
}, 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 }) return NextResponse.json(customer, { status: 201 })
} catch (dbError: unknown) { } catch (dbError: unknown) {
console.error("Database error creating customer:", dbError) console.error("Database error creating customer:", dbError)
...@@ -83,4 +124,3 @@ export async function POST(request: NextRequest) { ...@@ -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 { NextRequest, NextResponse } from "next/server"
import { prisma } from "@/lib/prisma" import { query } from "@/lib/db"
export async function DELETE( export async function DELETE(
request: NextRequest, request: NextRequest,
...@@ -7,9 +7,11 @@ export async function DELETE( ...@@ -7,9 +7,11 @@ export async function DELETE(
) { ) {
try { try {
const { id } = await params 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 }) return NextResponse.json({ success: true })
} catch (error) { } catch (error) {
...@@ -17,4 +19,3 @@ export async function DELETE( ...@@ -17,4 +19,3 @@ export async function DELETE(
return NextResponse.json({ error: "Failed to delete time entry" }, { status: 500 }) return NextResponse.json({ error: "Failed to delete time entry" }, { status: 500 })
} }
} }
import { NextRequest, NextResponse } from "next/server" 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) { export async function GET(request: NextRequest) {
try { try {
...@@ -7,21 +8,51 @@ export async function GET(request: NextRequest) { ...@@ -7,21 +8,51 @@ export async function GET(request: NextRequest) {
const customerId = searchParams.get("customerId") const customerId = searchParams.get("customerId")
const userId = searchParams.get("userId") const userId = searchParams.get("userId")
const timeEntries = await prisma.timeEntry.findMany({ let timeEntriesQuery = `
where: { SELECT te.*,
...(customerId ? { customerId } : {}), json_build_object(
...(userId ? { userId } : {}), 'id', c.id,
}, 'name', c.name,
include: { 'company', c.company
customer: { ) as customer,
select: { id: true, name: true, company: true }, json_build_object(
}, 'id', u.id,
user: { 'name', u.name,
select: { id: true, name: true, email: true }, 'email', u.email
}, ) as user
}, FROM time_entries te
orderBy: { createdAt: "desc" }, 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) return NextResponse.json(timeEntries)
} catch (error) { } catch (error) {
...@@ -42,29 +73,45 @@ export async function POST(request: NextRequest) { ...@@ -42,29 +73,45 @@ export async function POST(request: NextRequest) {
) )
} }
const timeEntry = await prisma.timeEntry.create({ const id = generateId()
data: { 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, description,
startTime: new Date(startTime), new Date(startTime),
endTime: new Date(endTime), new Date(endTime),
duration: parseInt(duration), parseInt(String(duration), 10),
customerId, customerId,
userId, userId,
}, now,
include: { now,
customer: { ]
select: { id: true, name: true, company: true }, )
},
user: { const timeEntry = rowToTimeEntry(result.rows[0])
select: { id: true, name: true, email: true },
}, // Get related customer and user
}, const customerResult = await query(
}) `SELECT id, name, company FROM customers WHERE id = $1`,
[customerId]
return NextResponse.json(timeEntry, { status: 201 }) )
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) { } catch (error) {
console.error("Error creating time entry:", error) console.error("Error creating time entry:", error)
return NextResponse.json({ error: "Failed to create time entry" }, { status: 500 }) return NextResponse.json({ error: "Failed to create time entry" }, { status: 500 })
} }
} }
import { NextRequest, NextResponse } from "next/server" 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() { export async function GET() {
try { try {
const users = await prisma.user.findMany({ const result = await query(
orderBy: { createdAt: "desc" }, `SELECT * FROM users ORDER BY "createdAt" DESC`
}) )
const users = result.rows.map(rowToUser)
return NextResponse.json(users) return NextResponse.json(users)
} catch (error) { } catch (error) {
console.error("Error fetching users:", error) console.error("Error fetching users:", error)
...@@ -24,26 +26,29 @@ export async function POST(request: NextRequest) { ...@@ -24,26 +26,29 @@ export async function POST(request: NextRequest) {
} }
// Check if user already exists // Check if user already exists
const existingUser = await prisma.user.findUnique({ const existingResult = await query(
where: { email }, `SELECT * FROM users WHERE email = $1`,
}) [email]
)
if (existingUser) { if (existingResult.rows.length > 0) {
return NextResponse.json(existingUser) return NextResponse.json(rowToUser(existingResult.rows[0]))
} }
const user = await prisma.user.create({ const id = generateId()
data: { const now = new Date()
email,
name,
image: image || null,
},
})
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 }) return NextResponse.json(user, { status: 201 })
} catch (error) { } catch (error) {
console.error("Error creating user:", error) console.error("Error creating user:", error)
return NextResponse.json({ error: "Failed to create user" }, { status: 500 }) 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,
}
}
This diff is collapsed.
...@@ -9,15 +9,15 @@ ...@@ -9,15 +9,15 @@
"lint": "eslint" "lint": "eslint"
}, },
"dependencies": { "dependencies": {
"@prisma/client": "^6.19.1",
"@radix-ui/react-label": "^2.1.8", "@radix-ui/react-label": "^2.1.8",
"@radix-ui/react-slot": "^1.2.4", "@radix-ui/react-slot": "^1.2.4",
"class-variance-authority": "^0.7.1", "class-variance-authority": "^0.7.1",
"clsx": "^2.1.1", "clsx": "^2.1.1",
"lucide-react": "^0.562.0", "lucide-react": "^0.562.0",
"nanoid": "^5.1.6",
"next": "16.1.1", "next": "16.1.1",
"next-themes": "^0.4.6", "next-themes": "^0.4.6",
"prisma": "^6.19.1", "pg": "^8.16.3",
"react": "19.2.3", "react": "19.2.3",
"react-dom": "19.2.3", "react-dom": "19.2.3",
"tailwind-merge": "^3.4.0", "tailwind-merge": "^3.4.0",
...@@ -26,6 +26,7 @@ ...@@ -26,6 +26,7 @@
"devDependencies": { "devDependencies": {
"@tailwindcss/postcss": "^4", "@tailwindcss/postcss": "^4",
"@types/node": "^20", "@types/node": "^20",
"@types/pg": "^8.16.0",
"@types/react": "^19", "@types/react": "^19",
"@types/react-dom": "^19", "@types/react-dom": "^19",
"eslint": "^9", "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