217 lines
8.1 KiB
TypeScript
217 lines
8.1 KiB
TypeScript
"use client"
|
||
|
||
import { useMemo, useState, useEffect } from "react"
|
||
import { Dialog, DialogContent, DialogDescription, DialogFooter, DialogHeader, DialogTitle } from "@/components/ui/dialog"
|
||
import { Input } from "@/components/ui/input"
|
||
import { Button } from "@/components/ui/button"
|
||
import { Label } from "@/components/ui/label"
|
||
import { ScrollArea } from "@/components/ui/scroll-area"
|
||
import { MemberNameWithStatus } from "@/components/member-name-with-status"
|
||
import { useFamily } from "@/context/family-context"
|
||
import { useToast } from "@/components/ui/use-toast"
|
||
import type { FamilyMember } from "@/types/family"
|
||
|
||
interface AddRelationDialogProps {
|
||
open: boolean
|
||
onOpenChange: (open: boolean) => void
|
||
member: FamilyMember
|
||
relationType: "child" | "spouse"
|
||
onCreateNew: (prefillName: string, relationType: "child" | "spouse") => void
|
||
}
|
||
|
||
interface CandidateMeta {
|
||
member: FamilyMember
|
||
fatherName?: string
|
||
motherName?: string
|
||
}
|
||
|
||
export function AddRelationDialog({
|
||
open,
|
||
onOpenChange,
|
||
member,
|
||
relationType,
|
||
onCreateNew,
|
||
}: AddRelationDialogProps) {
|
||
const { treeData, updateMember } = useFamily()
|
||
const { toast } = useToast()
|
||
const [query, setQuery] = useState("")
|
||
const [linkingId, setLinkingId] = useState<string | null>(null)
|
||
|
||
useEffect(() => {
|
||
if (!open) {
|
||
setQuery("")
|
||
setLinkingId(null)
|
||
}
|
||
}, [open])
|
||
|
||
const normalizedQuery = query.trim().toLowerCase()
|
||
|
||
const candidates: CandidateMeta[] = useMemo(() => {
|
||
if (!normalizedQuery) return []
|
||
|
||
const members = Object.values(treeData.members)
|
||
return members
|
||
.filter((candidate) => {
|
||
if (candidate.id === member.id) return false
|
||
if (!candidate.fullName.toLowerCase().includes(normalizedQuery) &&
|
||
!(candidate.givenName || "").toLowerCase().includes(normalizedQuery) &&
|
||
!(candidate.courtesyName || "").toLowerCase().includes(normalizedQuery)) {
|
||
return false
|
||
}
|
||
|
||
if (relationType === "child") {
|
||
if (candidate.generation !== (member.generation + 1)) return false
|
||
if (member.childrenIds?.includes(candidate.id)) return false
|
||
if (member.spouseIds?.includes(candidate.id)) return false
|
||
|
||
const fatherMatches = candidate.fatherId ? candidate.fatherId === member.id : true
|
||
const motherMatches = candidate.motherId ? candidate.motherId === member.id : true
|
||
|
||
if (member.gender === "MALE" && !fatherMatches) return false
|
||
if (member.gender === "FEMALE" && !motherMatches) return false
|
||
if (member.gender !== "MALE" && member.gender !== "FEMALE") {
|
||
// 非男女成员,允许填补任一缺失的父母
|
||
if (candidate.fatherId && candidate.motherId) return false
|
||
}
|
||
} else {
|
||
if (candidate.generation !== member.generation) return false
|
||
if (member.spouseIds?.includes(candidate.id)) return false
|
||
if (candidate.spouseIds?.includes(member.id)) return false
|
||
if (candidate.gender === member.gender) return false
|
||
}
|
||
|
||
return true
|
||
})
|
||
.slice(0, 8)
|
||
.map((candidate) => ({
|
||
member: candidate,
|
||
fatherName: candidate.fatherId ? treeData.members[candidate.fatherId]?.fullName : undefined,
|
||
motherName: candidate.motherId ? treeData.members[candidate.motherId]?.fullName : undefined,
|
||
}))
|
||
}, [member, relationType, treeData.members, normalizedQuery])
|
||
|
||
const handleLink = async (target: FamilyMember) => {
|
||
try {
|
||
setLinkingId(target.id)
|
||
if (relationType === "child") {
|
||
const payload: Partial<FamilyMember> = {}
|
||
if (member.gender === "FEMALE") {
|
||
payload.motherId = member.id
|
||
} else if (member.gender === "MALE") {
|
||
payload.fatherId = member.id
|
||
} else {
|
||
if (!target.fatherId) {
|
||
payload.fatherId = member.id
|
||
} else if (!target.motherId) {
|
||
payload.motherId = member.id
|
||
} else {
|
||
throw new Error("该成员已绑定父母")
|
||
}
|
||
}
|
||
await updateMember(target.id, payload)
|
||
toast({
|
||
title: "关联成功",
|
||
description: `已将 ${target.fullName} 标记为 ${member.fullName} 的子女。`
|
||
})
|
||
} else {
|
||
const existing = new Set(member.spouseIds || [])
|
||
existing.add(target.id)
|
||
await updateMember(member.id, { spouseIds: Array.from(existing) })
|
||
toast({
|
||
title: "关联成功",
|
||
description: `${member.fullName} 与 ${target.fullName} 已建立配偶关系。`
|
||
})
|
||
}
|
||
onOpenChange(false)
|
||
} catch (error) {
|
||
const message = error instanceof Error ? error.message : "关联失败"
|
||
toast({
|
||
title: "操作失败",
|
||
description: message,
|
||
variant: "destructive",
|
||
})
|
||
} finally {
|
||
setLinkingId(null)
|
||
}
|
||
}
|
||
|
||
const handleCreateNew = () => {
|
||
if (!normalizedQuery) return
|
||
onCreateNew(query.trim(), relationType)
|
||
}
|
||
|
||
return (
|
||
<Dialog open={open} onOpenChange={onOpenChange}>
|
||
<DialogContent className="sm:max-w-lg">
|
||
<DialogHeader>
|
||
<DialogTitle>添加{relationType === "child" ? "子女" : "配偶"}</DialogTitle>
|
||
<DialogDescription>
|
||
先输入姓名,系统会检索已有成员。若没有匹配,可直接新建。
|
||
</DialogDescription>
|
||
</DialogHeader>
|
||
|
||
<div className="space-y-4">
|
||
<div className="space-y-2">
|
||
<Label htmlFor="relation-name">成员姓名</Label>
|
||
<Input
|
||
id="relation-name"
|
||
placeholder="请输入姓名或字号"
|
||
value={query}
|
||
autoFocus
|
||
onChange={(e) => setQuery(e.target.value)}
|
||
/>
|
||
</div>
|
||
|
||
{normalizedQuery ? (
|
||
candidates.length > 0 ? (
|
||
<div>
|
||
<p className="text-sm text-muted-foreground mb-2">找到以下可能的匹配:</p>
|
||
<ScrollArea className="max-h-64 rounded-md border">
|
||
<div className="divide-y">
|
||
{candidates.map(({ member: candidate, fatherName, motherName }) => (
|
||
<div key={candidate.id} className="p-3 flex flex-col gap-2">
|
||
<div className="flex items-center justify-between gap-3">
|
||
<div>
|
||
<MemberNameWithStatus name={candidate.fullName} isDead={!!candidate.deathDate} />
|
||
<p className="text-xs text-muted-foreground">
|
||
第 {candidate.generation} 世 · {candidate.gender === "FEMALE" ? "女" : "男"}
|
||
</p>
|
||
</div>
|
||
<Button
|
||
size="sm"
|
||
onClick={() => handleLink(candidate)}
|
||
disabled={linkingId === candidate.id}
|
||
>
|
||
{linkingId === candidate.id ? "关联中..." : "关联"}
|
||
</Button>
|
||
</div>
|
||
<div className="text-xs text-muted-foreground">
|
||
<div>父亲:{fatherName || "未登记"}</div>
|
||
<div>母亲:{motherName || "未登记"}</div>
|
||
</div>
|
||
</div>
|
||
))}
|
||
</div>
|
||
</ScrollArea>
|
||
</div>
|
||
) : (
|
||
<p className="text-sm text-muted-foreground">未找到匹配成员,可直接创建新成员。</p>
|
||
)
|
||
) : (
|
||
<p className="text-sm text-muted-foreground">请输入姓名以搜索已有成员。</p>
|
||
)}
|
||
</div>
|
||
|
||
<DialogFooter className="flex flex-col gap-2 sm:flex-row sm:justify-between">
|
||
<Button variant="outline" onClick={() => onOpenChange(false)} className="sm:flex-1">
|
||
取消
|
||
</Button>
|
||
<Button onClick={handleCreateNew} disabled={!normalizedQuery} className="sm:flex-1">
|
||
新建“{query || "成员"}”
|
||
</Button>
|
||
</DialogFooter>
|
||
</DialogContent>
|
||
</Dialog>
|
||
)
|
||
}
|