import React, { useEffect, useMemo, useState } from "react"; import axios from "axios"; import { Button, Input, Switch, Textarea } from "@heroui/react"; import { useTranslation } from "react-i18next"; import { useLanguageStore } from "@/stores/languageStore"; type TranslationPayload = { cs?: string; en?: string; }; type ContestOption = { id: number; name: string | TranslationPayload; bands?: { id: number; name: string }[]; categories?: { id: number; name: string }[]; power_categories?: { id: number; name: string }[]; rule_set_id?: number | null; duration?: number; logs_deadline_days?: number; }; export type RoundFromApi = { id: number; contest_id: number; name: string | TranslationPayload; description?: string | TranslationPayload | null; bands?: { id: number; name: string }[]; categories?: { id: number; name: string }[]; power_categories?: { id: number; name: string }[]; rule_set_id?: number | null; is_active: boolean; is_test: boolean; is_sixhr: boolean; start_time: string | null; end_time: string | null; logs_deadline: string | null; }; type RoundFormMode = "create" | "edit"; type RoundCreateFormProps = { mode?: RoundFormMode; // default "create" round?: RoundFromApi | null; // pro edit onCreated?: (round: RoundFromApi) => void; onUpdated?: (round: RoundFromApi) => void; contestId?: number | null; }; const buildTranslationPayload = (cs: string, en: string): TranslationPayload => { const trimmedCs = cs.trim(); const trimmedEn = en.trim(); if (!trimmedCs && !trimmedEn) { return {}; } if (trimmedCs && trimmedEn) { return { cs: trimmedCs, en: trimmedEn, }; } const value = trimmedCs || trimmedEn; return { cs: value, en: value, }; }; const extractTranslations = ( field: string | TranslationPayload | null | undefined ): TranslationPayload => { if (!field) return {}; if (typeof field === "string") return { cs: field }; return field; }; const toDatetimeLocal = (value: string | null): string => { if (!value) return ""; const date = new Date(value); if (Number.isNaN(date.getTime())) return value; const pad = (n: number) => `${n}`.padStart(2, "0"); return `${date.getFullYear()}-${pad(date.getMonth() + 1)}-${pad(date.getDate())}T${pad(date.getHours())}:${pad(date.getMinutes())}`; }; const normalizeDatetime = (value: string): string | undefined => { if (!value.trim()) return undefined; return value; }; const isSixHourBandName = (name?: string | null) => { if (!name) return false; const lower = name.toLowerCase(); return lower.includes("145") || lower.includes("435"); }; export default function RoundCreateForm({ mode = "create", round, onCreated, onUpdated, contestId: forcedContestId = null, }: RoundCreateFormProps) { const { t } = useTranslation("common"); const locale = useLanguageStore((s) => s.locale); const isEdit = mode === "edit" && round != null; const [contestId, setContestId] = useState(forcedContestId ? String(forcedContestId) : ""); const [contests, setContests] = useState([]); const [loadingContests, setLoadingContests] = useState(false); const [nameCs, setNameCs] = useState(""); const [nameEn, setNameEn] = useState(""); const [descriptionCs, setDescriptionCs] = useState(""); const [descriptionEn, setDescriptionEn] = useState(""); const [startTime, setStartTime] = useState(""); const [endTime, setEndTime] = useState(""); const [logsDeadline, setLogsDeadline] = useState(""); const [contestDurationHours, setContestDurationHours] = useState(null); const [contestDeadlineDays, setContestDeadlineDays] = useState(null); const [isActive, setIsActive] = useState(true); const [isTest, setIsTest] = useState(false); const [isSixHr, setIsSixHr] = useState(false); const [availableBands, setAvailableBands] = useState([]); const [availableCategories, setAvailableCategories] = useState([]); const [availablePowerCategories, setAvailablePowerCategories] = useState([]); const [selectedBandIds, setSelectedBandIds] = useState([]); const [selectedCategoryIds, setSelectedCategoryIds] = useState([]); const [selectedPowerCategoryIds, setSelectedPowerCategoryIds] = useState([]); const [availableRuleSets, setAvailableRuleSets] = useState<{ id: number; name: string; code?: string | null }[]>([]); const [selectedRuleSetId, setSelectedRuleSetId] = useState(null); const [error, setError] = useState(null); const [success, setSuccess] = useState(null); const [submitting, setSubmitting] = useState(false); const hasAllowedSixHrBand = useMemo(() => { if (!selectedBandIds.length) return false; return availableBands.some((b) => selectedBandIds.includes(b.id) && isSixHourBandName(b.name)); }, [availableBands, selectedBandIds]); // načti seznam závodů pro select useEffect(() => { let active = true; (async () => { try { setLoadingContests(true); const [contestsRes, bandsRes, categoriesRes, powerCatsRes, ruleSetsRes] = await Promise.all([ axios.get("/api/contests", { withCredentials: true, headers: { Accept: "application/json" }, params: { lang: locale }, }), axios.get("/api/bands", { headers: { Accept: "application/json" } }), axios.get("/api/categories", { headers: { Accept: "application/json" } }), axios.get("/api/power-categories", { headers: { Accept: "application/json" } }), axios.get("/api/evaluation-rule-sets", { headers: { Accept: "application/json" } }), ]); if (!active) return; const normalize = (res: any) => (Array.isArray(res?.data) ? res.data : res?.data?.data ?? res.data ?? []); setContests(normalize(contestsRes)); setAvailableBands(normalize(bandsRes)); setAvailableCategories(normalize(categoriesRes)); setAvailablePowerCategories(normalize(powerCatsRes)); const ruleSets = normalize(ruleSetsRes); setAvailableRuleSets(ruleSets); if (!isEdit && !selectedRuleSetId) { const defaultRuleSet = ruleSets.find((item: any) => item.code === "default_vhf_compat"); if (defaultRuleSet) { setSelectedRuleSetId(defaultRuleSet.id); } } } catch { if (!active) return; setError(t("unable_to_load_contests") ?? "Nepodařilo se načíst seznam závodů."); } finally { if (active) setLoadingContests(false); } })(); return () => { active = false; }; }, [locale, t]); // předvyplnění při editaci useEffect(() => { if (!isEdit || !round) return; const name = extractTranslations(round.name); const desc = extractTranslations(round.description ?? null); setContestId(String(round.contest_id ?? "")); setNameCs(name.cs ?? ""); setNameEn(name.en ?? ""); setDescriptionCs(desc.cs ?? ""); setDescriptionEn(desc.en ?? ""); setStartTime(toDatetimeLocal(round.start_time)); setEndTime(toDatetimeLocal(round.end_time)); setLogsDeadline(toDatetimeLocal(round.logs_deadline)); setIsActive(!!round.is_active); setIsTest(!!round.is_test); setIsSixHr(!!round.is_sixhr); setSelectedBandIds(Array.isArray((round as any).bands) ? (round as any).bands.map((b: any) => b.id) : []); setSelectedCategoryIds(Array.isArray((round as any).categories) ? (round as any).categories.map((c: any) => c.id) : []); setSelectedPowerCategoryIds(Array.isArray((round as any).power_categories) ? (round as any).power_categories.map((p: any) => p.id) : []); setSelectedRuleSetId((round as any).rule_set_id ?? null); setError(null); setSuccess(null); }, [isEdit, round]); // pokud při editaci chybí vazby, doťukej detail kola useEffect(() => { if (!isEdit || !round) return; const needsDetail = !(Array.isArray((round as any).bands) && (round as any).bands.length) || !(Array.isArray((round as any).categories) && (round as any).categories.length) || !(Array.isArray((round as any).power_categories) && (round as any).power_categories.length); if (!needsDetail) return; let active = true; (async () => { try { const res = await axios.get(`/api/rounds/${round.id}`, { headers: { Accept: "application/json" }, params: { lang: locale }, withCredentials: true, }); if (!active) return; const data = res.data; setSelectedBandIds(Array.isArray(data.bands) ? data.bands.map((b: any) => b.id) : []); setSelectedCategoryIds(Array.isArray(data.categories) ? data.categories.map((c: any) => c.id) : []); setSelectedPowerCategoryIds(Array.isArray(data.power_categories) ? data.power_categories.map((p: any) => p.id) : []); setIsActive(!!data.is_active); setIsTest(!!data.is_test); setIsSixHr(!!data.is_sixhr); setStartTime(toDatetimeLocal(data.start_time)); setEndTime(toDatetimeLocal(data.end_time)); setLogsDeadline(toDatetimeLocal(data.logs_deadline)); } catch { // ignore } })(); return () => { active = false; }; }, [isEdit, round, locale]); // pokud máme zvolený contest, načti detail pro defaulty (vazby) a parametry useEffect(() => { if (!contestId) return; let active = true; (async () => { try { const res = await axios.get(`/api/contests/${contestId}`, { headers: { Accept: "application/json" }, params: { lang: locale }, withCredentials: true, }); if (!active) return; const data = res.data; if (!isEdit) { if (data?.bands) setSelectedBandIds(data.bands.map((b) => b.id)); if (data?.categories) setSelectedCategoryIds(data.categories.map((c) => c.id)); if (data?.power_categories) setSelectedPowerCategoryIds(data.power_categories.map((p) => p.id)); if (data?.rule_set_id) setSelectedRuleSetId(data.rule_set_id); } if (typeof data.duration === "number") setContestDurationHours(data.duration); if (typeof data.logs_deadline_days === "number") setContestDeadlineDays(data.logs_deadline_days); } catch { // ignore } })(); return () => { active = false; }; }, [contestId, locale, isEdit]); // Auto-nastavení konce kola podle startu a délky závodu useEffect(() => { if (!startTime || endTime || contestDurationHours == null) return; const startDate = new Date(startTime); if (Number.isNaN(startDate.getTime())) return; const endDate = new Date(startDate); endDate.setHours(endDate.getHours() + contestDurationHours); setEndTime(toDatetimeLocal(endDate.toISOString())); }, [startTime, endTime, contestDurationHours]); // Auto-nastavení uzávěrky logů při vyplnění/změně konce useEffect(() => { if (!endTime || contestDeadlineDays == null) return; const endDate = new Date(endTime); if (Number.isNaN(endDate.getTime())) return; const deadlineDate = new Date(endDate); deadlineDate.setDate(deadlineDate.getDate() + contestDeadlineDays); setLogsDeadline(toDatetimeLocal(deadlineDate.toISOString())); }, [endTime, contestDeadlineDays]); useEffect(() => { if (isSixHr && !hasAllowedSixHrBand) { setIsSixHr(false); } }, [hasAllowedSixHrBand, isSixHr]); const handleSubmit = async (event: React.FormEvent) => { event.preventDefault(); setError(null); setSuccess(null); if (!contestId.trim()) { setError(t("Vyber závod.") ?? "Vyber závod."); return; } const namePayload = buildTranslationPayload(nameCs, nameEn); if (Object.keys(namePayload).length === 0) { setError(t("Vyplň alespoň jeden překlad názvu kola.") ?? "Vyplň alespoň jeden překlad názvu kola."); return; } const descriptionPayload = buildTranslationPayload(descriptionCs, descriptionEn); const payload: Record = { contest_id: Number(contestId), name: namePayload, is_active: isActive, is_test: isTest, is_sixhr: isSixHr, }; if (Object.keys(descriptionPayload).length > 0) { payload.description = descriptionPayload; } const normalizedStart = normalizeDatetime(startTime); const normalizedEnd = normalizeDatetime(endTime); const normalizedDeadline = normalizeDatetime(logsDeadline); if (normalizedStart) payload.start_time = normalizedStart; if (normalizedEnd) payload.end_time = normalizedEnd; if (normalizedDeadline) payload.logs_deadline = normalizedDeadline; payload.band_ids = selectedBandIds; payload.category_ids = selectedCategoryIds; payload.power_category_ids = selectedPowerCategoryIds; if (selectedRuleSetId) payload.rule_set_id = selectedRuleSetId; if (isSixHr && !hasAllowedSixHrBand) { payload.is_sixhr = false; setIsSixHr(false); setError(t("six_hr_band_warning") ?? "6H kategorie je povolena jen pro pásma 145 MHz a 435 MHz."); } // 6h jen pro pásma 145 / 435 – pokud nesplňuje, vypni is_sixhr if (isSixHr) { const bands = (round as any)?.bands ?? []; const currentBands = (bands.length ? bands.map((b: any) => b.id) : selectedBandIds) ?? []; const hasAllowedBand = (round as any)?.bands ? bands.some((b: any) => isSixHourBandName(b.name)) : selectedBandIds.some((id) => { const found = (round as any)?.availableBands?.find?.((b: any) => b.id === id); return found ? isSixHourBandName(found.name) : true; }); if (!hasAllowedBand) { payload.is_sixhr = false; setIsSixHr(false); } } try { setSubmitting(true); await axios.get("/sanctum/csrf-cookie", { withCredentials: true }); let response; if (isEdit && round) { response = await axios.put(`/api/rounds/${round.id}`, payload, { headers: { Accept: "application/json" }, withCredentials: true, withXSRFToken: true, }); setSuccess(t("Kolo bylo upraveno.") ?? "Kolo bylo upraveno."); onUpdated?.(response.data as RoundFromApi); } else { response = await axios.post("/api/rounds", payload, { headers: { Accept: "application/json" }, withCredentials: true, withXSRFToken: true, }); setSuccess(t("Kolo bylo vytvořeno.") ?? "Kolo bylo vytvořeno."); onCreated?.(response.data as RoundFromApi); // reset formuláře po vytvoření setNameCs(""); setNameEn(""); setDescriptionCs(""); setDescriptionEn(""); setStartTime(""); setEndTime(""); setLogsDeadline(""); setIsActive(true); setIsTest(false); setIsSixHr(false); } } catch (e: any) { if (axios.isAxiosError(e)) { setError(e.response?.data?.message ?? "Chyba při ukládání kola."); } else { setError("Chyba při ukládání kola."); } } finally { setSubmitting(false); } }; return (
setNameCs(e.target.value)} isRequired /> setNameEn(e.target.value)} />