"use client"; import { searchItems } from "@/lib/search"; import Image from "@/components/ui/resilient-image"; import { ArrowLeftIcon, BookOpenIcon, ClipboardCopyIcon, ClipboardPasteIcon, CopyIcon, GripVerticalIcon, MousePointerClickIcon, PlusIcon, SearchIcon, Trash2Icon, XIcon } from "lucide-react"; import { useEffect, useMemo, useRef, useState, type ReactNode, type SetStateAction } from "react"; import { toast } from "sonner"; import { closestCenter, DndContext, KeyboardSensor, PointerSensor, useSensor, useSensors, type DragEndEvent } from "@dnd-kit/core"; import { arrayMove, rectSortingStrategy, SortableContext, sortableKeyboardCoordinates, useSortable } from "@dnd-kit/sortable"; import { CSS } from "@dnd-kit/utilities"; import { listTeamImportGuides, listTeamImportsForCharacter, loadTeamImportGuide, saveTeamSection } from "@/app/admin/actions"; import { AutosaveStatus } from "@/components/admin/autosave-status"; import { DeleteConfirmationDialog } from "@/components/admin/ui/delete-confirmation-dialog"; import { ArtifactSelect, CatalogDropdown, CharacterSelect, WeaponSelect } from "@/components/admin/ui/catalog-select"; import { ConstellationSelect, RefinementSelect } from "@/components/admin/ui/option-select"; import { ScrollableNav } from "@/components/public/scrollable-nav"; import { TeamArtifactIcon } from "@/components/public/team-search"; import { SimpleTooltip } from "@/components/tooltip"; import { Button } from "@/components/ui/button"; import { Badge } from "@/components/ui/badge"; import { Checkbox } from "@/components/ui/checkbox"; import { Alert, AlertDescription, AlertTitle } from "@/components/ui/alert"; import { Card, CardAction, CardContent, CardDescription, CardFooter, CardHeader, CardTitle } from "@/components/ui/card"; import { Empty, EmptyContent, EmptyDescription, EmptyHeader, EmptyMedia, EmptyTitle } from "@/components/ui/empty"; import { Field, FieldGroup, FieldLabel, FieldLegend, FieldSet } from "@/components/ui/field"; import { ClearableInput as Input } from "@/components/admin/ui/clearable-input"; import { Select, SelectContent, SelectGroup, SelectItem, SelectTrigger, SelectValue } from "@/components/ui/select"; import { Textarea } from "@/components/ui/textarea"; import { Separator } from "@/components/ui/separator"; import { Skeleton } from "@/components/ui/skeleton"; import { Switch } from "@/components/ui/switch"; import { ToggleGroup, ToggleGroupItem } from "@/components/ui/toggle-group"; import { Tabs, TabsList, TabsTrigger } from "@/components/ui/tabs"; import { Dialog, DialogContent, DialogDescription, DialogFooter, DialogHeader, DialogTitle } from "@/components/ui/dialog"; import { useAutosave } from "@/hooks/use-autosave"; import { adminDraftKey } from "@/lib/editor/local-draft"; import { teamSectionSchema } from "@/lib/guides/schemas"; import { elementBackgroundColor } from "@/lib/catalog/element"; import { supportsWeaponType } from "@/lib/catalog/lunaris"; import { characterRarityGradientClass, rarityGradientClass } from "@/lib/catalog/rarity"; import { isCustomTravelerKey } from "@/lib/catalog/travelers"; import { toTeamDraft } from "@/lib/guides/team-draft"; import { applyPresetFields, applyTeamFields, cloneTeamImports, filterTeamImportsByName, findInvalidTeamCatalogReference, findUnsupportedTeamWeapon, limitPresets, parseTeamPresets, parseTeamTransfer, serializeTeamTransfer, teamConstellationLabel, teamRefinementLabel, teamTransferFileName, teamPresetCookieKey, teamPresetStorageKey, type TeamApplySelection, type TeamMemberApplyField, type TeamToolMember, type TeamPreset, type TeamToolValue } from "@/lib/guides/team-tools"; import { searchTeams, type TeamSearchTarget } from "@/lib/guides/team-search"; import { formatGuideValidationIssues } from "@/lib/guides/validation-messages"; import { cn } from "@/lib/utils"; import { type CatalogItem, type EditorBase, createClientId, parseEditorDraft, ConditionControls, reorderConditions, ConditionRenameDialog } from "./shared"; export const teamSearchTargetLabels: Record = { all: "ทั้งหมด", team: "ชื่อทีม", character: "ตัวละคร", weapon: "อาวุธ", artifact: "อาร์ติแฟกต์", }; export const teamMemberApplyOptions = [ ["characterKey", "Character"], ["weaponKey", "Weapon"], ["artifactKey", "Artifact 1"], ["secondArtifactKey", "Artifact 2"], ["constellation", "C"], ["refinement", "R"], ["damage", "Damage"], ] as const satisfies readonly (readonly [TeamMemberApplyField, string])[]; export const multipleApplyStorageKey = (guideId: string) => `buzz-sheet:team-multiple-apply:${guideId}`; export function SortableTeamItem({ id, children, }: { id: string; children: (dragHandle: ReactNode) => ReactNode; }) { const { attributes, listeners, setNodeRef, transform, transition, isDragging, } = useSortable({ id }); return (
{children( , )}
); } export function SortableTeamMember({ id, children, }: { id: string; children: (dragHandle: ReactNode) => ReactNode; }) { const { attributes, listeners, setNodeRef, transform, transition, isDragging, } = useSortable({ id }); return (
{children( , )}
); } export type TeamImportGuide = { id: string; name: string; groups: Array<{ id: string; name: string | null; teams: Array<{ id: string; title: string; rotationSeconds: string; combo: string; members: Array; }>; }>; }; export type TeamEverywhereImport = { id: string; title: string; rotationSeconds: string; combo: string; guideName: string; conditionName: string | null; members: Array; }; export function TeamImportPreviewCard({ team, characters, weapons, artifacts, checked, checkboxId, onCheckedChange, sourceLabel, disabled = false, }: { team: TeamImportGuide["groups"][number]["teams"][number]; characters: CatalogItem[]; weapons: CatalogItem[]; artifacts: CatalogItem[]; checked: boolean; checkboxId: string; onCheckedChange: (checked: boolean) => void; sourceLabel?: string; disabled?: boolean; }) { const total = team.members.reduce((sum, member) => sum + Number(member.damage), 0); const seconds = Number(team.rotationSeconds); return ( {team.title} โรเทชั่น {seconds} วิ{sourceLabel ? ` - ${sourceLabel}` : ""} onCheckedChange(value === true)} /> {disabled ? "ชื่อซ้ำ" : "เลือก"}
{team.members.map((member, index) => { const character = characters.find((item) => item.key === member.characterKey); const weapon = weapons.find((item) => item.key === member.weaponKey); const artifact = artifacts.find((item) => item.key === member.artifactKey); const secondArtifact = artifacts.find((item) => item.key === member.secondArtifactKey); return (
{character ? {character.name} : null}
{weapon ? (
{weapon.name}
) : null} {artifact || secondArtifact ? ( ) : null}
{teamConstellationLabel(member.constellation)} {teamRefinementLabel(member.refinement)}

{character?.name ?? member.characterName}

{Number(member.damage).toLocaleString()}

{total ? ((Number(member.damage) / total) * 100).toFixed(1) : "0.0"}%

); })}

ดาเมจรวม

{total.toLocaleString()}

DPS

{seconds > 0 ? (total / seconds).toLocaleString(undefined, { maximumFractionDigits: 0 }) : "0"}

{team.combo}

); } export function TeamEditor({ guide, characters, weapons, artifacts, initial, }: EditorBase) { const fallbackMembers = Array.from({ length: 4 }, () => ({ characterKey: "", weaponKey: null, artifactKey: null, secondArtifactKey: null, constellation: "0", refinement: "1", damage: "0", })); const teamSensors = useSensors( useSensor(PointerSensor), useSensor(KeyboardSensor, { coordinateGetter: sortableKeyboardCoordinates, }), ); const memberSortableIds = useRef(new WeakMap()); const getMemberSortableId = (member: TeamToolMember) => { const existingId = memberSortableIds.current.get(member); if (existingId) return existingId; const id = createClientId("team-member"); memberSortableIds.current.set(member, id); return id; }; const [teamGroups, setTeamGroups] = useState(() => initial.teamGroups.length ? initial.teamGroups.map((group) => ({ clientId: group.id, name: group.name ?? "", note: group.note ?? "", iconMediaId: group.iconMediaId ?? null, iconUrl: group.iconUrl ?? null, teams: group.teams.map(({ id, ...team }) => ({ clientId: id, ...team })) })) : [{ clientId: "team-condition-default", name: "", note: "", iconMediaId: null, iconUrl: null, teams: [] as TeamToolValue[] }]); const [activeTeamGroupIndex, setActiveTeamGroupIndex] = useState(0); const [renamingTeamGroupIndex, setRenamingTeamGroupIndex] = useState(null); const teams = useMemo(() => teamGroups[activeTeamGroupIndex]?.teams ?? [], [activeTeamGroupIndex, teamGroups]); const setTeams = (update: SetStateAction) => setTeamGroups((current) => current.map((group, index) => index === activeTeamGroupIndex ? { ...group, teams: typeof update === "function" ? update(group.teams) : update } : group)); const [selectedTeamIds, setSelectedTeamIds] = useState>( () => new Set(), ); const [sourceTeamId, setSourceTeamId] = useState(teams[0]?.clientId ?? ""); const [applyOpen, setApplyOpen] = useState(false); const [presetOpen, setPresetOpen] = useState(false); const [importOpen, setImportOpen] = useState(false); const [importFiles, setImportFiles] = useState([]); const [guideImportOpen, setGuideImportOpen] = useState(false); const [everywhereImportOpen, setEverywhereImportOpen] = useState(false); const everywhereSearchId = useRef(0); const [everywhereCharacterKey, setEverywhereCharacterKey] = useState(""); const [everywhereSearchedKey, setEverywhereSearchedKey] = useState(""); const [everywhereTeams, setEverywhereTeams] = useState([]); const [everywhereLoading, setEverywhereLoading] = useState(false); const [everywhereError, setEverywhereError] = useState(null); const [everywhereSelectedIds, setEverywhereSelectedIds] = useState>(() => new Set()); const [ignoreSameName, setIgnoreSameName] = useState(true); const [importGuides, setImportGuides] = useState | null>(null); const [activeImportGuide, setActiveImportGuide] = useState(null); const [selectedImports, setSelectedImports] = useState>>({}); const [guideImportLoading, setGuideImportLoading] = useState<"guides" | "teams" | null>(null); const [guideImportError, setGuideImportError] = useState(null); const [guideImportCondition, setGuideImportCondition] = useState("0"); const [guideTeamQuery, setGuideTeamQuery] = useState(""); const [guideTeamSearchTarget, setGuideTeamSearchTarget] = useState("all"); const [guideListQuery, setGuideListQuery] = useState(""); const [presetName, setPresetName] = useState(""); const [presets, setPresets] = useState([]); const [presetsHydrated, setPresetsHydrated] = useState(false); const [presetsGuideId, setPresetsGuideId] = useState(null); const [applyFields, setApplyFields] = useState([]); const [multipleApplyGuideId, setMultipleApplyGuideId] = useState(null); const [searchQuery, setSearchQuery] = useState(""); const [searchTarget, setSearchTarget] = useState("all"); const teamSearch = useMemo(() => { const characterNames = new Map(characters.map((item) => [item.key, [item.name, ...(item.aliases ?? [])]])); const weaponNames = new Map(weapons.map((item) => [item.key, [item.name, ...(item.aliases ?? [])]])); const artifactNames = new Map(artifacts.map((item) => [item.key, [item.name, ...(item.aliases ?? [])]])); return searchTeams(teams.map((team, teamIndex) => ({ team, teamIndex })), ({ team }) => ({ team: [team.title], character: team.members.flatMap((member) => { const name = characterNames.get(member.characterKey); return name ?? []; }), weapon: team.members.flatMap((member) => { const name = member.weaponKey ? weaponNames.get(member.weaponKey) : undefined; return name ?? []; }), artifact: team.members.flatMap((member) => [member.artifactKey, member.secondArtifactKey].flatMap((key) => { const name = key ? artifactNames.get(key) : undefined; return name ?? []; }), ), }), searchQuery, searchTarget); }, [artifacts, characters, searchQuery, searchTarget, teams, weapons]); const activeImportGroup = activeImportGuide?.groups[Number(guideImportCondition)] ?? activeImportGuide?.groups[0]; const importTeamSearch = useMemo(() => { const characterNames = new Map(characters.map((item) => [item.key, [item.name, ...(item.aliases ?? [])]])); const weaponNames = new Map(weapons.map((item) => [item.key, [item.name, ...(item.aliases ?? [])]])); const artifactNames = new Map(artifacts.map((item) => [item.key, [item.name, ...(item.aliases ?? [])]])); return searchTeams(activeImportGroup?.teams ?? [], (team) => ({ team: [team.title], character: team.members.flatMap((member) => characterNames.get(member.characterKey) ?? [member.characterName]), weapon: team.members.flatMap((member) => member.weaponKey ? weaponNames.get(member.weaponKey) ?? [] : []), artifact: team.members.flatMap((member) => [member.artifactKey, member.secondArtifactKey].flatMap((key) => key ? artifactNames.get(key) ?? [] : [])), }), guideTeamQuery, guideTeamSearchTarget); }, [activeImportGroup, artifacts, characters, guideTeamQuery, guideTeamSearchTarget, weapons]); const importGuideSearch = useMemo(() => { return searchItems(importGuides ?? [], guideListQuery, (sourceGuide) => `${sourceGuide.name} ${sourceGuide.characterKey}`, (sourceGuide) => [sourceGuide.name]); }, [guideListQuery, importGuides]); const filteredTeams = teamSearch.items; const filteredImportTeams = importTeamSearch.items; const filteredImportGuides = importGuideSearch.items; const eligibleEverywhereTeams = filterTeamImportsByName(teams, everywhereTeams, ignoreSameName); const eligibleEverywhereIds = new Set(eligibleEverywhereTeams.map((team) => team.id)); const selectedEverywhereTeams = eligibleEverywhereTeams.filter((team) => everywhereSelectedIds.has(team.id)); const allEverywhereTeamsSelected = eligibleEverywhereTeams.length > 0 && eligibleEverywhereTeams.every((team) => everywhereSelectedIds.has(team.id)); const value = { groups: teamGroups.map((group) => ({ name: group.name.trim() || null, note: group.note.trim() || null, iconMediaId: group.iconMediaId, teams: group.teams.map((team) => ({ title: team.title, rotationSeconds: Number(team.rotationSeconds), combo: team.combo, members: team.members.map((member) => ({ ...member, damage: Number(member.damage), })), })) })), }; const validation = teamSectionSchema.safeParse({ ...value, expectedVersion: guide.version, }); const teamFieldInvalid = (teamIndex: number, field: string) => !validation.success && validation.error.issues.some((issue) => issue.path[0] === "groups" && issue.path[1] === activeTeamGroupIndex && issue.path[2] === "teams" && issue.path[3] === teamIndex && issue.path[4] === field, ); const memberFieldInvalid = (teamIndex: number, memberIndex: number, field: string) => !validation.success && validation.error.issues.some((issue) => issue.path[0] === "groups" && issue.path[1] === activeTeamGroupIndex && issue.path[2] === "teams" && issue.path[3] === teamIndex && issue.path[4] === "members" && issue.path[5] === memberIndex && issue.path[6] === field, ); const localDraft = { teamGroups, activeTeamGroupIndex }; useEffect(() => { const timer = window.setTimeout(() => { try { setPresets(parseTeamPresets(window.localStorage.getItem(teamPresetStorageKey(guide.id)))); } catch { setPresets([]); } setPresetsHydrated(true); setPresetsGuideId(guide.id); }, 0); return () => window.clearTimeout(timer); }, [guide.id]); useEffect(() => { const timer = window.setTimeout(() => { try { const stored = JSON.parse( window.localStorage.getItem(multipleApplyStorageKey(guide.id)) ?? "null", ) as { selectedTeamIds?: unknown; applyFields?: unknown } | null; const teamIds = new Set(initial.teamGroups.flatMap((group) => group.teams.map((team) => team.id))); setSelectedTeamIds(new Set( Array.isArray(stored?.selectedTeamIds) ? stored.selectedTeamIds.filter( (id): id is string => typeof id === "string" && teamIds.has(id), ) : [], )); setApplyFields( Array.isArray(stored?.applyFields) ? stored.applyFields.filter( (field): field is TeamApplySelection => typeof field === "string", ) : [], ); } catch { setSelectedTeamIds(new Set()); setApplyFields([]); } setMultipleApplyGuideId(guide.id); }, 0); return () => window.clearTimeout(timer); }, [guide.id, initial.teamGroups]); useEffect(() => { if (multipleApplyGuideId !== guide.id) return; try { window.localStorage.setItem( multipleApplyStorageKey(guide.id), JSON.stringify({ selectedTeamIds: [...selectedTeamIds], applyFields, }), ); } catch { // Browser storage is optional; multiple apply remains usable without it. } }, [applyFields, guide.id, multipleApplyGuideId, selectedTeamIds]); useEffect(() => { if (!presetsHydrated || presetsGuideId !== guide.id) return; try { window.localStorage.setItem(teamPresetStorageKey(guide.id), JSON.stringify(presets)); document.cookie = `${teamPresetCookieKey(guide.id)}=${encodeURIComponent(JSON.stringify(presets.map(({ id, name, createdAt }) => ({ id, name, createdAt })) ))}; path=/; max-age=${60 * 60 * 24 * 365}; SameSite=Lax`; } catch { // Browser storage is optional; the editor remains usable without it. } }, [guide.id, presets, presetsGuideId, presetsHydrated]); const autosave = useAutosave({ guideId: guide.id, scope: "team", value, valid: validation.success, initialVersion: guide.version, persist: (next, expectedVersion, intent) => saveTeamSection(guide.id, { ...next, expectedVersion }, intent), storageKey: adminDraftKey(guide.id, "team"), localDraft, parseLocalDraft: (candidate) => parseEditorDraft(candidate, ["teamGroups"]), restoreLocalDraft: (draft) => { setTeamGroups(draft.teamGroups.map((group) => ({ ...group, name: group.name ?? "" }))); setActiveTeamGroupIndex(draft.activeTeamGroupIndex ?? 0); }, }); const handleTeamDragEnd = ({ active, over }: DragEndEvent) => { if (!over || active.id === over.id) return; setTeams((current) => { const from = current.findIndex((team) => team.clientId === active.id); const to = current.findIndex((team) => team.clientId === over.id); return from === -1 || to === -1 ? current : arrayMove(current, from, to); }); }; const handleTeamMemberDragEnd = ( teamId: string, { active, over }: DragEndEvent, ) => { if (!over || active.id === over.id) return; setTeams((current) => current.map((team) => { if (team.clientId !== teamId) return team; const memberIds = team.members.map(getMemberSortableId); const from = memberIds.indexOf(String(active.id)); const to = memberIds.indexOf(String(over.id)); return from === -1 || to === -1 ? team : { ...team, members: arrayMove(team.members, from, to) }; }), ); }; const updateTeam = ( teamIndex: number, update: Partial>, ) => { setTeams((current) => current.map((team, index) => index === teamIndex ? { ...team, ...update } : team, ), ); }; const updateTeamMember = ( teamIndex: number, memberIndex: number, update: Partial, ) => { setTeams((current) => current.map((team, index) => index === teamIndex ? { ...team, members: team.members.map((member, innerIndex) => { if (innerIndex !== memberIndex) return member; const nextMember = { ...member, ...update }; memberSortableIds.current.set( nextMember, getMemberSortableId(member), ); return nextMember; }), } : team, ), ); }; const toggleTeamSelection = (teamId: string, checked: boolean) => { setSelectedTeamIds((current) => { const next = new Set(current); if (checked) next.add(teamId); else next.delete(teamId); return next; }); }; const toggleApplyField = (field: TeamApplySelection, checked: boolean) => { setApplyFields((current) => { const next = new Set(current); if (checked) next.add(field); else next.delete(field); return [...next]; }); }; const applyChanges = () => { if (!sourceTeamId || !selectedTeamIds.size || !applyFields.length) return; const nextTeams = applyTeamFields( teams, sourceTeamId, selectedTeamIds, applyFields, ); const unsupported = findUnsupportedTeamWeapon( nextTeams, selectedTeamIds, characters, weapons, ); if (unsupported) { toast.error( `${unsupported.characterName} ไม่รองรับอาวุธ ${unsupported.weaponName}`, ); return; } setTeams(nextTeams); setSelectedTeamIds(new Set()); setApplyOpen(false); }; const savePreset = () => { const source = teams.find((team) => team.clientId === sourceTeamId); const name = presetName.trim(); if (!source || !name) return; const preset: TeamPreset = { id: createClientId("preset"), name, createdAt: new Date().toISOString(), team: { title: source.title, rotationSeconds: source.rotationSeconds, combo: source.combo, members: source.members.map((member) => ({ ...member })), }, }; setPresets((current) => limitPresets([preset, ...current])); setPresetName(""); setPresetOpen(false); }; const applyPreset = (preset: TeamPreset) => { if (!selectedTeamIds.size || !applyFields.length) return; const nextTeams = applyPresetFields( teams, preset, selectedTeamIds, applyFields, ); const unsupported = findUnsupportedTeamWeapon( nextTeams, selectedTeamIds, characters, weapons, ); if (unsupported) { toast.error( `${unsupported.characterName} ไม่รองรับอาวุธ ${unsupported.weaponName}`, ); return; } setTeams(nextTeams); setSelectedTeamIds(new Set()); }; const exportTeam = async (team: TeamToolValue) => { let serialized: string; try { serialized = serializeTeamTransfer(team); } catch { toast.error("Export Team ไม่สำเร็จ กรุณาตรวจสอบข้อมูล Team"); return; } const url = URL.createObjectURL(new Blob([serialized], { type: "application/json" })); const link = document.createElement("a"); link.href = url; link.download = teamTransferFileName(team.title); link.click(); URL.revokeObjectURL(url); try { await navigator.clipboard.writeText(serialized); toast.success(`คัดลอกและดาวน์โหลด ${team.title} แล้ว`); } catch { toast.success(`ดาวน์โหลด ${teamTransferFileName(team.title)} แล้ว`); } }; const appendImportedTeams = (importedTeams: Array>) => { if (!importedTeams.length) return false; for (const imported of importedTeams) { const catalogIssue = findInvalidTeamCatalogReference( imported, characters, weapons, artifacts, ); if (catalogIssue) { toast.error(catalogIssue); return false; } } setTeams((current) => [ ...current, ...cloneTeamImports(importedTeams, () => createClientId("team")), ]); return true; }; const importTeamFiles = async () => { try { const serializedTeams = await Promise.all(importFiles.map((file) => file.text())); if (!serializedTeams.length) return; const importedTeams = serializedTeams.map(parseTeamTransfer); if (!appendImportedTeams(importedTeams)) return; setImportFiles([]); setImportOpen(false); toast.success(`Import ${importedTeams.length} Team แล้ว`); } catch { toast.error("มีไฟล์ Team ที่ไม่ถูกต้องหรือเป็นเวอร์ชันที่ไม่รองรับ"); } }; const openGuideImport = async () => { setImportOpen(false); setGuideImportOpen(true); setActiveImportGuide(null); setSelectedImports({}); setGuideListQuery(""); setGuideImportError(null); setGuideImportLoading("guides"); const result = await listTeamImportGuides(guide.id); setGuideImportLoading(null); if (result.status === "success") setImportGuides(result.guides); else setGuideImportError(result.message); }; const openEverywhereImport = () => { everywhereSearchId.current += 1; setImportOpen(false); setEverywhereImportOpen(true); setEverywhereCharacterKey(""); setEverywhereSearchedKey(""); setEverywhereTeams([]); setEverywhereSelectedIds(new Set()); setIgnoreSameName(true); setEverywhereError(null); setEverywhereLoading(false); }; const searchEverywhereTeams = async (characterKey: string) => { const searchId = ++everywhereSearchId.current; setEverywhereSearchedKey(""); setEverywhereTeams([]); setEverywhereSelectedIds(new Set()); setEverywhereError(null); setEverywhereLoading(true); const result = await listTeamImportsForCharacter(guide.id, characterKey); if (searchId !== everywhereSearchId.current) return; setEverywhereLoading(false); if (result.status === "success") { setEverywhereTeams(result.teams); setEverywhereSearchedKey(characterKey); } else setEverywhereError(result.message); }; const importEverywhereTeams = (sourceTeams: TeamEverywhereImport[]) => { if (!sourceTeams.length) return; const importedTeams = sourceTeams.map(toTeamDraft); if (!appendImportedTeams(importedTeams)) return; setEverywhereImportOpen(false); setEverywhereSelectedIds(new Set()); toast.success(`Import ${importedTeams.length} Team แล้ว`); }; const enterImportGuide = async (guideId: string) => { setGuideImportError(null); setGuideImportLoading("teams"); const result = await loadTeamImportGuide(guideId); setGuideImportLoading(null); if (result.status === "success") { setGuideImportCondition("0"); setGuideTeamQuery(""); setGuideTeamSearchTarget("all"); setActiveImportGuide(result.guide); } else setGuideImportError(result.message); }; const confirmGuideImport = () => { const importedTeams = Object.values(selectedImports); if (!appendImportedTeams(importedTeams)) return; setGuideImportOpen(false); setSelectedImports({}); toast.success(`Import ${importedTeams.length} Team แล้ว`); }; return ( Team Calc ลากเพื่อเรียง Team แล้วใส่ความเสียหายของตัวละคร 4 ตัว ระบบจะแสดงสัดส่วนและ Total DPS และบันทึกอัตโนมัติ { const group = teamGroups[nextIndex]; if (!group) return; setActiveTeamGroupIndex(nextIndex); setSelectedTeamIds(new Set()); setSourceTeamId(group.teams[0]?.clientId ?? ""); }} onRename={setRenamingTeamGroupIndex} onReorder={(from, to) => { const reordered = reorderConditions(teamGroups, activeTeamGroupIndex, from, to); setTeamGroups(reordered.items); setActiveTeamGroupIndex(reordered.activeIndex); }} onAdd={(details) => { setTeamGroups([...teamGroups, { clientId: createClientId("team-condition"), ...details, note: "", teams: [] }]); setActiveTeamGroupIndex(teamGroups.length); setSelectedTeamIds(new Set()); setSourceTeamId(""); }} onDuplicate={() => { const source = teamGroups[activeTeamGroupIndex]; if (!source) return; const copiedTeams = source.teams.map((team) => ({ ...team, clientId: createClientId("team"), members: team.members.map((member) => ({ ...member })) })); setTeamGroups([...teamGroups, { clientId: createClientId("team-condition"), name: source.name, note: source.note, iconMediaId: source.iconMediaId, iconUrl: source.iconUrl, teams: copiedTeams }]); setActiveTeamGroupIndex(teamGroups.length); setSelectedTeamIds(new Set()); setSourceTeamId(copiedTeams[0]?.clientId ?? ""); }} onDelete={() => { setTeamGroups(teamGroups.filter((_, index) => index !== activeTeamGroupIndex)); setActiveTeamGroupIndex(Math.max(0, activeTeamGroupIndex - 1)); setSelectedTeamIds(new Set()); }} /> { if (!open) setRenamingTeamGroupIndex(null); }} onChange={(name) => setTeamGroups(teamGroups.map((group, index) => index === renamingTeamGroupIndex ? { ...group, name } : group))} onIconChange={(icon) => setTeamGroups(teamGroups.map((group, index) => index === renamingTeamGroupIndex ? { ...group, ...icon } : group))} /> Condition note (optional)