diff --git a/src/app/admin/matches/upload/page.tsx b/src/app/admin/matches/upload/page.tsx index c7b0b11..4106a96 100644 --- a/src/app/admin/matches/upload/page.tsx +++ b/src/app/admin/matches/upload/page.tsx @@ -4,33 +4,82 @@ import { useState, useRef, useEffect } from "react" import { useRouter } from "next/navigation" import Navigation from "@/components/Navigation" +// Interface for a single manual match entry +interface ManualMatchEntry { + id: number + round: string + table: string + team1P1: string + team1P2: string + team2P1: string + team2P2: string + team1Score: string + team2Score: string + isCasual: boolean +} + export default function UploadMatchesPage() { const router = useRouter() + const [activeTab, setActiveTab] = useState<"csv" | "manual">("csv") + + // CSV Upload state const [selectedFile, setSelectedFile] = useState(null) const [selectedTournament, setSelectedTournament] = useState("") - const [error, setError] = useState("") - const [success, setSuccess] = useState("") - const [isLoading, setIsLoading] = useState(false) - const [tournaments, setTournaments] = useState<{ id: number; name: string }[]>([]) + const [csvError, setCsvError] = useState("") + const [csvSuccess, setCsvSuccess] = useState("") + const [isCsvLoading, setIsCsvLoading] = useState(false) const fileInputRef = useRef(null) + + // Manual Entry state + const [manualTournament, setManualTournament] = useState("") + const [manualMatches, setManualMatches] = useState([ + { + id: Date.now(), + round: "", + table: "", + team1P1: "", + team1P2: "", + team2P1: "", + team2P2: "", + team1Score: "", + team2Score: "", + isCasual: false, + }, + ]) + const [manualError, setManualError] = useState("") + const [manualSuccess, setManualSuccess] = useState("") + const [isManualLoading, setIsManualLoading] = useState(false) + + // Player list for autocomplete + const [players, setPlayers] = useState<{ id: number; name: string }[]>([]) + const [tournaments, setTournaments] = useState<{ id: number; name: string }[]>([]) - // Fetch tournaments on mount + // Fetch tournaments and players on mount useEffect(() => { + // Fetch tournaments fetch(`${window.location.origin}/api/tournaments`) .then((res) => res.json()) .then((data) => { if (data.tournaments) { setTournaments(data.tournaments) - // If no tournaments exist, create one automatically - if (data.tournaments.length === 0) { - createDefaultTournament() - } else if (data.tournaments.length > 0) { - // Auto-select the most recent tournament - setSelectedTournament(data.tournaments[0].id.toString()) + if (data.tournaments.length > 0) { + const recentId = data.tournaments[0].id.toString() + setSelectedTournament(recentId) + setManualTournament(recentId) } } }) .catch((err) => console.error("Failed to fetch tournaments:", err)) + + // Fetch players + fetch(`${window.location.origin}/api/players`) + .then((res) => res.json()) + .then((data) => { + if (data.players) { + setPlayers(data.players) + } + }) + .catch((err) => console.error("Failed to fetch players:", err)) }, []) const createDefaultTournament = async () => { @@ -46,42 +95,45 @@ export default function UploadMatchesPage() { }) const data = await response.json() if (response.ok && data.tournament) { - setTournaments([data.tournament]) + const newTournaments = [data.tournament] + setTournaments(newTournaments) setSelectedTournament(data.tournament.id.toString()) + setManualTournament(data.tournament.id.toString()) } } catch (err) { console.error("Failed to create default tournament:", err) } } + // CSV Upload handlers const handleFileChange = (e: React.ChangeEvent) => { const file = e.target.files?.[0] if (file) { if (!file.name.endsWith(".csv")) { - setError("Please upload a CSV file") + setCsvError("Please upload a CSV file") return } setSelectedFile(file) - setError("") + setCsvError("") } } - const handleSubmit = async (e: React.FormEvent) => { + const handleCsvSubmit = async (e: React.FormEvent) => { e.preventDefault() - setError("") - setSuccess("") + setCsvError("") + setCsvSuccess("") if (!selectedFile) { - setError("Please select a CSV file to upload") + setCsvError("Please select a CSV file to upload") return } if (!selectedTournament) { - setError("Please select a tournament") + setCsvError("Please select a tournament") return } - setIsLoading(true) + setIsCsvLoading(true) try { const formData = new FormData() @@ -99,7 +151,7 @@ export default function UploadMatchesPage() { throw new Error(data.error || "Failed to upload CSV") } - setSuccess( + setCsvSuccess( `Successfully imported ${data.importedCount} matches. ` + `${data.errorCount || 0} errors occurred.` + (data.errors ? `\n\nErrors:\n${data.errors.join("\n")}` : "") @@ -107,18 +159,127 @@ export default function UploadMatchesPage() { // Reset form setSelectedFile(null) - setSelectedTournament("") if (fileInputRef.current) { fileInputRef.current.value = "" } } catch (err) { if (err instanceof Error) { - setError(err.message) + setCsvError(err.message) } else { - setError("An unknown error occurred") + setCsvError("An unknown error occurred") } } finally { - setIsLoading(false) + setIsCsvLoading(false) + } + } + + // Manual Entry handlers + const addMatchEntry = () => { + const newEntry: ManualMatchEntry = { + id: Date.now(), + round: "", + table: "", + team1P1: "", + team1P2: "", + team2P1: "", + team2P2: "", + team1Score: "", + team2Score: "", + isCasual: false, + } + setManualMatches([...manualMatches, newEntry]) + } + + const removeMatchEntry = (id: number) => { + if (manualMatches.length > 1) { + setManualMatches(manualMatches.filter((m) => m.id !== id)) + } + } + + const updateMatchEntry = (id: number, field: keyof ManualMatchEntry, value: string | boolean) => { + setManualMatches( + manualMatches.map((m) => (m.id === id ? { ...m, [field]: value } : m)) + ) + } + + const handleManualSubmit = async (e: React.FormEvent) => { + e.preventDefault() + setManualError("") + setManualSuccess("") + + if (!manualTournament) { + setManualError("Please select a tournament") + return + } + + // Validate at least one match has required data + const validMatches = manualMatches.filter( + (m) => + m.team1P1 && m.team1P2 && m.team2P1 && m.team2P2 && m.team1Score && m.team2Score + ) + + if (validMatches.length === 0) { + setManualError("Please enter at least one match with all player names and scores") + return + } + + setIsManualLoading(true) + + try { + const matchesData = validMatches.map((m) => ({ + eventId: parseInt(manualTournament), + round: m.round ? parseInt(m.round) : undefined, + table: m.table || undefined, + team1P1Name: m.team1P1, + team1P2Name: m.team1P2, + team2P1Name: m.team2P1, + team2P2Name: m.team2P2, + team1Score: parseInt(m.team1Score), + team2Score: parseInt(m.team2Score), + isCasual: m.isCasual, + })) + + const response = await fetch(`${window.location.origin}/api/matches/bulk`, { + method: "POST", + headers: { "Content-Type": "application/json" }, + body: JSON.stringify({ matches: matchesData }), + }) + + const data = await response.json() + + if (!response.ok) { + throw new Error(data.error || "Failed to create matches") + } + + setManualSuccess( + `Successfully created ${data.importedCount} matches. ` + + `${data.errorCount || 0} errors occurred.` + + (data.errors ? `\n\nErrors:\n${data.errors.join("\n")}` : "") + ) + + // Reset form + setManualMatches([ + { + id: Date.now(), + round: "", + table: "", + team1P1: "", + team1P2: "", + team2P1: "", + team2P2: "", + team1Score: "", + team2Score: "", + isCasual: false, + }, + ]) + } catch (err) { + if (err instanceof Error) { + setManualError(err.message) + } else { + setManualError("An unknown error occurred") + } + } finally { + setIsManualLoading(false) } } @@ -126,174 +287,367 @@ export default function UploadMatchesPage() {
-
+

