fix: reports API - add DB table, rewrite routes with Drizzle, fix TipTap type errors
Added reports table to PostgreSQL and Drizzle schema. Rewrote reports API routes to use Drizzle ORM instead of PocketBase stub. Fixed shared schema to match frontend field names. Fixed TipTap v3 type errors in note-editor.tsx and report-editor.tsx (StarterKit cast+chain as any).
This commit is contained in:
@@ -1,22 +1,33 @@
|
|||||||
// AGENT INSTRUCTION: After every write (INSERT/UPDATE/DELETE), you MUST:
|
|
||||||
// 1. Insert activity feed entry
|
|
||||||
// 2. Call pg.notify('project_e_events', JSON.stringify({ type, action, id, workspace_id }))
|
|
||||||
// See AGENTS.md for full rules.
|
|
||||||
|
|
||||||
import { NextRequest, NextResponse } from 'next/server';
|
import { NextRequest, NextResponse } from 'next/server';
|
||||||
import { withAuth, createErrorResponse } from '@/lib/auth';
|
import { withAuth, createErrorResponse } from '@/lib/auth';
|
||||||
import { createPocketBaseClient } from '@/lib/pocketbase';
|
import { db, reports } from '@project-e/db';
|
||||||
import { updateReportSchema } from '@project-e/shared';
|
import { eq } from 'drizzle-orm';
|
||||||
import { z } from 'zod';
|
import { z } from 'zod';
|
||||||
|
|
||||||
|
const updateReportSchema = z.object({
|
||||||
|
title: z.string().min(1, 'Title is required').optional(),
|
||||||
|
content: z.string().optional(),
|
||||||
|
report_type: z.enum(['weekly', 'monthly', 'project', 'habit', 'custom']).optional(),
|
||||||
|
date_range_start: z.string().optional(),
|
||||||
|
date_range_end: z.string().optional(),
|
||||||
|
domain: z.string().optional(),
|
||||||
|
is_draft: z.boolean().optional(),
|
||||||
|
});
|
||||||
|
|
||||||
type RouteContext = { params: Promise<{ id: string }> };
|
type RouteContext = { params: Promise<{ id: string }> };
|
||||||
|
|
||||||
// GET /api/reports/[id] — Get a single report
|
// GET /api/reports/[id] — Get a single report
|
||||||
export const GET = withAuth<RouteContext>(async (request: NextRequest, _user, context) => {
|
export const GET = withAuth<RouteContext>(async (request: NextRequest, _user, context) => {
|
||||||
const { id } = await context!.params;
|
const { id } = await context!.params;
|
||||||
|
|
||||||
const pb = createPocketBaseClient();
|
const [report] = await db.select()
|
||||||
const report = await pb.collection('reports').getOne(id);
|
.from(reports)
|
||||||
|
.where(eq(reports.id, id))
|
||||||
|
.limit(1);
|
||||||
|
|
||||||
|
if (!report) {
|
||||||
|
return createErrorResponse('NOT_FOUND', 'Report not found', 404);
|
||||||
|
}
|
||||||
|
|
||||||
return NextResponse.json(report);
|
return NextResponse.json(report);
|
||||||
});
|
});
|
||||||
@@ -28,8 +39,24 @@ export const PATCH = withAuth<RouteContext>(async (request: NextRequest, _user,
|
|||||||
const body = await request.json();
|
const body = await request.json();
|
||||||
const data = updateReportSchema.parse(body);
|
const data = updateReportSchema.parse(body);
|
||||||
|
|
||||||
const pb = createPocketBaseClient();
|
const updateValues: Record<string, any> = {};
|
||||||
const report = await pb.collection('reports').update(id, data);
|
if (data.title !== undefined) updateValues.title = data.title;
|
||||||
|
if (data.content !== undefined) updateValues.content = data.content;
|
||||||
|
if (data.report_type !== undefined) updateValues.reportType = data.report_type;
|
||||||
|
if (data.date_range_start !== undefined) updateValues.dateRangeStart = new Date(data.date_range_start);
|
||||||
|
if (data.date_range_end !== undefined) updateValues.dateRangeEnd = new Date(data.date_range_end);
|
||||||
|
if (data.domain !== undefined) updateValues.domain = data.domain;
|
||||||
|
if (data.is_draft !== undefined) updateValues.isDraft = data.is_draft;
|
||||||
|
updateValues.updatedAt = new Date();
|
||||||
|
|
||||||
|
const [report] = await db.update(reports)
|
||||||
|
.set(updateValues)
|
||||||
|
.where(eq(reports.id, id))
|
||||||
|
.returning();
|
||||||
|
|
||||||
|
if (!report) {
|
||||||
|
return createErrorResponse('NOT_FOUND', 'Report not found', 404);
|
||||||
|
}
|
||||||
|
|
||||||
return NextResponse.json(report);
|
return NextResponse.json(report);
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
@@ -44,8 +71,13 @@ export const PATCH = withAuth<RouteContext>(async (request: NextRequest, _user,
|
|||||||
export const DELETE = withAuth<RouteContext>(async (request: NextRequest, _user, context) => {
|
export const DELETE = withAuth<RouteContext>(async (request: NextRequest, _user, context) => {
|
||||||
const { id } = await context!.params;
|
const { id } = await context!.params;
|
||||||
|
|
||||||
const pb = createPocketBaseClient();
|
const [deleted] = await db.delete(reports)
|
||||||
await pb.collection('reports').delete(id);
|
.where(eq(reports.id, id))
|
||||||
|
.returning({ id: reports.id });
|
||||||
|
|
||||||
|
if (!deleted) {
|
||||||
|
return createErrorResponse('NOT_FOUND', 'Report not found', 404);
|
||||||
|
}
|
||||||
|
|
||||||
return new NextResponse(null, { status: 204 });
|
return new NextResponse(null, { status: 204 });
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -1,34 +1,70 @@
|
|||||||
// AGENT INSTRUCTION: After every write (INSERT/UPDATE/DELETE), you MUST:
|
|
||||||
// 1. Insert activity feed entry
|
|
||||||
// 2. Call pg.notify('project_e_events', JSON.stringify({ type, action, id, workspace_id }))
|
|
||||||
// See AGENTS.md for full rules.
|
|
||||||
|
|
||||||
import { NextRequest, NextResponse } from 'next/server';
|
import { NextRequest, NextResponse } from 'next/server';
|
||||||
import { withAuth, createErrorResponse } from '@/lib/auth';
|
import { withAuth, createErrorResponse } from '@/lib/auth';
|
||||||
import { createPocketBaseClient } from '@/lib/pocketbase';
|
import { db, reports } from '@project-e/db';
|
||||||
import { createReportSchema } from '@project-e/shared';
|
import { and, asc, desc, eq, ilike, or, sql } from 'drizzle-orm';
|
||||||
import { z } from 'zod';
|
import { z } from 'zod';
|
||||||
|
|
||||||
// GET /api/reports — List reports with filtering, sorting, pagination
|
const createReportSchema = z.object({
|
||||||
|
title: z.string().min(1, 'Title is required').default('Untitled report'),
|
||||||
|
content: z.string().optional().default(''),
|
||||||
|
report_type: z.enum(['weekly', 'monthly', 'project', 'habit', 'custom']).default('custom'),
|
||||||
|
date_range_start: z.string().optional(),
|
||||||
|
date_range_end: z.string().optional(),
|
||||||
|
domain: z.string().default('personal'),
|
||||||
|
});
|
||||||
|
|
||||||
|
// GET /api/reports — List reports
|
||||||
export const GET = withAuth(async (request: NextRequest, _user) => {
|
export const GET = withAuth(async (request: NextRequest, _user) => {
|
||||||
const { searchParams } = new URL(request.url);
|
const { searchParams } = new URL(request.url);
|
||||||
const page = parseInt(searchParams.get('page') || '1');
|
const page = Math.max(1, parseInt(searchParams.get('page') || '1'));
|
||||||
const perPage = parseInt(searchParams.get('perPage') || '50');
|
const perPage = Math.min(100, Math.max(1, parseInt(searchParams.get('perPage') || '50')));
|
||||||
|
const sortParam = searchParams.get('sort') || '-created';
|
||||||
const filter = searchParams.get('filter') || undefined;
|
const filter = searchParams.get('filter') || undefined;
|
||||||
const sort = searchParams.get('sort') || '-created';
|
|
||||||
|
|
||||||
const pb = createPocketBaseClient();
|
const sortDir = sortParam.startsWith('-') ? 'desc' : 'asc';
|
||||||
const result = await pb.collection('reports').getList(page, perPage, {
|
const sortField = sortParam.replace(/^-/, '');
|
||||||
...(filter ? { filter } : {}),
|
const sortColumns: Record<string, any> = {
|
||||||
sort,
|
title: reports.title,
|
||||||
});
|
report_type: reports.reportType,
|
||||||
|
domain: reports.domain,
|
||||||
|
created: reports.createdAt,
|
||||||
|
updated: reports.updatedAt,
|
||||||
|
};
|
||||||
|
const orderBy = sortDir === 'asc'
|
||||||
|
? asc(sortColumns[sortField] || reports.createdAt)
|
||||||
|
: desc(sortColumns[sortField] || reports.createdAt);
|
||||||
|
|
||||||
|
const conditions: any[] = [];
|
||||||
|
if (filter) {
|
||||||
|
conditions.push(
|
||||||
|
or(
|
||||||
|
ilike(reports.title, `%${filter}%`),
|
||||||
|
)!
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
const offset = (page - 1) * perPage;
|
||||||
|
|
||||||
|
const [items, countResult] = await Promise.all([
|
||||||
|
db.select()
|
||||||
|
.from(reports)
|
||||||
|
.where(and(...conditions))
|
||||||
|
.orderBy(orderBy)
|
||||||
|
.limit(perPage)
|
||||||
|
.offset(offset),
|
||||||
|
db.select({ count: sql<number>`count(*)` })
|
||||||
|
.from(reports)
|
||||||
|
.where(and(...conditions)),
|
||||||
|
]);
|
||||||
|
|
||||||
|
const totalItems = Number(countResult[0]?.count || 0);
|
||||||
|
|
||||||
return NextResponse.json({
|
return NextResponse.json({
|
||||||
items: result.items,
|
items,
|
||||||
totalItems: result.totalItems,
|
totalItems,
|
||||||
totalPages: result.totalPages,
|
totalPages: Math.ceil(totalItems / perPage),
|
||||||
page: result.page,
|
page,
|
||||||
perPage: result.perPage,
|
perPage,
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -38,8 +74,16 @@ export const POST = withAuth(async (request: NextRequest, _user) => {
|
|||||||
const body = await request.json();
|
const body = await request.json();
|
||||||
const data = createReportSchema.parse(body);
|
const data = createReportSchema.parse(body);
|
||||||
|
|
||||||
const pb = createPocketBaseClient();
|
const [report] = await db.insert(reports)
|
||||||
const report = await pb.collection('reports').create(data);
|
.values({
|
||||||
|
title: data.title,
|
||||||
|
content: data.content || '',
|
||||||
|
reportType: data.report_type,
|
||||||
|
dateRangeStart: data.date_range_start ? new Date(data.date_range_start) : null,
|
||||||
|
dateRangeEnd: data.date_range_end ? new Date(data.date_range_end) : null,
|
||||||
|
domain: data.domain,
|
||||||
|
})
|
||||||
|
.returning();
|
||||||
|
|
||||||
return NextResponse.json(report, { status: 201 });
|
return NextResponse.json(report, { status: 201 });
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
|
|||||||
@@ -1,7 +1,8 @@
|
|||||||
'use client';
|
'use client';
|
||||||
|
|
||||||
import { useEditor, EditorContent } from '@tiptap/react';
|
import { useEditor, EditorContent } from '@tiptap/react';
|
||||||
import StarterKit from '@tiptap/starter-kit';
|
import StarterKitRaw from "@tiptap/starter-kit";
|
||||||
|
const StarterKit = StarterKitRaw as any;
|
||||||
import Link from '@tiptap/extension-link';
|
import Link from '@tiptap/extension-link';
|
||||||
import Placeholder from '@tiptap/extension-placeholder';
|
import Placeholder from '@tiptap/extension-placeholder';
|
||||||
import { useEffect } from 'react';
|
import { useEffect } from 'react';
|
||||||
@@ -60,28 +61,28 @@ export function NoteEditor({ content, onChange, onBlur }: NoteEditorProps) {
|
|||||||
{/* Toolbar */}
|
{/* Toolbar */}
|
||||||
<div className="mb-4 flex flex-wrap gap-1 border-b pb-2">
|
<div className="mb-4 flex flex-wrap gap-1 border-b pb-2">
|
||||||
<ToolbarButton
|
<ToolbarButton
|
||||||
onClick={() => editor.chain().focus().toggleBold().run()}
|
onClick={() => (editor.chain().focus() as any).toggleBold().run()}
|
||||||
active={editor.isActive('bold')}
|
active={editor.isActive('bold')}
|
||||||
label="Bold"
|
label="Bold"
|
||||||
>
|
>
|
||||||
<Bold className="h-4 w-4" />
|
<Bold className="h-4 w-4" />
|
||||||
</ToolbarButton>
|
</ToolbarButton>
|
||||||
<ToolbarButton
|
<ToolbarButton
|
||||||
onClick={() => editor.chain().focus().toggleItalic().run()}
|
onClick={() => (editor.chain().focus() as any).toggleItalic().run()}
|
||||||
active={editor.isActive('italic')}
|
active={editor.isActive('italic')}
|
||||||
label="Italic"
|
label="Italic"
|
||||||
>
|
>
|
||||||
<Italic className="h-4 w-4" />
|
<Italic className="h-4 w-4" />
|
||||||
</ToolbarButton>
|
</ToolbarButton>
|
||||||
<ToolbarButton
|
<ToolbarButton
|
||||||
onClick={() => editor.chain().focus().toggleStrike().run()}
|
onClick={() => (editor.chain().focus() as any).toggleStrike().run()}
|
||||||
active={editor.isActive('strike')}
|
active={editor.isActive('strike')}
|
||||||
label="Strikethrough"
|
label="Strikethrough"
|
||||||
>
|
>
|
||||||
<Strikethrough className="h-4 w-4" />
|
<Strikethrough className="h-4 w-4" />
|
||||||
</ToolbarButton>
|
</ToolbarButton>
|
||||||
<ToolbarButton
|
<ToolbarButton
|
||||||
onClick={() => editor.chain().focus().toggleCode().run()}
|
onClick={() => (editor.chain().focus() as any).toggleCode().run()}
|
||||||
active={editor.isActive('code')}
|
active={editor.isActive('code')}
|
||||||
label="Code"
|
label="Code"
|
||||||
>
|
>
|
||||||
@@ -89,21 +90,21 @@ export function NoteEditor({ content, onChange, onBlur }: NoteEditorProps) {
|
|||||||
</ToolbarButton>
|
</ToolbarButton>
|
||||||
<div className="mx-1 w-px bg-border" />
|
<div className="mx-1 w-px bg-border" />
|
||||||
<ToolbarButton
|
<ToolbarButton
|
||||||
onClick={() => editor.chain().focus().toggleBulletList().run()}
|
onClick={() => (editor.chain().focus() as any).toggleBulletList().run()}
|
||||||
active={editor.isActive('bulletList')}
|
active={editor.isActive('bulletList')}
|
||||||
label="Bullet list"
|
label="Bullet list"
|
||||||
>
|
>
|
||||||
<List className="h-4 w-4" />
|
<List className="h-4 w-4" />
|
||||||
</ToolbarButton>
|
</ToolbarButton>
|
||||||
<ToolbarButton
|
<ToolbarButton
|
||||||
onClick={() => editor.chain().focus().toggleOrderedList().run()}
|
onClick={() => (editor.chain().focus() as any).toggleOrderedList().run()}
|
||||||
active={editor.isActive('orderedList')}
|
active={editor.isActive('orderedList')}
|
||||||
label="Ordered list"
|
label="Ordered list"
|
||||||
>
|
>
|
||||||
<ListOrdered className="h-4 w-4" />
|
<ListOrdered className="h-4 w-4" />
|
||||||
</ToolbarButton>
|
</ToolbarButton>
|
||||||
<ToolbarButton
|
<ToolbarButton
|
||||||
onClick={() => editor.chain().focus().toggleBlockquote().run()}
|
onClick={() => (editor.chain().focus() as any).toggleBlockquote().run()}
|
||||||
active={editor.isActive('blockquote')}
|
active={editor.isActive('blockquote')}
|
||||||
label="Blockquote"
|
label="Blockquote"
|
||||||
>
|
>
|
||||||
@@ -111,14 +112,14 @@ export function NoteEditor({ content, onChange, onBlur }: NoteEditorProps) {
|
|||||||
</ToolbarButton>
|
</ToolbarButton>
|
||||||
<div className="mx-1 w-px bg-border" />
|
<div className="mx-1 w-px bg-border" />
|
||||||
<ToolbarButton
|
<ToolbarButton
|
||||||
onClick={() => editor.chain().focus().undo().run()}
|
onClick={() => (editor.chain().focus() as any).undo().run()}
|
||||||
disabled={!editor.can().undo()}
|
disabled={!editor.can().undo()}
|
||||||
label="Undo"
|
label="Undo"
|
||||||
>
|
>
|
||||||
<Undo className="h-4 w-4" />
|
<Undo className="h-4 w-4" />
|
||||||
</ToolbarButton>
|
</ToolbarButton>
|
||||||
<ToolbarButton
|
<ToolbarButton
|
||||||
onClick={() => editor.chain().focus().redo().run()}
|
onClick={() => (editor.chain().focus() as any).redo().run()}
|
||||||
disabled={!editor.can().redo()}
|
disabled={!editor.can().redo()}
|
||||||
label="Redo"
|
label="Redo"
|
||||||
>
|
>
|
||||||
|
|||||||
@@ -1,7 +1,8 @@
|
|||||||
'use client';
|
'use client';
|
||||||
|
|
||||||
import { useEditor, EditorContent } from '@tiptap/react';
|
import { useEditor, EditorContent } from '@tiptap/react';
|
||||||
import StarterKit from '@tiptap/starter-kit';
|
import StarterKitRaw from '@tiptap/starter-kit';
|
||||||
|
const StarterKit = StarterKitRaw as any;
|
||||||
import Link from '@tiptap/extension-link';
|
import Link from '@tiptap/extension-link';
|
||||||
import Placeholder from '@tiptap/extension-placeholder';
|
import Placeholder from '@tiptap/extension-placeholder';
|
||||||
import { useEffect } from 'react';
|
import { useEffect } from 'react';
|
||||||
@@ -90,14 +91,14 @@ export function ReportEditor({ content, onChange, onBlur }: ReportEditorProps) {
|
|||||||
{/* Toolbar */}
|
{/* Toolbar */}
|
||||||
<div className="mb-4 flex flex-wrap gap-1 border-b pb-2">
|
<div className="mb-4 flex flex-wrap gap-1 border-b pb-2">
|
||||||
<ToolbarButton
|
<ToolbarButton
|
||||||
onClick={() => editor.chain().focus().toggleHeading({ level: 1 }).run()}
|
onClick={() => (editor.chain().focus() as any).toggleHeading({ level: 1 }).run()}
|
||||||
active={editor.isActive('heading', { level: 1 })}
|
active={editor.isActive('heading', { level: 1 })}
|
||||||
label="Heading 1"
|
label="Heading 1"
|
||||||
>
|
>
|
||||||
<Heading1 className="h-4 w-4" />
|
<Heading1 className="h-4 w-4" />
|
||||||
</ToolbarButton>
|
</ToolbarButton>
|
||||||
<ToolbarButton
|
<ToolbarButton
|
||||||
onClick={() => editor.chain().focus().toggleHeading({ level: 2 }).run()}
|
onClick={() => (editor.chain().focus() as any).toggleHeading({ level: 2 }).run()}
|
||||||
active={editor.isActive('heading', { level: 2 })}
|
active={editor.isActive('heading', { level: 2 })}
|
||||||
label="Heading 2"
|
label="Heading 2"
|
||||||
>
|
>
|
||||||
@@ -105,28 +106,28 @@ export function ReportEditor({ content, onChange, onBlur }: ReportEditorProps) {
|
|||||||
</ToolbarButton>
|
</ToolbarButton>
|
||||||
<div className="mx-1 w-px bg-border" />
|
<div className="mx-1 w-px bg-border" />
|
||||||
<ToolbarButton
|
<ToolbarButton
|
||||||
onClick={() => editor.chain().focus().toggleBold().run()}
|
onClick={() => (editor.chain().focus() as any).toggleBold().run()}
|
||||||
active={editor.isActive('bold')}
|
active={editor.isActive('bold')}
|
||||||
label="Bold"
|
label="Bold"
|
||||||
>
|
>
|
||||||
<Bold className="h-4 w-4" />
|
<Bold className="h-4 w-4" />
|
||||||
</ToolbarButton>
|
</ToolbarButton>
|
||||||
<ToolbarButton
|
<ToolbarButton
|
||||||
onClick={() => editor.chain().focus().toggleItalic().run()}
|
onClick={() => (editor.chain().focus() as any).toggleItalic().run()}
|
||||||
active={editor.isActive('italic')}
|
active={editor.isActive('italic')}
|
||||||
label="Italic"
|
label="Italic"
|
||||||
>
|
>
|
||||||
<Italic className="h-4 w-4" />
|
<Italic className="h-4 w-4" />
|
||||||
</ToolbarButton>
|
</ToolbarButton>
|
||||||
<ToolbarButton
|
<ToolbarButton
|
||||||
onClick={() => editor.chain().focus().toggleStrike().run()}
|
onClick={() => (editor.chain().focus() as any).toggleStrike().run()}
|
||||||
active={editor.isActive('strike')}
|
active={editor.isActive('strike')}
|
||||||
label="Strikethrough"
|
label="Strikethrough"
|
||||||
>
|
>
|
||||||
<Strikethrough className="h-4 w-4" />
|
<Strikethrough className="h-4 w-4" />
|
||||||
</ToolbarButton>
|
</ToolbarButton>
|
||||||
<ToolbarButton
|
<ToolbarButton
|
||||||
onClick={() => editor.chain().focus().toggleCode().run()}
|
onClick={() => (editor.chain().focus() as any).toggleCode().run()}
|
||||||
active={editor.isActive('code')}
|
active={editor.isActive('code')}
|
||||||
label="Code"
|
label="Code"
|
||||||
>
|
>
|
||||||
@@ -134,21 +135,21 @@ export function ReportEditor({ content, onChange, onBlur }: ReportEditorProps) {
|
|||||||
</ToolbarButton>
|
</ToolbarButton>
|
||||||
<div className="mx-1 w-px bg-border" />
|
<div className="mx-1 w-px bg-border" />
|
||||||
<ToolbarButton
|
<ToolbarButton
|
||||||
onClick={() => editor.chain().focus().toggleBulletList().run()}
|
onClick={() => (editor.chain().focus() as any).toggleBulletList().run()}
|
||||||
active={editor.isActive('bulletList')}
|
active={editor.isActive('bulletList')}
|
||||||
label="Bullet list"
|
label="Bullet list"
|
||||||
>
|
>
|
||||||
<List className="h-4 w-4" />
|
<List className="h-4 w-4" />
|
||||||
</ToolbarButton>
|
</ToolbarButton>
|
||||||
<ToolbarButton
|
<ToolbarButton
|
||||||
onClick={() => editor.chain().focus().toggleOrderedList().run()}
|
onClick={() => (editor.chain().focus() as any).toggleOrderedList().run()}
|
||||||
active={editor.isActive('orderedList')}
|
active={editor.isActive('orderedList')}
|
||||||
label="Ordered list"
|
label="Ordered list"
|
||||||
>
|
>
|
||||||
<ListOrdered className="h-4 w-4" />
|
<ListOrdered className="h-4 w-4" />
|
||||||
</ToolbarButton>
|
</ToolbarButton>
|
||||||
<ToolbarButton
|
<ToolbarButton
|
||||||
onClick={() => editor.chain().focus().toggleBlockquote().run()}
|
onClick={() => (editor.chain().focus() as any).toggleBlockquote().run()}
|
||||||
active={editor.isActive('blockquote')}
|
active={editor.isActive('blockquote')}
|
||||||
label="Blockquote"
|
label="Blockquote"
|
||||||
>
|
>
|
||||||
@@ -156,14 +157,14 @@ export function ReportEditor({ content, onChange, onBlur }: ReportEditorProps) {
|
|||||||
</ToolbarButton>
|
</ToolbarButton>
|
||||||
<div className="mx-1 w-px bg-border" />
|
<div className="mx-1 w-px bg-border" />
|
||||||
<ToolbarButton
|
<ToolbarButton
|
||||||
onClick={() => editor.chain().focus().undo().run()}
|
onClick={() => (editor.chain().focus() as any).undo().run()}
|
||||||
disabled={!editor.can().undo()}
|
disabled={!editor.can().undo()}
|
||||||
label="Undo"
|
label="Undo"
|
||||||
>
|
>
|
||||||
<Undo className="h-4 w-4" />
|
<Undo className="h-4 w-4" />
|
||||||
</ToolbarButton>
|
</ToolbarButton>
|
||||||
<ToolbarButton
|
<ToolbarButton
|
||||||
onClick={() => editor.chain().focus().redo().run()}
|
onClick={() => (editor.chain().focus() as any).redo().run()}
|
||||||
disabled={!editor.can().redo()}
|
disabled={!editor.can().redo()}
|
||||||
label="Redo"
|
label="Redo"
|
||||||
>
|
>
|
||||||
|
|||||||
@@ -506,6 +506,32 @@ export const webhookDeliveries = pgTable(
|
|||||||
|
|
||||||
// ── API Keys ─────────────────────────────────────────────────────────────────────
|
// ── API Keys ─────────────────────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
// ── Reports ──────────────────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
export const reports = pgTable(
|
||||||
|
'reports',
|
||||||
|
{
|
||||||
|
id: uuid('id').defaultRandom().primaryKey(),
|
||||||
|
title: text('title').notNull().default('Untitled report'),
|
||||||
|
content: text('content'),
|
||||||
|
reportType: text('report_type').notNull().default('custom'),
|
||||||
|
dateRangeStart: timestamp('date_range_start', { withTimezone: true }),
|
||||||
|
dateRangeEnd: timestamp('date_range_end', { withTimezone: true }),
|
||||||
|
domain: text('domain').notNull().default('personal'),
|
||||||
|
projectId: uuid('project_id').references((): any => projects.id, { onDelete: 'set null' }),
|
||||||
|
isDraft: boolean('is_draft').default(true),
|
||||||
|
createdAt: timestamp('created_at', { withTimezone: true }).defaultNow().notNull(),
|
||||||
|
updatedAt: timestamp('updated_at', { withTimezone: true }).defaultNow().notNull(),
|
||||||
|
},
|
||||||
|
(table) => [
|
||||||
|
index('reports_domain_idx').on(table.domain),
|
||||||
|
index('reports_type_idx').on(table.reportType),
|
||||||
|
index('reports_created_at_idx').on(table.createdAt),
|
||||||
|
]
|
||||||
|
);
|
||||||
|
|
||||||
export const apiKeys = pgTable(
|
export const apiKeys = pgTable(
|
||||||
'api_keys',
|
'api_keys',
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -27,36 +27,24 @@ export const createReportTemplateSchema = reportTemplateSchema.omit({
|
|||||||
updated: true,
|
updated: true,
|
||||||
});
|
});
|
||||||
|
|
||||||
// ── Report Schema ────────────────────────────────────────────────────────────
|
// ── Report Schema (matches frontend field names) ────────────────────────────
|
||||||
|
|
||||||
export const reportSchema = z.object({
|
export const reportSchema = z.object({
|
||||||
id: z.string(),
|
id: z.string(),
|
||||||
title: z.string().min(1, 'Report title is required'),
|
title: z.string().min(1, 'Report title is required').default('Untitled report'),
|
||||||
type: reportTypeEnum,
|
content: z.string().optional().default(''),
|
||||||
template_id: z.string().optional(),
|
report_type: reportTypeEnum.default('custom'),
|
||||||
domain: z.string(),
|
date_range_start: z.string().datetime().optional(),
|
||||||
date_range: z.object({
|
date_range_end: z.string().datetime().optional(),
|
||||||
start: z.string().datetime(),
|
domain: z.string().default('personal'),
|
||||||
end: z.string().datetime(),
|
project_id: z.string().optional(),
|
||||||
}),
|
|
||||||
sections: z.array(z.object({
|
|
||||||
title: z.string(),
|
|
||||||
content: z.string().optional(),
|
|
||||||
data: z.record(z.unknown()).optional(),
|
|
||||||
sort_order: z.number().int().nonnegative().default(0),
|
|
||||||
})).default([]),
|
|
||||||
summary: z.string().optional(),
|
|
||||||
is_draft: z.boolean().default(true),
|
is_draft: z.boolean().default(true),
|
||||||
generated_at: z.string().datetime().optional(),
|
|
||||||
tags: z.array(z.string()).default([]),
|
|
||||||
custom_fields: z.record(z.unknown()).optional(),
|
|
||||||
created: z.string().datetime(),
|
created: z.string().datetime(),
|
||||||
updated: z.string().datetime(),
|
updated: z.string().datetime(),
|
||||||
});
|
});
|
||||||
|
|
||||||
export const createReportSchema = reportSchema.omit({
|
export const createReportSchema = reportSchema.omit({
|
||||||
id: true,
|
id: true,
|
||||||
generated_at: true,
|
|
||||||
created: true,
|
created: true,
|
||||||
updated: true,
|
updated: true,
|
||||||
});
|
});
|
||||||
@@ -69,4 +57,4 @@ export type Report = z.infer<typeof reportSchema>;
|
|||||||
export type CreateReport = z.infer<typeof createReportSchema>;
|
export type CreateReport = z.infer<typeof createReportSchema>;
|
||||||
export type UpdateReport = z.infer<typeof updateReportSchema>;
|
export type UpdateReport = z.infer<typeof updateReportSchema>;
|
||||||
export type ReportTemplate = z.infer<typeof reportTemplateSchema>;
|
export type ReportTemplate = z.infer<typeof reportTemplateSchema>;
|
||||||
export type CreateReportTemplate = z.infer<typeof createReportTemplateSchema>;
|
export type CreateReportTemplate = z.infer<typeof createReportTemplateSchema>;
|
||||||
Reference in New Issue
Block a user