import React, { useState, useEffect } from "react"; import axios from "axios"; import { Button, Input, Switch, Textarea } from "@heroui/react"; import { useContestRefreshStore } from "@/stores/contestRefreshStore"; import { useTranslation } from "react-i18next"; type TranslationPayload = { cs?: string; en?: string; }; type ContestFromApi = { 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; url?: string | null; evaluator?: string | null; email?: string | null; email2?: string | null; is_mcr: boolean; is_sixhr: boolean; is_active: boolean; is_test: boolean; start_time: string | null; duration: number; logs_deadline_days: number; }; type ContestFormMode = "create" | "edit"; type ContestCreateFormProps = { mode?: ContestFormMode; // default "create" contest?: ContestFromApi | null; // pro edit onCreated?: (contest: ContestFromApi) => void; onUpdated?: (contest: ContestFromApi) => void; }; type Option = { id: number; name: string; code?: string | null }; const buildTranslationPayload = (cs: string, en: string): TranslationPayload => { const trimmedCs = cs.trim(); const trimmedEn = en.trim(); // nic nevyplněno → prázdný objekt if (!trimmedCs && !trimmedEn) { return {}; } // oba jazyky vyplněné → použij obě hodnoty if (trimmedCs && trimmedEn) { return { cs: trimmedCs, en: trimmedEn, }; } // vyplněný jen jeden jazyk → použij ho pro oba 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 normalizeStartTime = (time: string): string | undefined => { if (!time.trim()) return undefined; return time.length === 5 ? `${time}:00` : time; }; export default function ContestCreateForm({ mode = "create", contest, onCreated, onUpdated, }: ContestCreateFormProps) { const { t } = useTranslation("common"); const isEdit = mode === "edit" && contest != null; const [nameCs, setNameCs] = useState(""); const [nameEn, setNameEn] = useState(""); const [descriptionCs, setDescriptionCs] = useState(""); const [descriptionEn, setDescriptionEn] = useState(""); const [url, setUrl] = useState(""); const [evaluator, setEvaluator] = useState(""); const [email, setEmail] = useState(""); const [email2, setEmail2] = useState(""); const [startTime, setStartTime] = useState(""); const [durationHours, setDurationHours] = useState("24"); const [deadlineDays, setDeadlineDays] = useState("3"); const [isMcr, setIsMcr] = useState(false); const [isSixHr, setIsSixHr] = useState(false); const [isActive, setIsActive] = useState(true); const [availableBands, setAvailableBands] = useState([]); const [availableCategories, setAvailableCategories] = useState([]); const [availablePowerCategories, setAvailablePowerCategories] = useState([]); const [availableRuleSets, setAvailableRuleSets] = useState([]); const [selectedRuleSetId, setSelectedRuleSetId] = useState(null); const [selectedBandIds, setSelectedBandIds] = useState([]); const [selectedCategoryIds, setSelectedCategoryIds] = useState([]); const [selectedPowerCategoryIds, setSelectedPowerCategoryIds] = useState([]); const [loadingOptions, setLoadingOptions] = useState(false); const [error, setError] = useState(null); const [success, setSuccess] = useState(null); const [submitting, setSubmitting] = useState(false); // načti volby pro checkboxy useEffect(() => { let active = true; (async () => { try { setLoadingOptions(true); const [bandsRes, categoriesRes, powerCatsRes, ruleSetsRes] = await Promise.all([ 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): Option[] => Array.isArray(res.data ?? res) ? (res.data ?? res) : []; setAvailableBands(normalize(bandsRes.data ?? bandsRes)); setAvailableCategories(normalize(categoriesRes.data ?? categoriesRes)); setAvailablePowerCategories(normalize(powerCatsRes.data ?? powerCatsRes)); const normalizedRuleSets = normalize(ruleSetsRes.data ?? ruleSetsRes); setAvailableRuleSets(normalizedRuleSets); if (!isEdit && !selectedRuleSetId) { const defaultRuleSet = normalizedRuleSets.find((item) => item.code === "default_vhf_compat"); if (defaultRuleSet) { setSelectedRuleSetId(defaultRuleSet.id); } } } catch { if (!active) return; setError("Nepodařilo se načíst seznam pásem/kategorií."); } finally { if (active) setLoadingOptions(false); } })(); return () => { active = false; }; }, []); React.useEffect(() => { if (!isEdit || !contest) return; const name = extractTranslations(contest.name); const desc = extractTranslations(contest.description ?? null); setNameCs(name.cs ?? ""); setNameEn(name.en ?? ""); setDescriptionCs(desc.cs ?? ""); setDescriptionEn(desc.en ?? ""); setUrl(contest.url ?? ""); setEvaluator(contest.evaluator ?? ""); setEmail(contest.email ?? ""); setEmail2(contest.email2 ?? ""); setStartTime(contest.start_time ?? ""); setDurationHours(String(contest.duration ?? 24)); setDeadlineDays(String(contest.logs_deadline_days ?? 3)); setIsMcr(!!contest.is_mcr); setIsSixHr(!!contest.is_sixhr); setIsActive(!!contest.is_active); setSelectedBandIds(contest.bands?.map((b) => b.id) ?? []); setSelectedCategoryIds(contest.categories?.map((c) => c.id) ?? []); setSelectedPowerCategoryIds(contest.power_categories?.map((p) => p.id) ?? []); setSelectedRuleSetId(contest.rule_set_id ?? null); setError(null); setSuccess(null); }, [isEdit, contest]); const triggerRefresh = useContestRefreshStore((s) => s.triggerRefresh); const handleSubmit = async (event: React.FormEvent) => { event.preventDefault(); setError(null); setSuccess(null); const namePayload = buildTranslationPayload(nameCs, nameEn); if (Object.keys(namePayload).length === 0) { setError("Vyplň alespoň jeden překlad názvu závodu."); return; } const descriptionPayload = buildTranslationPayload(descriptionCs, descriptionEn); const payload: Record = { name: namePayload, is_mcr: isMcr, is_sixhr: isSixHr, is_active: isActive, }; if (Object.keys(descriptionPayload).length > 0) { payload.description = descriptionPayload; } if (url.trim()) payload.url = url.trim(); if (evaluator.trim()) payload.evaluator = evaluator.trim(); if (email.trim()) payload.email = email.trim(); if (email2.trim()) payload.email2 = email2.trim(); const normalizedStartTime = normalizeStartTime(startTime); if (normalizedStartTime) payload.start_time = normalizedStartTime; const durationNumber = durationHours.trim() ? Number(durationHours) : undefined; const deadlineNumber = deadlineDays.trim() ? Number(deadlineDays) : undefined; if (durationNumber !== undefined && Number.isNaN(durationNumber)) { setError(t("Délka závodu musí být číslo.")); return; } if (deadlineNumber !== undefined && Number.isNaN(deadlineNumber)) { setError(t("Uzávěrka logů musí být číslo.")); return; } if (durationNumber !== undefined) payload.duration = durationNumber; if (deadlineNumber !== undefined) payload.logs_deadline_days = deadlineNumber; payload.band_ids = selectedBandIds; payload.category_ids = selectedCategoryIds; payload.power_category_ids = selectedPowerCategoryIds; if (selectedRuleSetId) payload.rule_set_id = selectedRuleSetId; try { setSubmitting(true); await axios.get("/sanctum/csrf-cookie", { withCredentials: true }); let response; if (isEdit && contest) { response = await axios.put(`/api/contests/${contest.id}`, payload, { headers: { Accept: "application/json", }, withCredentials: true, withXSRFToken: true, }); setSuccess("Závod byl upraven."); triggerRefresh(); onUpdated?.(response.data); } else { response = await axios.post("/api/contests", payload, { headers: { Accept: "application/json", }, withCredentials: true, withXSRFToken: true, }); setSuccess("Závod byl vytvořen."); triggerRefresh(); onCreated?.(response.data); } } catch (err) { if (axios.isAxiosError(err)) { const apiErrors = err.response?.data?.errors ?? err.response?.data?.message ?? "Nepodařilo se uložit závod."; setError( typeof apiErrors === "string" ? apiErrors : "Nepodařilo se uložit závod." ); } else { setError("Nepodařilo se uložit závod."); } } finally { setSubmitting(false); } }; const toggleId = (id: number, list: number[], setter: (v: number[]) => void) => { setter(list.includes(id) ? list.filter((x) => x !== id) : [...list, id]); }; return ( Závod je aktivní MČR 6H závod Pásma {availableBands.map((band) => ( toggleId(band.id, selectedBandIds, setSelectedBandIds)} disabled={loadingOptions || submitting} /> {band.name} ))} Kategorie {availableCategories.map((cat) => ( toggleId(cat.id, selectedCategoryIds, setSelectedCategoryIds)} disabled={loadingOptions || submitting} /> {cat.name} ))} Výkonové kategorie {availablePowerCategories.map((p) => ( toggleId(p.id, selectedPowerCategoryIds, setSelectedPowerCategoryIds)} disabled={loadingOptions || submitting} /> {p.name} ))} Ruleset setSelectedRuleSetId(e.target.value ? Number(e.target.value) : null)} disabled={loadingOptions || submitting} className="w-full border rounded px-3 py-2 text-sm bg-white dark:bg-gray-900" > -- {availableRuleSets.map((ruleSet) => ( {ruleSet.name} ))} {error && {error}} {success && {success}} {isEdit ? "Uložit změny" : "Vytvořit závod"} ); }
Pásma
Kategorie
Výkonové kategorie
Ruleset
{error}
{success}