- Upload Match Results (CSV) + Upload Match Results

-
-
- {error && ( -
-
- {error} -
-
- )} - - {success && ( -
-
- {success} -
-
- )} + {/* Tab Navigation */} +
+ +
- {/* Tournament Selection */} -
- -
- +
+ {/* CSV Upload Tab */} + {activeTab === "csv" && ( + + {csvError && ( +
+
+ {csvError} +
+
+ )} + + {csvSuccess && ( +
+
+ {csvSuccess} +
+
+ )} + + {/* Tournament Selection */} +
+ +
+ +
+
+ + {/* File Upload */} +
+ +
+
+ +
+ +

or drag and drop

+
+

+ CSV file with match results +

+ {selectedFile && ( +

+ Selected: {selectedFile.name} +

+ )} +
+
+
+ + {/* CSV Format Guide */} +
+

+ CSV Format Requirements +

+
    +
  • Event #: Tournament ID (optional if selected above)
  • +
  • Round: Round number (1, 2, 3...)
  • +
  • Table: Table name
  • +
  • Seat 1: Player 1 name (Team 1)
  • +
  • Seat 3: Player 2 name (Team 1)
  • +
  • Odds Points: Score for Team 1
  • +
  • Seat 2: Player 1 name (Team 2)
  • +
  • Seat 4: Player 2 name (Team 2)
  • +
  • Evens Points: Score for Team 2
  • +
+
+ + {/* Submit Button */} +
-
+ + )} - {/* File Upload */} -
- -
-
- -
- -

