From e3e5e89df629c20806dbec637fb2e7c06b4702b4 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E6=9D=8E=E8=B6=8514pro?= Date: Sun, 25 Jan 2026 21:55:29 +0800 Subject: [PATCH] fix: force dynamic for meeting page --- .bolt/config.json | 3 + .bolt/ignore | 2 + .bolt/prompt | 9 + .eslintrc.json | 3 + .gitignore | 36 + .vercelignore | 7 + README.md | 1 + app/admin/metrics/page.tsx | 332 + app/admin/templates/page.tsx | 436 + app/admin/users/page.tsx | 251 + app/analysis/page.tsx | 411 + app/api/checkins/route.ts | 136 + app/api/krs/[id]/route.ts | 109 + app/api/krs/route.ts | 114 + app/api/meetings/[id]/route.ts | 70 + app/api/meetings/route.ts | 59 + app/api/metrics/[id]/route.ts | 67 + app/api/metrics/route.ts | 48 + app/api/objectives/[id]/route.ts | 68 + app/api/objectives/route.ts | 48 + app/api/projects/[id]/members/route.ts | 66 + app/api/projects/[id]/route.ts | 91 + app/api/projects/route.ts | 58 + app/api/seed/route.ts | 284 + app/api/templates/[id]/route.ts | 68 + app/api/templates/route.ts | 49 + app/api/users/[id]/route.ts | 60 + app/api/users/route.ts | 38 + app/globals.css | 82 + app/krs/[id]/client-page.tsx | 651 ++ app/krs/[id]/page.tsx | 8 + app/layout.tsx | 27 + app/meeting/page.tsx | 550 ++ app/my-krs/page.tsx | 202 + app/page.tsx | 311 + app/projects/[id]/client-page.tsx | 546 ++ app/projects/[id]/krs/new/page.tsx | 435 + app/projects/[id]/objectives/new/page.tsx | 171 + app/projects/[id]/page.tsx | 8 + app/projects/new/page.tsx | 215 + app/projects/page.tsx | 129 + components.json | 20 + components/app-shell.tsx | 69 + components/init-wizard.tsx | 106 + components/main-nav.tsx | 98 + components/progress-ring.tsx | 69 + components/status-badge.tsx | 57 + components/ui/accordion.tsx | 58 + components/ui/alert-dialog.tsx | 141 + components/ui/alert.tsx | 59 + components/ui/aspect-ratio.tsx | 7 + components/ui/avatar.tsx | 50 + components/ui/badge.tsx | 36 + components/ui/breadcrumb.tsx | 115 + components/ui/button.tsx | 56 + components/ui/calendar.tsx | 66 + components/ui/card.tsx | 86 + components/ui/carousel.tsx | 262 + components/ui/chart.tsx | 365 + components/ui/checkbox.tsx | 30 + components/ui/collapsible.tsx | 11 + components/ui/command.tsx | 155 + components/ui/context-menu.tsx | 200 + components/ui/dialog.tsx | 122 + components/ui/drawer.tsx | 118 + components/ui/dropdown-menu.tsx | 200 + components/ui/form.tsx | 179 + components/ui/hover-card.tsx | 29 + components/ui/input-otp.tsx | 71 + components/ui/input.tsx | 25 + components/ui/label.tsx | 26 + components/ui/menubar.tsx | 236 + components/ui/navigation-menu.tsx | 128 + components/ui/pagination.tsx | 117 + components/ui/popover.tsx | 31 + components/ui/progress.tsx | 28 + components/ui/radio-group.tsx | 44 + components/ui/resizable.tsx | 45 + components/ui/scroll-area.tsx | 48 + components/ui/select.tsx | 160 + components/ui/separator.tsx | 31 + components/ui/sheet.tsx | 140 + components/ui/skeleton.tsx | 15 + components/ui/slider.tsx | 28 + components/ui/sonner.tsx | 31 + components/ui/switch.tsx | 29 + components/ui/table.tsx | 117 + components/ui/tabs.tsx | 55 + components/ui/textarea.tsx | 24 + components/ui/toast.tsx | 129 + components/ui/toaster.tsx | 35 + components/ui/toggle-group.tsx | 61 + components/ui/toggle.tsx | 45 + components/ui/tooltip.tsx | 30 + components/user-context.tsx | 85 + components/user-switcher.tsx | 58 + hooks/use-toast.ts | 191 + lib/constants.ts | 33 + lib/error-utils.ts | 27 + lib/kr-utils.ts | 94 + lib/supabase.ts | 6 + lib/utils.ts | 6 + netlify.toml | 6 + next.config.js | 9 + package-lock.json | 7263 +++++++++++++++++ package.json | 74 + postcss.config.js | 6 + .../20260123142127_create_okr_tables.sql | 166 + tailwind.config.ts | 90 + tsconfig.json | 27 + types/database.ts | 179 + vercel.json | 1 + 112 files changed, 19073 insertions(+) create mode 100644 .bolt/config.json create mode 100644 .bolt/ignore create mode 100644 .bolt/prompt create mode 100644 .eslintrc.json create mode 100644 .gitignore create mode 100644 .vercelignore create mode 100644 README.md create mode 100644 app/admin/metrics/page.tsx create mode 100644 app/admin/templates/page.tsx create mode 100644 app/admin/users/page.tsx create mode 100644 app/analysis/page.tsx create mode 100644 app/api/checkins/route.ts create mode 100644 app/api/krs/[id]/route.ts create mode 100644 app/api/krs/route.ts create mode 100644 app/api/meetings/[id]/route.ts create mode 100644 app/api/meetings/route.ts create mode 100644 app/api/metrics/[id]/route.ts create mode 100644 app/api/metrics/route.ts create mode 100644 app/api/objectives/[id]/route.ts create mode 100644 app/api/objectives/route.ts create mode 100644 app/api/projects/[id]/members/route.ts create mode 100644 app/api/projects/[id]/route.ts create mode 100644 app/api/projects/route.ts create mode 100644 app/api/seed/route.ts create mode 100644 app/api/templates/[id]/route.ts create mode 100644 app/api/templates/route.ts create mode 100644 app/api/users/[id]/route.ts create mode 100644 app/api/users/route.ts create mode 100644 app/globals.css create mode 100644 app/krs/[id]/client-page.tsx create mode 100644 app/krs/[id]/page.tsx create mode 100644 app/layout.tsx create mode 100644 app/meeting/page.tsx create mode 100644 app/my-krs/page.tsx create mode 100644 app/page.tsx create mode 100644 app/projects/[id]/client-page.tsx create mode 100644 app/projects/[id]/krs/new/page.tsx create mode 100644 app/projects/[id]/objectives/new/page.tsx create mode 100644 app/projects/[id]/page.tsx create mode 100644 app/projects/new/page.tsx create mode 100644 app/projects/page.tsx create mode 100644 components.json create mode 100644 components/app-shell.tsx create mode 100644 components/init-wizard.tsx create mode 100644 components/main-nav.tsx create mode 100644 components/progress-ring.tsx create mode 100644 components/status-badge.tsx create mode 100644 components/ui/accordion.tsx create mode 100644 components/ui/alert-dialog.tsx create mode 100644 components/ui/alert.tsx create mode 100644 components/ui/aspect-ratio.tsx create mode 100644 components/ui/avatar.tsx create mode 100644 components/ui/badge.tsx create mode 100644 components/ui/breadcrumb.tsx create mode 100644 components/ui/button.tsx create mode 100644 components/ui/calendar.tsx create mode 100644 components/ui/card.tsx create mode 100644 components/ui/carousel.tsx create mode 100644 components/ui/chart.tsx create mode 100644 components/ui/checkbox.tsx create mode 100644 components/ui/collapsible.tsx create mode 100644 components/ui/command.tsx create mode 100644 components/ui/context-menu.tsx create mode 100644 components/ui/dialog.tsx create mode 100644 components/ui/drawer.tsx create mode 100644 components/ui/dropdown-menu.tsx create mode 100644 components/ui/form.tsx create mode 100644 components/ui/hover-card.tsx create mode 100644 components/ui/input-otp.tsx create mode 100644 components/ui/input.tsx create mode 100644 components/ui/label.tsx create mode 100644 components/ui/menubar.tsx create mode 100644 components/ui/navigation-menu.tsx create mode 100644 components/ui/pagination.tsx create mode 100644 components/ui/popover.tsx create mode 100644 components/ui/progress.tsx create mode 100644 components/ui/radio-group.tsx create mode 100644 components/ui/resizable.tsx create mode 100644 components/ui/scroll-area.tsx create mode 100644 components/ui/select.tsx create mode 100644 components/ui/separator.tsx create mode 100644 components/ui/sheet.tsx create mode 100644 components/ui/skeleton.tsx create mode 100644 components/ui/slider.tsx create mode 100644 components/ui/sonner.tsx create mode 100644 components/ui/switch.tsx create mode 100644 components/ui/table.tsx create mode 100644 components/ui/tabs.tsx create mode 100644 components/ui/textarea.tsx create mode 100644 components/ui/toast.tsx create mode 100644 components/ui/toaster.tsx create mode 100644 components/ui/toggle-group.tsx create mode 100644 components/ui/toggle.tsx create mode 100644 components/ui/tooltip.tsx create mode 100644 components/user-context.tsx create mode 100644 components/user-switcher.tsx create mode 100644 hooks/use-toast.ts create mode 100644 lib/constants.ts create mode 100644 lib/error-utils.ts create mode 100644 lib/kr-utils.ts create mode 100644 lib/supabase.ts create mode 100644 lib/utils.ts create mode 100644 netlify.toml create mode 100644 next.config.js create mode 100644 package-lock.json create mode 100644 package.json create mode 100644 postcss.config.js create mode 100644 supabase/migrations/20260123142127_create_okr_tables.sql create mode 100644 tailwind.config.ts create mode 100644 tsconfig.json create mode 100644 types/database.ts create mode 100644 vercel.json diff --git a/.bolt/config.json b/.bolt/config.json new file mode 100644 index 0000000..f236591 --- /dev/null +++ b/.bolt/config.json @@ -0,0 +1,3 @@ +{ + "template": "nextjs-shadcn" +} diff --git a/.bolt/ignore b/.bolt/ignore new file mode 100644 index 0000000..bbe3a15 --- /dev/null +++ b/.bolt/ignore @@ -0,0 +1,2 @@ +components/ui/* +hooks/use-toast.ts diff --git a/.bolt/prompt b/.bolt/prompt new file mode 100644 index 0000000..88d020b --- /dev/null +++ b/.bolt/prompt @@ -0,0 +1,9 @@ +For all designs I ask you to make, have them be beautiful, not cookie cutter. Make webpages that are fully featured and worthy for production. + +When using client-side hooks (useState and useEffect) in a component that's being treated as a Server Component by Next.js, always add the "use client" directive at the top of the file. + +Do not write code that will trigger this error: "Warning: Extra attributes from the server: %s%s""class,style" + +By default, this template supports JSX syntax with Tailwind CSS classes, the shadcn/ui library, React hooks, and Lucide React for icons. Do not install other packages for UI themes, icons, etc unless absolutely necessary or I request them. + +Use icons from lucide-react for logos. diff --git a/.eslintrc.json b/.eslintrc.json new file mode 100644 index 0000000..bffb357 --- /dev/null +++ b/.eslintrc.json @@ -0,0 +1,3 @@ +{ + "extends": "next/core-web-vitals" +} diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..9b1913e --- /dev/null +++ b/.gitignore @@ -0,0 +1,36 @@ +# See https://help.github.com/articles/ignoring-files/ for more about ignoring files. + +# dependencies +/node_modules +/.pnp +.pnp.js + +# testing +/coverage + +# next.js +/.next/ +/out/ + +# production +/build + +# misc +.DS_Store +*.pem + +# debug +npm-debug.log* +yarn-debug.log* +yarn-error.log* + +# local env files +.env +.env*.local + +# vercel +.vercel + +# typescript +*.tsbuildinfo +next-env.d.ts diff --git a/.vercelignore b/.vercelignore new file mode 100644 index 0000000..a48e5d3 --- /dev/null +++ b/.vercelignore @@ -0,0 +1,7 @@ +node_modules +build +dist +.git +.trae +.log +.figma \ No newline at end of file diff --git a/README.md b/README.md new file mode 100644 index 0000000..659fa57 --- /dev/null +++ b/README.md @@ -0,0 +1 @@ +ok r diff --git a/app/admin/metrics/page.tsx b/app/admin/metrics/page.tsx new file mode 100644 index 0000000..0a86390 --- /dev/null +++ b/app/admin/metrics/page.tsx @@ -0,0 +1,332 @@ +'use client'; + +import { useEffect, useState } from 'react'; +import { useForm } from 'react-hook-form'; +import { zodResolver } from '@hookform/resolvers/zod'; +import * as z from 'zod'; +import { Card, CardContent } from '@/components/ui/card'; +import { Button } from '@/components/ui/button'; +import { Input } from '@/components/ui/input'; +import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from '@/components/ui/select'; +import { Form, FormControl, FormField, FormItem, FormLabel, FormMessage } from '@/components/ui/form'; +import { Dialog, DialogContent, DialogHeader, DialogTitle, DialogTrigger } from '@/components/ui/dialog'; +import { Table, TableBody, TableCell, TableHead, TableHeader, TableRow } from '@/components/ui/table'; +import { Switch } from '@/components/ui/switch'; +import { useUser } from '@/components/user-context'; +import { DIRECTION_LABELS } from '@/lib/constants'; +import type { Metric } from '@/types/database'; +import { toast } from 'sonner'; +import { Plus, Pencil, Trash2, Gauge, Loader2, TrendingUp, TrendingDown } from 'lucide-react'; +import { getErrorMessage } from '@/lib/error-utils'; + +const metricSchema = z.object({ + name: z.string().min(1, '名称不能为空'), + unit: z.string().min(1, '单位不能为空'), + direction: z.enum(['increase', 'decrease']), + category: z.string().optional(), +}); + +type MetricFormValues = z.infer; + +export default function AdminMetricsPage() { + const { currentUser } = useUser(); + const [metrics, setMetrics] = useState([]); + const [isLoading, setIsLoading] = useState(true); + const [isDialogOpen, setIsDialogOpen] = useState(false); + const [editingMetric, setEditingMetric] = useState(null); + const [isSubmitting, setIsSubmitting] = useState(false); + + const form = useForm({ + resolver: zodResolver(metricSchema), + defaultValues: { + name: '', + unit: '', + direction: 'increase', + category: '', + }, + }); + + const fetchMetrics = async () => { + try { + const res = await fetch('/api/metrics'); + if (res.ok) { + const data = await res.json(); + setMetrics(data); + } + } catch (error) { + console.error('Failed to fetch metrics:', error); + } finally { + setIsLoading(false); + } + }; + + useEffect(() => { + fetchMetrics(); + }, []); + + const onSubmit = async (values: MetricFormValues) => { + setIsSubmitting(true); + try { + const url = editingMetric ? `/api/metrics/${editingMetric.id}` : '/api/metrics'; + const method = editingMetric ? 'PATCH' : 'POST'; + + const res = await fetch(url, { + method, + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify(values), + }); + + if (!res.ok) { + const data = await res.json(); + throw new Error(data.error || '操作失败'); + } + + toast.success(editingMetric ? '指标已更新' : '指标已创建'); + setIsDialogOpen(false); + setEditingMetric(null); + form.reset(); + fetchMetrics(); + } catch (error) { + toast.error(getErrorMessage(error)); + } finally { + setIsSubmitting(false); + } + }; + + const handleEdit = (metric: Metric) => { + setEditingMetric(metric); + form.reset({ + name: metric.name, + unit: metric.unit, + direction: metric.direction, + category: metric.category || '', + }); + setIsDialogOpen(true); + }; + + const handleToggleActive = async (metric: Metric) => { + try { + const res = await fetch(`/api/metrics/${metric.id}`, { + method: 'PATCH', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify({ is_active: !metric.is_active }), + }); + + if (!res.ok) throw new Error('操作失败'); + + toast.success(metric.is_active ? '已停用' : '已启用'); + fetchMetrics(); + } catch (error) { + toast.error(getErrorMessage(error)); + } + }; + + const handleDelete = async (metricId: string) => { + if (!confirm('确定要删除此指标吗?')) return; + + try { + const res = await fetch(`/api/metrics/${metricId}`, { method: 'DELETE' }); + if (!res.ok) throw new Error('删除失败'); + + toast.success('指标已删除'); + fetchMetrics(); + } catch (error) { + toast.error(getErrorMessage(error)); + } + }; + + const handleDialogClose = () => { + setIsDialogOpen(false); + setEditingMetric(null); + form.reset(); + }; + + if (currentUser?.role !== 'owner') { + return ( +
+

仅老板可访问此页面

+
+ ); + } + + if (isLoading) { + return ( +
+
+
+
+
+
+ ); + } + + const categories = Array.from(new Set(metrics.map(m => m.category).filter(Boolean))); + + return ( +
+
+
+

+ + 指标库管理 +

+

管理可复用的 KR 指标

+
+ + + + + + + + {editingMetric ? '编辑指标' : '新建指标'} + +
+ + ( + + 指标名称 * + + + + + + )} + /> + + ( + + 单位 * + + + + + + )} + /> + + ( + + 方向 + + + + )} + /> + + ( + + 分类 + + + + + + )} + /> + +
+ + +
+ + +
+
+
+ + + + + + + 指标名称 + 单位 + 方向 + 分类 + 状态 + 操作 + + + + {metrics.map(metric => ( + + {metric.name} + {metric.unit} + +
+ {metric.direction === 'increase' ? ( + + ) : ( + + )} + {DIRECTION_LABELS[metric.direction]} +
+
+ + {metric.category || '-'} + + + handleToggleActive(metric)} + /> + + + + + +
+ ))} + + {metrics.length === 0 && ( + + + 暂无指标 + + + )} +
+
+
+
+
+ ); +} diff --git a/app/admin/templates/page.tsx b/app/admin/templates/page.tsx new file mode 100644 index 0000000..80ee337 --- /dev/null +++ b/app/admin/templates/page.tsx @@ -0,0 +1,436 @@ +'use client'; + +import { useEffect, useState } from 'react'; +import { useForm, useFieldArray } from 'react-hook-form'; +import { zodResolver } from '@hookform/resolvers/zod'; +import * as z from 'zod'; +import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card'; +import { Button } from '@/components/ui/button'; +import { Input } from '@/components/ui/input'; +import { Textarea } from '@/components/ui/textarea'; +import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from '@/components/ui/select'; +import { Form, FormControl, FormField, FormItem, FormLabel, FormMessage } from '@/components/ui/form'; +import { Dialog, DialogContent, DialogHeader, DialogTitle, DialogTrigger } from '@/components/ui/dialog'; +import { Switch } from '@/components/ui/switch'; +import { useUser } from '@/components/user-context'; +import { DIRECTION_LABELS } from '@/lib/constants'; +import type { Template } from '@/types/database'; +import { toast } from 'sonner'; +import { Plus, Pencil, Trash2, Files, Loader2, X } from 'lucide-react'; +import { getErrorMessage } from '@/lib/error-utils'; + +const templateItemSchema = z.object({ + title: z.string().min(1, '标题不能为空'), + metricName: z.string().min(1, '指标名称不能为空'), + unit: z.string().min(1, '单位不能为空'), + direction: z.enum(['increase', 'decrease']), + targetValue: z.number().positive('目标值必须大于0'), +}); + +const templateSchema = z.object({ + name: z.string().min(1, '名称不能为空'), + description: z.string().optional(), + project_type: z.string().optional(), + items: z.array(templateItemSchema).min(1, '至少需要一个模板项'), +}); + +type TemplateFormValues = z.infer; + +export default function AdminTemplatesPage() { + // Force re-compile + const { currentUser } = useUser(); + const [templates, setTemplates] = useState([]); + const [isLoading, setIsLoading] = useState(true); + const [isDialogOpen, setIsDialogOpen] = useState(false); + const [editingTemplate, setEditingTemplate] = useState