mirror of
https://github.com/Dvorinka/MyClubServer.git
synced 2026-06-04 02:32:57 +00:00
dev day #65,5
This commit is contained in:
@@ -22,6 +22,8 @@ type MatchItem = {
|
||||
time: string; // HH:MM
|
||||
home: string;
|
||||
away: string;
|
||||
home_id?: string;
|
||||
away_id?: string;
|
||||
venue?: string;
|
||||
home_logo_url?: string;
|
||||
away_logo_url?: string;
|
||||
@@ -51,6 +53,7 @@ const CalendarPage: React.FC = () => {
|
||||
const [searchParams] = useSearchParams();
|
||||
const toast = useToast();
|
||||
const [clubName, setClubName] = useState<string>('');
|
||||
const [clubId, setClubId] = useState<string>('');
|
||||
const [clubType, setClubType] = useState<'football' | 'futsal'>('football');
|
||||
const [standings, setStandings] = useState<any[]>([]);
|
||||
|
||||
@@ -264,6 +267,8 @@ const CalendarPage: React.FC = () => {
|
||||
time,
|
||||
home: m.home,
|
||||
away: m.away,
|
||||
home_id: m.home_id,
|
||||
away_id: m.away_id,
|
||||
venue: m.venue,
|
||||
home_logo_url: getOverrideLogo(m.home, m.home_logo_url),
|
||||
away_logo_url: getOverrideLogo(m.away, m.away_logo_url),
|
||||
@@ -307,6 +312,8 @@ const CalendarPage: React.FC = () => {
|
||||
time,
|
||||
home: m.home,
|
||||
away: m.away,
|
||||
home_id: m.home_id,
|
||||
away_id: m.away_id,
|
||||
venue: m.venue,
|
||||
home_logo_url: getOverrideLogo(m.home, m.home_logo_url),
|
||||
away_logo_url: getOverrideLogo(m.away, m.away_logo_url),
|
||||
@@ -399,6 +406,7 @@ const CalendarPage: React.FC = () => {
|
||||
setCompLinks(compLinkMap);
|
||||
setStandings(standingsData);
|
||||
if (json?.name) setClubName(String(json.name));
|
||||
if (json?.club_internal_id) setClubId(String(json.club_internal_id));
|
||||
if (json?.club_type) setClubType(json.club_type);
|
||||
// Set active tab from query ?comp=<id>
|
||||
const compQ = searchParams.get('comp');
|
||||
@@ -521,8 +529,21 @@ const CalendarPage: React.FC = () => {
|
||||
|
||||
const s = parseScore(m.score);
|
||||
if (!s) return null;
|
||||
const ourIsHome = isClubTeam(m.home);
|
||||
const ourIsAway = isClubTeam(m.away);
|
||||
|
||||
// First try ID-based matching (most reliable)
|
||||
let ourIsHome = false;
|
||||
let ourIsAway = false;
|
||||
if (clubId && m.home_id && m.away_id) {
|
||||
ourIsHome = m.home_id === clubId;
|
||||
ourIsAway = m.away_id === clubId;
|
||||
}
|
||||
|
||||
// Fallback to name matching if IDs not available or no match
|
||||
if (!ourIsHome && !ourIsAway) {
|
||||
ourIsHome = isClubTeam(m.home);
|
||||
ourIsAway = isClubTeam(m.away);
|
||||
}
|
||||
|
||||
if (!ourIsHome && !ourIsAway) return null; // unknown perspective
|
||||
if (s.h === s.a) return { label: 'Remíza', color: 'blue' };
|
||||
const ourGoals = ourIsHome ? s.h : s.a;
|
||||
|
||||
@@ -15,6 +15,8 @@ type MatchItem = {
|
||||
time: string;
|
||||
home: string;
|
||||
away: string;
|
||||
home_id?: string;
|
||||
away_id?: string;
|
||||
home_logo_url?: string;
|
||||
away_logo_url?: string;
|
||||
score?: string | null;
|
||||
@@ -23,7 +25,7 @@ type MatchItem = {
|
||||
venue?: string;
|
||||
competition?: string;
|
||||
competitionName?: string;
|
||||
};
|
||||
}
|
||||
|
||||
const MatchesPage: React.FC = () => {
|
||||
const [clubName, setClubName] = useState<string>('');
|
||||
@@ -115,8 +117,21 @@ const MatchesPage: React.FC = () => {
|
||||
const getSentiment = (m: MatchItem): { label: 'Výhra'|'Remíza'|'Prohra'; colorScheme: 'green'|'blue'|'red' } | null => {
|
||||
const s = parseScore(m.score);
|
||||
if (!s) return null;
|
||||
const ourIsHome = isClubTeam(m.home);
|
||||
const ourIsAway = isClubTeam(m.away);
|
||||
|
||||
// First try ID-based matching (most reliable)
|
||||
let ourIsHome = false;
|
||||
let ourIsAway = false;
|
||||
if (clubId && m.home_id && m.away_id) {
|
||||
ourIsHome = m.home_id === clubId;
|
||||
ourIsAway = m.away_id === clubId;
|
||||
}
|
||||
|
||||
// Fallback to name matching if IDs not available or no match
|
||||
if (!ourIsHome && !ourIsAway) {
|
||||
ourIsHome = isClubTeam(m.home);
|
||||
ourIsAway = isClubTeam(m.away);
|
||||
}
|
||||
|
||||
if (!ourIsHome && !ourIsAway) return null;
|
||||
if (s.h === s.a) return { label: 'Remíza', colorScheme: 'blue' };
|
||||
const ourGoals = ourIsHome ? s.h : s.a;
|
||||
@@ -343,6 +358,8 @@ const MatchesPage: React.FC = () => {
|
||||
time,
|
||||
home: m.home,
|
||||
away: m.away,
|
||||
home_id: m.home_id,
|
||||
away_id: m.away_id,
|
||||
home_logo_url: getOverrideLogo(m.home, m.home_id, m.home_logo_url),
|
||||
away_logo_url: getOverrideLogo(m.away, m.away_id, m.away_logo_url),
|
||||
score: actualScore,
|
||||
|
||||
@@ -36,6 +36,12 @@ const MatchLinkBadge: React.FC<{ articleId: number }> = ({ articleId }) => {
|
||||
staleTime: 60_000,
|
||||
retry: false,
|
||||
});
|
||||
|
||||
// Show loading state while fetching
|
||||
if (linkQ.isLoading) {
|
||||
return <Badge colorScheme="gray">Načítání...</Badge>;
|
||||
}
|
||||
|
||||
const mid = (linkQ.data as any)?.external_match_id;
|
||||
if (!mid) return <Badge colorScheme="gray">Nepropojeno</Badge>;
|
||||
|
||||
@@ -651,38 +657,19 @@ const ArticlesAdminPage = () => {
|
||||
// Forward the payload as-is so new fields (youtube, gallery) are persisted
|
||||
createArticle(payload),
|
||||
onSuccess: async (created: any) => {
|
||||
try {
|
||||
// If a match was selected (from temp storage), link it now that we have an article ID
|
||||
const matchRaw = tempMatchLink || matchIdInput;
|
||||
const matchToLink = typeof matchRaw === 'string' ? matchRaw : String(matchRaw || '');
|
||||
const matchId = matchToLink.trim();
|
||||
if (matchId && created?.id) {
|
||||
await putArticleMatchLink(created.id, { external_match_id: matchId, title: (editing as any)?.title || '' });
|
||||
setLinkedMatchId(matchId);
|
||||
toast({
|
||||
title: 'Článek vytvořen a propojen se zápasem',
|
||||
description: `Match ID: ${matchId}`,
|
||||
status: 'success',
|
||||
duration: 3000,
|
||||
isClosable: true
|
||||
});
|
||||
} else {
|
||||
toast({
|
||||
title: 'Článek byl úspěšně vytvořen',
|
||||
status: 'success',
|
||||
duration: 3000,
|
||||
isClosable: true
|
||||
});
|
||||
}
|
||||
// Clear temporary storage after successful creation
|
||||
setTempMatchLink('');
|
||||
} finally {
|
||||
qc.invalidateQueries({ queryKey: ['admin-articles'] });
|
||||
qc.invalidateQueries({ queryKey: ['articles'] });
|
||||
qc.invalidateQueries({ queryKey: ['recentArticles'] });
|
||||
qc.invalidateQueries({ queryKey: ['article-match-link'] }); // Invalidate match links
|
||||
closeModal();
|
||||
}
|
||||
console.log('Article created successfully in mutation callback:', created);
|
||||
// Note: Match linking is now handled in onSubmit() to avoid race conditions
|
||||
// Clear temporary storage
|
||||
setTempMatchLink('');
|
||||
setMatchIdInput('');
|
||||
|
||||
// Invalidate queries to refresh the list
|
||||
qc.invalidateQueries({ queryKey: ['admin-articles'] });
|
||||
qc.invalidateQueries({ queryKey: ['articles'] });
|
||||
qc.invalidateQueries({ queryKey: ['recentArticles'] });
|
||||
qc.invalidateQueries({ queryKey: ['article-match-link'] }); // Invalidate match links
|
||||
|
||||
// Don't close modal here - let onSubmit handle it after match linking
|
||||
},
|
||||
onError: (e: any) => {
|
||||
console.error('Error creating article:', e);
|
||||
@@ -702,18 +689,16 @@ const ArticlesAdminPage = () => {
|
||||
updateArticle(id, payload),
|
||||
onSuccess: (_, variables) => {
|
||||
const articleId = variables.id;
|
||||
toast({
|
||||
title: 'Článek byl úspěšně aktualizován',
|
||||
status: 'success',
|
||||
duration: 3000,
|
||||
isClosable: true
|
||||
});
|
||||
console.log('Article updated successfully in mutation callback:', articleId);
|
||||
|
||||
// Invalidate queries to refresh the list
|
||||
qc.invalidateQueries({ queryKey: ['admin-articles'] });
|
||||
qc.invalidateQueries({ queryKey: ['articles'] });
|
||||
qc.invalidateQueries({ queryKey: ['recentArticles'] });
|
||||
qc.invalidateQueries({ queryKey: ['article-match-link', articleId] }); // Invalidate specific match link
|
||||
qc.invalidateQueries({ queryKey: ['article', `id:${articleId}`] }); // Invalidate article detail
|
||||
closeModal();
|
||||
|
||||
// Success toast and modal closing handled in onSubmit()
|
||||
},
|
||||
onError: (e: any) => {
|
||||
console.error('Error updating article:', e);
|
||||
@@ -864,7 +849,7 @@ const ArticlesAdminPage = () => {
|
||||
} catch { return undefined; }
|
||||
};
|
||||
|
||||
const onSubmit = async () => {
|
||||
const onSubmit = async (options: { keepOpen?: boolean } = {}) => {
|
||||
if (!editing) return;
|
||||
// Require category selection by name (kategorie je povinná)
|
||||
if (!String((editing as any)?.category_name || '').trim()) {
|
||||
@@ -932,10 +917,83 @@ const ArticlesAdminPage = () => {
|
||||
// Log the payload for debugging
|
||||
console.log('Saving article with payload:', JSON.stringify(payload, null, 2));
|
||||
|
||||
// Debug: Log match link state before submission
|
||||
console.log('Match link state before submit:', {
|
||||
tempMatchLink,
|
||||
matchIdInput,
|
||||
linkedMatchId,
|
||||
isNewArticle: !(editing as any)?.id
|
||||
});
|
||||
|
||||
if ((editing as any)?.id) {
|
||||
// Update existing article
|
||||
await updateMut.mutateAsync({ id: (editing as any).id, payload });
|
||||
|
||||
// Handle match linking for existing articles (update or delete)
|
||||
const matchRaw = matchIdInput || linkedMatchId;
|
||||
const matchId = String(matchRaw || '').trim();
|
||||
let matchLinked = false;
|
||||
if (matchId) {
|
||||
try {
|
||||
await putArticleMatchLink((editing as any).id, { external_match_id: matchId, title: editing.title || '' });
|
||||
console.log('Match link updated for existing article');
|
||||
matchLinked = true;
|
||||
} catch (err: any) {
|
||||
console.error('Failed to update match link:', err);
|
||||
}
|
||||
}
|
||||
|
||||
// Show success message
|
||||
toast({
|
||||
title: matchLinked ? 'Článek aktualizován a propojen se zápasem' : 'Článek byl úspěšně aktualizován',
|
||||
status: 'success',
|
||||
duration: 3000,
|
||||
isClosable: true
|
||||
});
|
||||
} else {
|
||||
await createMut.mutateAsync(payload);
|
||||
// Create new article
|
||||
const created = await createMut.mutateAsync(payload);
|
||||
|
||||
// Handle match linking for new articles
|
||||
const matchRaw = tempMatchLink || matchIdInput;
|
||||
const matchId = String(matchRaw || '').trim();
|
||||
if (matchId && created?.id) {
|
||||
console.log('Linking new article', created.id, 'with match', matchId);
|
||||
try {
|
||||
await putArticleMatchLink(created.id, { external_match_id: matchId, title: editing.title || '' });
|
||||
console.log('Match link created for new article');
|
||||
setLinkedMatchId(matchId);
|
||||
toast({
|
||||
title: 'Článek vytvořen a propojen se zápasem',
|
||||
description: `Match ID: ${matchId}`,
|
||||
status: 'success',
|
||||
duration: 3000,
|
||||
isClosable: true
|
||||
});
|
||||
} catch (err: any) {
|
||||
console.error('Failed to link match:', err);
|
||||
toast({
|
||||
title: 'Článek vytvořen, ale propojení se zápasem selhalo',
|
||||
description: err?.response?.data?.error || err?.message || 'Zkuste propojit zápas ručně',
|
||||
status: 'warning',
|
||||
duration: 5000,
|
||||
isClosable: true
|
||||
});
|
||||
}
|
||||
} else if (created?.id) {
|
||||
// No match to link, just show success
|
||||
toast({
|
||||
title: 'Článek byl úspěšně vytvořen',
|
||||
status: 'success',
|
||||
duration: 3000,
|
||||
isClosable: true
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
// Close modal after successful save (unless keepOpen is true)
|
||||
if (!options.keepOpen) {
|
||||
closeModal();
|
||||
}
|
||||
|
||||
} catch (error: any) {
|
||||
@@ -1781,13 +1839,30 @@ const ArticlesAdminPage = () => {
|
||||
qc.invalidateQueries({ queryKey: ['linked-polls'] });
|
||||
}} />
|
||||
) : (
|
||||
<Alert status="info" borderRadius="md">
|
||||
<Alert status="warning" borderRadius="md">
|
||||
<AlertIcon />
|
||||
<VStack align="start" spacing={1}>
|
||||
<Text fontWeight="semibold">Nejprve uložte článek</Text>
|
||||
<VStack align="start" spacing={2}>
|
||||
<Text fontWeight="semibold">Článek ještě není uložen</Text>
|
||||
<Text fontSize="sm">
|
||||
Pro vytvoření nebo propojení ankety nejprve uložte článek tlačítkem "Uložit" níže. Poté se vrátíte do úprav a budete moci přidat ankety.
|
||||
Pro propojení anket s článkem musíte nejprve článek uložit. Klikněte na "Uložit" níže - článek se uloží jako koncept a poté budete moci přidat ankety.
|
||||
</Text>
|
||||
<Button
|
||||
size="sm"
|
||||
colorScheme="blue"
|
||||
onClick={async () => {
|
||||
// Save article as draft first, keep modal open
|
||||
try {
|
||||
await onSubmit({ keepOpen: true });
|
||||
// Switch to poll tab after save
|
||||
setActiveTabIndex(5); // Poll tab is index 5
|
||||
} catch (error) {
|
||||
// Error is handled by onSubmit
|
||||
}
|
||||
}}
|
||||
isLoading={createMut.isLoading}
|
||||
>
|
||||
Uložit jako koncept a přidat ankety
|
||||
</Button>
|
||||
</VStack>
|
||||
</Alert>
|
||||
)}
|
||||
@@ -1798,7 +1873,7 @@ const ArticlesAdminPage = () => {
|
||||
</ModalBody>
|
||||
<ModalFooter>
|
||||
<Button variant="ghost" mr={3} onClick={closeModal}>Zrušit</Button>
|
||||
<Button colorScheme="blue" onClick={onSubmit} isLoading={createMut.isLoading || updateMut.isLoading}>Uložit</Button>
|
||||
<Button colorScheme="blue" onClick={() => onSubmit()} isLoading={createMut.isLoading || updateMut.isLoading}>Uložit</Button>
|
||||
</ModalFooter>
|
||||
</ModalContent>
|
||||
</Modal>
|
||||
|
||||
Reference in New Issue
Block a user