or drag and drop

+ {/* Manual Entry Tab */} + {activeTab === "manual" && ( +
+ {manualError && ( +
+
+ {manualError}
-

- CSV file with match results -

- {selectedFile && ( -

- Selected: {selectedFile.name} -

- )} +
+ )} + + {manualSuccess && ( +
+
+ {manualSuccess} +
+
+ )} + + {/* Tournament Selection */} +
+ +
+
-
- {/* CSV Format Guide */} -
-

- CSV Format Requirements -

-
    -
  • Event #: Tournament ID (optional if selected above)
  • -
  • Round: Round number (1, 2, 3...)
  • -
  • Table: Table name (Clubs, Hearts, Diamonds, Spades, Stars)
  • -
  • Seat 1: Player 1 name (Odds team)
  • -
  • Seat 3: Player 2 name (Odds team)
  • -
  • Odds Points: Score for Odds team
  • -
  • Seat 2: Player 1 name (Evens team)
  • -
  • Seat 4: Player 2 name (Evens team)
  • -
  • Evens Points: Score for Evens team
  • -
  • Winner: "Odds" or "Evens" (optional)
  • -
-
+ {/* Manual Match Entries */} +
+
+

+ Match Entries ({manualMatches.length}) +

+ +
- {/* Submit Button */} -
- -
- + {manualMatches.map((match, index) => ( +
+
+ Match {index + 1} + {manualMatches.length > 1 && ( + + )} +
- {/* Sample CSV */} -
-

- Sample CSV Format -

-
-                {`Event #,Round,Table,Seat 1,Seat 3,Odds Points,Seat 2,Seat 4,Evens Points,Winner
-1,1,Clubs,Derrick,Jesse C,5,Emma,Alissa,10,Evens
-1,1,Hearts,Kevin,Andy,8,Ellie,Jesse,6,Odds`}
-              
-
+
+ {/* Round and Table */} +
+ + updateMatchEntry(match.id, "round", e.target.value)} + className="mt-1 block w-full border border-gray-300 rounded-md shadow-sm py-1 px-2 text-sm focus:outline-none focus:ring-green-500 focus:border-green-500" + placeholder="1" + /> +
+
+ + updateMatchEntry(match.id, "table", e.target.value)} + className="mt-1 block w-full border border-gray-300 rounded-md shadow-sm py-1 px-2 text-sm focus:outline-none focus:ring-green-500 focus:border-green-500" + placeholder="Table 1" + /> +
+
+ {/* Team 1 */} +
+
+ Team 1 + updateMatchEntry(match.id, "team1Score", e.target.value)} + className="w-16 text-center border border-blue-300 rounded py-1 px-2 text-sm" + placeholder="Score" + min="0" + /> +
+
+ updateMatchEntry(match.id, "team1P1", e.target.value)} + list="players-list" + className="block w-full border border-blue-300 rounded-md shadow-sm py-1 px-2 text-sm focus:outline-none focus:ring-blue-500 focus:border-blue-500" + placeholder="Player 1" + /> + updateMatchEntry(match.id, "team1P2", e.target.value)} + list="players-list" + className="block w-full border border-blue-300 rounded-md shadow-sm py-1 px-2 text-sm focus:outline-none focus:ring-blue-500 focus:border-blue-500" + placeholder="Player 2" + /> +
+
+ + {/* Team 2 */} +
+
+ Team 2 + updateMatchEntry(match.id, "team2Score", e.target.value)} + className="w-16 text-center border border-red-300 rounded py-1 px-2 text-sm" + placeholder="Score" + min="0" + /> +
+
+ updateMatchEntry(match.id, "team2P1", e.target.value)} + list="players-list" + className="block w-full border border-red-300 rounded-md shadow-sm py-1 px-2 text-sm focus:outline-none focus:ring-red-500 focus:border-red-500" + placeholder="Player 1" + /> + updateMatchEntry(match.id, "team2P2", e.target.value)} + list="players-list" + className="block w-full border border-red-300 rounded-md shadow-sm py-1 px-2 text-sm focus:outline-none focus:ring-red-500 focus:border-red-500" + placeholder="Player 2" + /> +
+
+ + {/* Casual Match Checkbox */} +
+ updateMatchEntry(match.id, "isCasual", e.target.checked)} + className="h-4 w-4 text-green-600 focus:ring-green-500 border-gray-300 rounded" + /> + +
+
+ ))} +
+ + {/* Player autocomplete datalist */} + + {players.map((player) => ( + + + {/* Submit Button */} +
+ +
+ + )} + + {/* Back Button */}