652 lines
25 KiB
TypeScript
652 lines
25 KiB
TypeScript
'use client';
|
||
|
||
import { useEffect, useState, useCallback } from 'react';
|
||
import Link from 'next/link';
|
||
import { useRouter } from 'next/navigation';
|
||
import { useForm } 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 { Switch } from '@/components/ui/switch';
|
||
import { Dialog, DialogContent, DialogHeader, DialogTitle, DialogTrigger } from '@/components/ui/dialog';
|
||
import { AlertDialog, AlertDialogAction, AlertDialogCancel, AlertDialogContent, AlertDialogDescription, AlertDialogFooter, AlertDialogHeader, AlertDialogTitle, AlertDialogTrigger } from '@/components/ui/alert-dialog';
|
||
import { StatusBadge, StatusDot } from '@/components/status-badge';
|
||
import { ProgressRing } from '@/components/progress-ring';
|
||
import { useUser } from '@/components/user-context';
|
||
import { Label } from '@/components/ui/label';
|
||
import {
|
||
calculateProgress,
|
||
calculateExpectedProgress,
|
||
getDaysRemaining,
|
||
formatDateRange,
|
||
isKRActive,
|
||
} from '@/lib/kr-utils';
|
||
import { REASON_CODES, PERIOD_TYPES, DIRECTION_LABELS } from '@/lib/constants';
|
||
import type { KR, User, Checkin, Objective } from '@/types/database';
|
||
import { toast } from 'sonner';
|
||
import {
|
||
ArrowLeft,
|
||
Plus,
|
||
Clock,
|
||
TrendingUp,
|
||
TrendingDown,
|
||
Target,
|
||
AlertTriangle,
|
||
Calendar,
|
||
User as UserIcon,
|
||
HandHelping,
|
||
Loader2,
|
||
Pencil,
|
||
Trash2,
|
||
} from 'lucide-react';
|
||
import { getErrorMessage } from '@/lib/error-utils';
|
||
|
||
interface KRWithRelations extends KR {
|
||
owner: User | null;
|
||
project: { id: string; name: string; status: string } | null;
|
||
objective: Objective | null;
|
||
}
|
||
|
||
interface CheckinWithRelations extends Checkin {
|
||
user: User | null;
|
||
}
|
||
|
||
const checkinSchema = z.object({
|
||
current_value: z.number(),
|
||
confidence: z.number().min(0).max(100),
|
||
reason_code: z.string().optional(),
|
||
note: z.string().optional(),
|
||
});
|
||
|
||
type CheckinFormValues = z.infer<typeof checkinSchema>;
|
||
|
||
export default function KRDetailPage({ krId }: { krId: string }) {
|
||
const router = useRouter();
|
||
const { currentUser } = useUser();
|
||
|
||
const [kr, setKR] = useState<KRWithRelations | null>(null);
|
||
const [checkins, setCheckins] = useState<CheckinWithRelations[]>([]);
|
||
const [isLoading, setIsLoading] = useState(true);
|
||
const [isCheckinOpen, setIsCheckinOpen] = useState(false);
|
||
const [isSubmitting, setIsSubmitting] = useState(false);
|
||
const [isDeleting, setIsDeleting] = useState(false);
|
||
const [isEditing, setIsEditing] = useState(false);
|
||
const [isEditDialogOpen, setIsEditDialogOpen] = useState(false);
|
||
|
||
const fetchData = useCallback(async () => {
|
||
try {
|
||
const [krRes, checkinsRes] = await Promise.all([
|
||
fetch(`/api/krs/${krId}`),
|
||
fetch(`/api/checkins?kr_id=${krId}`),
|
||
]);
|
||
|
||
if (krRes.ok) {
|
||
const krData = await krRes.json();
|
||
setKR(krData);
|
||
}
|
||
|
||
if (checkinsRes.ok) {
|
||
const checkinsData = await checkinsRes.json();
|
||
setCheckins(checkinsData);
|
||
}
|
||
} catch (error) {
|
||
console.error('Failed to fetch KR data:', error);
|
||
} finally {
|
||
setIsLoading(false);
|
||
}
|
||
}, [krId]);
|
||
|
||
useEffect(() => {
|
||
fetchData();
|
||
}, [fetchData]);
|
||
|
||
const form = useForm<CheckinFormValues>({
|
||
resolver: zodResolver(checkinSchema),
|
||
defaultValues: {
|
||
current_value: kr?.current_value || 0,
|
||
confidence: kr?.confidence || 80,
|
||
reason_code: '',
|
||
note: '',
|
||
},
|
||
});
|
||
|
||
useEffect(() => {
|
||
if (kr) {
|
||
form.reset({
|
||
current_value: kr.current_value,
|
||
confidence: kr.confidence,
|
||
reason_code: kr.reason_code || '',
|
||
note: '',
|
||
});
|
||
}
|
||
}, [kr, form]);
|
||
|
||
const onCheckinSubmit = async (values: CheckinFormValues) => {
|
||
if (!kr) return;
|
||
|
||
setIsSubmitting(true);
|
||
try {
|
||
const res = await fetch('/api/checkins', {
|
||
method: 'POST',
|
||
headers: { 'Content-Type': 'application/json' },
|
||
body: JSON.stringify({
|
||
kr_id: kr.id,
|
||
project_id: kr.project_id,
|
||
user_id: currentUser?.id,
|
||
current_value: values.current_value,
|
||
confidence: values.confidence,
|
||
reason_code: values.reason_code || null,
|
||
note: values.note || null,
|
||
}),
|
||
});
|
||
|
||
if (!res.ok) {
|
||
const data = await res.json();
|
||
throw new Error(data.error || '打卡失败');
|
||
}
|
||
|
||
toast.success('打卡成功');
|
||
setIsCheckinOpen(false);
|
||
fetchData();
|
||
} catch (error) {
|
||
toast.error(getErrorMessage(error));
|
||
} finally {
|
||
setIsSubmitting(false);
|
||
}
|
||
};
|
||
|
||
const toggleNeedHelp = async () => {
|
||
if (!kr) return;
|
||
|
||
try {
|
||
const res = await fetch(`/api/krs/${kr.id}`, {
|
||
method: 'PATCH',
|
||
headers: { 'Content-Type': 'application/json' },
|
||
body: JSON.stringify({
|
||
need_owner_help: !kr.need_owner_help,
|
||
skipReasonCodeCheck: true,
|
||
}),
|
||
});
|
||
|
||
if (res.ok) {
|
||
toast.success(kr.need_owner_help ? '已取消求助' : '已发送求助请求');
|
||
fetchData();
|
||
}
|
||
} catch (error) {
|
||
toast.error('操作失败');
|
||
}
|
||
};
|
||
|
||
const handleDeleteKR = async () => {
|
||
if (!kr) return;
|
||
setIsDeleting(true);
|
||
try {
|
||
const res = await fetch(`/api/krs/${kr.id}`, { method: 'DELETE' });
|
||
if (!res.ok) throw new Error('删除失败');
|
||
toast.success('KR 已删除');
|
||
router.push(`/projects/${kr.project_id}`);
|
||
} catch (error) {
|
||
toast.error(getErrorMessage(error));
|
||
} finally {
|
||
setIsDeleting(false);
|
||
}
|
||
};
|
||
|
||
const handleEditKR = async (e: React.FormEvent<HTMLFormElement>) => {
|
||
e.preventDefault();
|
||
if (!kr) return;
|
||
|
||
setIsEditing(true);
|
||
const formData = new FormData(e.currentTarget);
|
||
|
||
try {
|
||
const res = await fetch(`/api/krs/${kr.id}`, {
|
||
method: 'PATCH',
|
||
headers: { 'Content-Type': 'application/json' },
|
||
body: JSON.stringify({
|
||
title: formData.get('title'),
|
||
metric_name: formData.get('metric_name'),
|
||
unit: formData.get('unit'),
|
||
target_value: parseFloat(formData.get('target_value') as string),
|
||
period_start: formData.get('period_start'),
|
||
period_end: formData.get('period_end'),
|
||
}),
|
||
});
|
||
|
||
if (!res.ok) throw new Error('更新失败');
|
||
|
||
toast.success('KR 已更新');
|
||
setIsEditDialogOpen(false);
|
||
fetchData();
|
||
} catch (error) {
|
||
toast.error(getErrorMessage(error));
|
||
} finally {
|
||
setIsEditing(false);
|
||
}
|
||
};
|
||
|
||
if (isLoading) {
|
||
return (
|
||
<div className="max-w-4xl mx-auto px-4 py-8">
|
||
<div className="animate-pulse space-y-6">
|
||
<div className="h-6 bg-slate-200 rounded w-24" />
|
||
<div className="h-10 bg-slate-200 rounded w-64" />
|
||
<div className="grid grid-cols-1 lg:grid-cols-3 gap-6">
|
||
<div className="lg:col-span-2 h-96 bg-slate-200 rounded-lg" />
|
||
<div className="h-64 bg-slate-200 rounded-lg" />
|
||
</div>
|
||
</div>
|
||
</div>
|
||
);
|
||
}
|
||
|
||
if (!kr) {
|
||
return (
|
||
<div className="max-w-4xl mx-auto px-4 py-8 text-center">
|
||
<p className="text-muted-foreground">KR 不存在</p>
|
||
<Button asChild className="mt-4" variant="outline">
|
||
<Link href="/my-krs">返回我的 KR</Link>
|
||
</Button>
|
||
</div>
|
||
);
|
||
}
|
||
|
||
const progress = calculateProgress(kr);
|
||
const expectedProgress = calculateExpectedProgress(kr);
|
||
const daysLeft = getDaysRemaining(kr.period_end);
|
||
const isActive = isKRActive(kr);
|
||
const isOwner = currentUser?.id === kr.owner_user_id;
|
||
const TrendIcon = kr.direction === 'increase' ? TrendingUp : TrendingDown;
|
||
|
||
return (
|
||
<div className="max-w-4xl mx-auto px-4 py-8">
|
||
<div className="mb-6">
|
||
<Link
|
||
href="/my-krs"
|
||
className="inline-flex items-center text-sm text-muted-foreground hover:text-slate-800 mb-4"
|
||
>
|
||
<ArrowLeft className="h-4 w-4 mr-1" />
|
||
返回我的 KR
|
||
</Link>
|
||
|
||
<div className="flex items-start justify-between">
|
||
<div>
|
||
<div className="flex items-center gap-3 mb-2">
|
||
<h1 className="text-2xl font-bold text-slate-800">{kr.title}</h1>
|
||
<StatusBadge status={kr.status} />
|
||
{kr.need_owner_help && (
|
||
<span className="text-xs bg-amber-100 text-amber-700 px-2 py-1 rounded flex items-center gap-1">
|
||
<HandHelping className="h-3 w-3" />
|
||
需要帮助
|
||
</span>
|
||
)}
|
||
</div>
|
||
<div className="flex items-center gap-3 text-sm text-muted-foreground">
|
||
<Link href={`/projects/${kr.project_id}`} className="hover:text-slate-800">
|
||
{kr.project?.name}
|
||
</Link>
|
||
<span>|</span>
|
||
<span className="flex items-center gap-1">
|
||
<UserIcon className="h-4 w-4" />
|
||
{kr.owner?.name || '未分配'}
|
||
</span>
|
||
<span>|</span>
|
||
<span className="flex items-center gap-1">
|
||
<Calendar className="h-4 w-4" />
|
||
{formatDateRange(kr.period_start, kr.period_end)}
|
||
</span>
|
||
</div>
|
||
</div>
|
||
|
||
{isActive && (
|
||
<div className="flex gap-2">
|
||
{currentUser?.role === 'owner' && (
|
||
<>
|
||
<Dialog open={isEditDialogOpen} onOpenChange={setIsEditDialogOpen}>
|
||
<DialogTrigger asChild>
|
||
<Button variant="outline" size="icon">
|
||
<Pencil className="h-4 w-4" />
|
||
</Button>
|
||
</DialogTrigger>
|
||
<DialogContent>
|
||
<DialogHeader>
|
||
<DialogTitle>编辑 KR</DialogTitle>
|
||
</DialogHeader>
|
||
<form onSubmit={handleEditKR} className="space-y-4">
|
||
<div>
|
||
<Label htmlFor="kr-title">KR 标题</Label>
|
||
<Input id="kr-title" name="title" defaultValue={kr.title} required />
|
||
</div>
|
||
<div className="grid grid-cols-2 gap-4">
|
||
<div>
|
||
<Label htmlFor="kr-metric">指标名称</Label>
|
||
<Input id="kr-metric" name="metric_name" defaultValue={kr.metric_name} required />
|
||
</div>
|
||
<div>
|
||
<Label htmlFor="kr-unit">单位</Label>
|
||
<Input id="kr-unit" name="unit" defaultValue={kr.unit} required />
|
||
</div>
|
||
</div>
|
||
<div>
|
||
<Label htmlFor="kr-target">目标值</Label>
|
||
<Input type="number" step="any" id="kr-target" name="target_value" defaultValue={kr.target_value} required />
|
||
</div>
|
||
<div className="grid grid-cols-2 gap-4">
|
||
<div>
|
||
<Label htmlFor="kr-start">开始日期</Label>
|
||
<Input type="date" id="kr-start" name="period_start" defaultValue={kr.period_start} required />
|
||
</div>
|
||
<div>
|
||
<Label htmlFor="kr-end">结束日期</Label>
|
||
<Input type="date" id="kr-end" name="period_end" defaultValue={kr.period_end} required />
|
||
</div>
|
||
</div>
|
||
<Button type="submit" disabled={isEditing} className="w-full">
|
||
{isEditing && <Loader2 className="h-4 w-4 mr-2 animate-spin" />}
|
||
保存修改
|
||
</Button>
|
||
</form>
|
||
</DialogContent>
|
||
</Dialog>
|
||
|
||
<AlertDialog>
|
||
<AlertDialogTrigger asChild>
|
||
<Button variant="outline" size="icon" className="text-rose-500 hover:text-rose-600">
|
||
<Trash2 className="h-4 w-4" />
|
||
</Button>
|
||
</AlertDialogTrigger>
|
||
<AlertDialogContent>
|
||
<AlertDialogHeader>
|
||
<AlertDialogTitle>确定要删除此 KR 吗?</AlertDialogTitle>
|
||
<AlertDialogDescription>
|
||
此操作无法撤销。这将永久删除 KR 及其所有打卡记录。
|
||
</AlertDialogDescription>
|
||
</AlertDialogHeader>
|
||
<AlertDialogFooter>
|
||
<AlertDialogCancel>取消</AlertDialogCancel>
|
||
<AlertDialogAction
|
||
onClick={handleDeleteKR}
|
||
className="bg-rose-600 hover:bg-rose-700"
|
||
disabled={isDeleting}
|
||
>
|
||
{isDeleting && <Loader2 className="h-4 w-4 mr-2 animate-spin" />}
|
||
删除
|
||
</AlertDialogAction>
|
||
</AlertDialogFooter>
|
||
</AlertDialogContent>
|
||
</AlertDialog>
|
||
</>
|
||
)}
|
||
<Dialog open={isCheckinOpen} onOpenChange={setIsCheckinOpen}>
|
||
<DialogTrigger asChild>
|
||
<Button className="bg-teal-600 hover:bg-teal-700">
|
||
<Plus className="h-4 w-4 mr-2" />
|
||
打卡更新
|
||
</Button>
|
||
</DialogTrigger>
|
||
<DialogContent>
|
||
<DialogHeader>
|
||
<DialogTitle>KR 进度打卡</DialogTitle>
|
||
</DialogHeader>
|
||
<Form {...form}>
|
||
<form onSubmit={form.handleSubmit(onCheckinSubmit)} className="space-y-4">
|
||
<FormField
|
||
control={form.control}
|
||
name="current_value"
|
||
render={({ field }) => (
|
||
<FormItem>
|
||
<FormLabel>当前值 ({kr.unit})</FormLabel>
|
||
<FormControl>
|
||
<Input
|
||
type="number"
|
||
step="any"
|
||
{...field}
|
||
onChange={e => field.onChange(parseFloat(e.target.value) || 0)}
|
||
/>
|
||
</FormControl>
|
||
<p className="text-xs text-muted-foreground">
|
||
目标值: {kr.target_value} {kr.unit}
|
||
</p>
|
||
<FormMessage />
|
||
</FormItem>
|
||
)}
|
||
/>
|
||
|
||
<FormField
|
||
control={form.control}
|
||
name="confidence"
|
||
render={({ field }) => (
|
||
<FormItem>
|
||
<FormLabel>信心度 ({field.value}%)</FormLabel>
|
||
<FormControl>
|
||
<Input
|
||
type="range"
|
||
min="0"
|
||
max="100"
|
||
{...field}
|
||
onChange={e => field.onChange(parseInt(e.target.value))}
|
||
className="w-full"
|
||
/>
|
||
</FormControl>
|
||
<div className="flex justify-between text-xs text-muted-foreground">
|
||
<span>0%</span>
|
||
<span>50%</span>
|
||
<span>100%</span>
|
||
</div>
|
||
<FormMessage />
|
||
</FormItem>
|
||
)}
|
||
/>
|
||
|
||
<FormField
|
||
control={form.control}
|
||
name="reason_code"
|
||
render={({ field }) => (
|
||
<FormItem>
|
||
<FormLabel>问题原因 (如有)</FormLabel>
|
||
<Select
|
||
onValueChange={(value) => field.onChange(value === "unspecified" ? "" : value)}
|
||
value={field.value || "unspecified"}
|
||
>
|
||
<FormControl>
|
||
<SelectTrigger>
|
||
<SelectValue placeholder="选择原因类型" />
|
||
</SelectTrigger>
|
||
</FormControl>
|
||
<SelectContent>
|
||
<SelectItem value="unspecified">无问题</SelectItem>
|
||
{Object.entries(REASON_CODES).map(([key, label]) => (
|
||
<SelectItem key={key} value={key}>
|
||
{label}
|
||
</SelectItem>
|
||
))}
|
||
</SelectContent>
|
||
</Select>
|
||
<FormMessage />
|
||
</FormItem>
|
||
)}
|
||
/>
|
||
|
||
<FormField
|
||
control={form.control}
|
||
name="note"
|
||
render={({ field }) => (
|
||
<FormItem>
|
||
<FormLabel>备注</FormLabel>
|
||
<FormControl>
|
||
<Textarea placeholder="补充说明" rows={2} {...field} />
|
||
</FormControl>
|
||
<FormMessage />
|
||
</FormItem>
|
||
)}
|
||
/>
|
||
|
||
<Button type="submit" className="w-full" disabled={isSubmitting}>
|
||
{isSubmitting && <Loader2 className="h-4 w-4 mr-2 animate-spin" />}
|
||
提交打卡
|
||
</Button>
|
||
</form>
|
||
</Form>
|
||
</DialogContent>
|
||
</Dialog>
|
||
</div>
|
||
)}
|
||
</div>
|
||
</div>
|
||
|
||
<div className="grid grid-cols-1 lg:grid-cols-3 gap-6">
|
||
<div className="lg:col-span-2 space-y-6">
|
||
<Card>
|
||
<CardContent className="pt-6">
|
||
<div className="flex items-center justify-between mb-6">
|
||
<div className="flex items-center gap-6">
|
||
<ProgressRing progress={progress} status={kr.status} size={100} strokeWidth={8} />
|
||
<div>
|
||
<p className="text-3xl font-bold text-slate-800">
|
||
{kr.current_value} / {kr.target_value}
|
||
</p>
|
||
<p className="text-muted-foreground">{kr.unit}</p>
|
||
<div className="flex items-center gap-2 mt-2">
|
||
<TrendIcon className="h-4 w-4 text-muted-foreground" />
|
||
<span className="text-sm text-muted-foreground">
|
||
{DIRECTION_LABELS[kr.direction]}
|
||
</span>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
<div className="text-right">
|
||
<p className="text-sm text-muted-foreground mb-1">期望进度</p>
|
||
<p className="text-2xl font-semibold text-slate-600">
|
||
{Math.round(expectedProgress * 100)}%
|
||
</p>
|
||
{isActive && (
|
||
<p className={`text-sm ${daysLeft <= 3 ? 'text-amber-600' : 'text-muted-foreground'}`}>
|
||
<Clock className="h-3 w-3 inline mr-1" />
|
||
剩余 {daysLeft} 天
|
||
</p>
|
||
)}
|
||
</div>
|
||
</div>
|
||
|
||
{kr.reason_code && kr.status !== 'green' && (
|
||
<div className="bg-amber-50 rounded-lg p-4 flex items-start gap-3">
|
||
<AlertTriangle className="h-5 w-5 text-amber-500 mt-0.5" />
|
||
<div>
|
||
<p className="font-medium text-amber-800">{REASON_CODES[kr.reason_code]}</p>
|
||
{kr.note && <p className="text-sm text-amber-700 mt-1">{kr.note}</p>}
|
||
</div>
|
||
</div>
|
||
)}
|
||
</CardContent>
|
||
</Card>
|
||
|
||
<Card>
|
||
<CardHeader>
|
||
<CardTitle className="text-lg">打卡记录</CardTitle>
|
||
</CardHeader>
|
||
<CardContent>
|
||
{checkins.length === 0 ? (
|
||
<p className="text-center text-muted-foreground py-8">暂无打卡记录</p>
|
||
) : (
|
||
<div className="space-y-4">
|
||
{checkins.map((checkin, index) => (
|
||
<div key={checkin.id} className="flex items-start gap-4">
|
||
<div className="flex flex-col items-center">
|
||
<StatusDot status={checkin.status_snapshot} size="md" />
|
||
{index < checkins.length - 1 && (
|
||
<div className="w-0.5 h-12 bg-slate-200 mt-2" />
|
||
)}
|
||
</div>
|
||
<div className="flex-1 pb-4">
|
||
<div className="flex items-center justify-between mb-1">
|
||
<span className="font-medium">
|
||
{checkin.current_value} {kr.unit}
|
||
</span>
|
||
<span className="text-sm text-muted-foreground">
|
||
{new Date(checkin.created_at).toLocaleString('zh-CN', {
|
||
month: 'short',
|
||
day: 'numeric',
|
||
hour: '2-digit',
|
||
minute: '2-digit',
|
||
})}
|
||
</span>
|
||
</div>
|
||
<div className="text-sm text-muted-foreground">
|
||
{checkin.user?.name} | 信心度 {checkin.confidence}%
|
||
{checkin.delta_value !== null && checkin.delta_value !== 0 && (
|
||
<span className={checkin.delta_value > 0 ? 'text-emerald-600' : 'text-rose-600'}>
|
||
{' '}({checkin.delta_value > 0 ? '+' : ''}{checkin.delta_value})
|
||
</span>
|
||
)}
|
||
</div>
|
||
{checkin.reason_code && (
|
||
<p className="text-sm text-amber-600 mt-1">
|
||
{REASON_CODES[checkin.reason_code]}
|
||
</p>
|
||
)}
|
||
{checkin.note && (
|
||
<p className="text-sm text-slate-600 mt-1">{checkin.note}</p>
|
||
)}
|
||
</div>
|
||
</div>
|
||
))}
|
||
</div>
|
||
)}
|
||
</CardContent>
|
||
</Card>
|
||
</div>
|
||
|
||
<div className="space-y-6">
|
||
<Card>
|
||
<CardHeader>
|
||
<CardTitle className="text-lg">KR 详情</CardTitle>
|
||
</CardHeader>
|
||
<CardContent className="space-y-4">
|
||
<div>
|
||
<p className="text-sm text-muted-foreground">周期类型</p>
|
||
<p className="font-medium">{PERIOD_TYPES[kr.period_type]}</p>
|
||
</div>
|
||
<div>
|
||
<p className="text-sm text-muted-foreground">指标名称</p>
|
||
<p className="font-medium">{kr.metric_name}</p>
|
||
</div>
|
||
<div>
|
||
<p className="text-sm text-muted-foreground">信心度</p>
|
||
<p className="font-medium">{kr.confidence}%</p>
|
||
</div>
|
||
{kr.objective && (
|
||
<div>
|
||
<p className="text-sm text-muted-foreground">关联目标</p>
|
||
<p className="font-medium">{kr.objective.title}</p>
|
||
</div>
|
||
)}
|
||
</CardContent>
|
||
</Card>
|
||
|
||
{isOwner && isActive && (
|
||
<Card>
|
||
<CardHeader>
|
||
<CardTitle className="text-lg">快捷操作</CardTitle>
|
||
</CardHeader>
|
||
<CardContent>
|
||
<div className="flex items-center justify-between">
|
||
<div>
|
||
<p className="font-medium">请求老板帮助</p>
|
||
<p className="text-sm text-muted-foreground">遇到困难时发送求助</p>
|
||
</div>
|
||
<Switch checked={kr.need_owner_help} onCheckedChange={toggleNeedHelp} />
|
||
</div>
|
||
</CardContent>
|
||
</Card>
|
||
)}
|
||
</div>
|
||
</div>
|
||
</div>
|
||
);
|
||
}
|