import crypto from "crypto"; import { Suggestion, VoteDirection } from "../../types/gen/types.gen"; import getStorage from "./storage"; /** Interní reprezentace návrhu uložená ve storage (včetně seznamů hlasujících). */ interface StoredSuggestion { id: string; author: string; title: string; description: string; createdAt: string; /** Loginy uživatelů hlasujících PRO návrh */ upvoters: string[]; /** Loginy uživatelů hlasujících PROTI návrhu */ downvoters: string[]; /** Příznak vyřešeného (zapracovaného) návrhu - nastavuje se pouze ručním zásahem do dat */ resolved?: boolean; } const storage = getStorage(); const STORAGE_KEY = 'suggestions'; /** Načte interní seznam návrhů ze storage. */ async function loadSuggestions(): Promise { return (await storage.getData(STORAGE_KEY)) ?? []; } /** * Převede interní návrh na DTO pro daného uživatele - skryje seznamy hlasujících * a doplní hlas přihlášeného uživatele a příznak vlastnictví. */ function toDto(suggestion: StoredSuggestion, login: string): Suggestion { let myVote: VoteDirection | undefined; if (suggestion.upvoters.includes(login)) { myVote = 'up'; } else if (suggestion.downvoters.includes(login)) { myVote = 'down'; } return { id: suggestion.id, author: suggestion.author, title: suggestion.title, description: suggestion.description, voteScore: suggestion.upvoters.length - suggestion.downvoters.length, myVote, isMine: suggestion.author === login, resolved: suggestion.resolved ?? false, }; } /** * Vrátí seznam návrhů jako DTO pro daného uživatele, seřazený sestupně dle skóre * (při shodě skóre stabilně dle data vytvoření vzestupně). * * @param login login přihlášeného uživatele */ export async function listSuggestions(login: string): Promise { const suggestions = await loadSuggestions(); return suggestions .map(s => toDto(s, login)) .sort((a, b) => b.voteScore - a.voteScore); } /** * Přidá nový návrh. Autorovi se automaticky nastaví hlas pro. * * @param login login autora * @param title název návrhu * @param description detailní popis návrhu * @returns aktualizovaný seznam návrhů jako DTO */ export async function addSuggestion(login: string, title: string, description: string): Promise { const trimmedTitle = title?.trim(); const trimmedDescription = description?.trim(); if (!trimmedTitle) { throw new Error('Název návrhu nesmí být prázdný'); } if (!trimmedDescription) { throw new Error('Popis návrhu nesmí být prázdný'); } const suggestions = await loadSuggestions(); suggestions.push({ id: crypto.randomUUID(), author: login, title: trimmedTitle, description: trimmedDescription, createdAt: new Date().toISOString(), // Autor automaticky hlasuje pro svůj návrh upvoters: [login], downvoters: [], }); await storage.setData(STORAGE_KEY, suggestions); return listSuggestions(login); } /** * Přepne hlas uživatele u návrhu. Klik na již aktivní směr hlas zruší, * opačný směr stávající hlas přepíše. * * @param login login hlasujícího uživatele * @param id identifikátor návrhu * @param direction směr hlasu, na který uživatel klikl * @returns aktualizovaný seznam návrhů jako DTO */ export async function voteSuggestion(login: string, id: string, direction: VoteDirection): Promise { const suggestions = await loadSuggestions(); const suggestion = suggestions.find(s => s.id === id); if (!suggestion) { throw new Error('Návrh nebyl nalezen'); } if (suggestion.resolved) { throw new Error('Pro vyřešený návrh nelze hlasovat'); } const hadUp = suggestion.upvoters.includes(login); const hadDown = suggestion.downvoters.includes(login); // Nejprve odebereme případný stávající hlas uživatele suggestion.upvoters = suggestion.upvoters.filter(l => l !== login); suggestion.downvoters = suggestion.downvoters.filter(l => l !== login); // Hlas přidáme pouze pokud uživatel neklikl na již aktivní směr (jinak ho jen zrušíme) if (direction === 'up' && !hadUp) { suggestion.upvoters.push(login); } else if (direction === 'down' && !hadDown) { suggestion.downvoters.push(login); } await storage.setData(STORAGE_KEY, suggestions); return listSuggestions(login); } /** * Smaže návrh včetně všech jeho hlasů. Smazat lze pouze vlastní návrh. * * @param login login uživatele požadujícího smazání * @param id identifikátor návrhu ke smazání * @returns aktualizovaný seznam návrhů jako DTO */ export async function deleteSuggestion(login: string, id: string): Promise { const suggestions = await loadSuggestions(); const suggestion = suggestions.find(s => s.id === id); if (!suggestion) { throw new Error('Návrh nebyl nalezen'); } if (suggestion.author !== login) { throw new Error('Smazat lze pouze vlastní návrh'); } const filtered = suggestions.filter(s => s.id !== id); await storage.setData(STORAGE_KEY, filtered); return listSuggestions(login); }