diff --git a/client/src/components/modals/SuggestionDetailModal.tsx b/client/src/components/modals/SuggestionDetailModal.tsx
index 564d98a..57c4100 100644
--- a/client/src/components/modals/SuggestionDetailModal.tsx
+++ b/client/src/components/modals/SuggestionDetailModal.tsx
@@ -16,6 +16,7 @@ export default function SuggestionDetailModal({ suggestion, onClose }: Readonly<
Navrhovatel: {suggestion?.author} · Hlasy: {suggestion?.voteScore}
+ {suggestion?.resolved && <> · Vyřešeno>}
{suggestion?.description}
diff --git a/client/src/pages/SuggestionsPage.scss b/client/src/pages/SuggestionsPage.scss
index 100e707..a9675c8 100644
--- a/client/src/pages/SuggestionsPage.scss
+++ b/client/src/pages/SuggestionsPage.scss
@@ -36,6 +36,29 @@
margin-top: 32px;
}
+ .resolved-section {
+ width: 100%;
+ max-width: 900px;
+ margin-top: 48px;
+
+ h2 {
+ font-size: 1.4rem;
+ font-weight: 700;
+ color: var(--luncher-text);
+ margin-bottom: 8px;
+ }
+ }
+
+ .suggestions-table.resolved {
+ th {
+ background: var(--luncher-text-secondary);
+ }
+
+ td.col-score {
+ color: var(--luncher-text-secondary);
+ }
+ }
+
.suggestions-table {
width: 100%;
max-width: 900px;
diff --git a/client/src/pages/SuggestionsPage.tsx b/client/src/pages/SuggestionsPage.tsx
index d4e13e1..7dbd44e 100644
--- a/client/src/pages/SuggestionsPage.tsx
+++ b/client/src/pages/SuggestionsPage.tsx
@@ -60,6 +60,54 @@ export default function SuggestionsPage() {
}
};
+ // Vykreslí jeden řádek tabulky. Vyřešené návrhy jsou read-only (bez hlasování),
+ // ale autor je stále může smazat.
+ const renderRow = (suggestion: Suggestion) => (
+ {suggestion.description}}
+ >
+ setDetail(suggestion)}>
+ | {suggestion.author} |
+ {suggestion.title} |
+ {suggestion.voteScore} |
+ e.stopPropagation()}>
+ {!suggestion.resolved && (
+ <>
+
+
+ >
+ )}
+ {suggestion.isMine && (
+
+ )}
+ |
+
+
+ );
+
if (!auth?.login) {
return ;
}
@@ -68,6 +116,9 @@ export default function SuggestionsPage() {
return ;
}
+ const activeSuggestions = suggestions.filter(s => !s.resolved);
+ const resolvedSuggestions = suggestions.filter(s => s.resolved);
+
return (
<>
@@ -86,59 +137,45 @@ export default function SuggestionsPage() {
{suggestions.length === 0 ? (
Zatím nebyly přidány žádné návrhy. Buďte první!
) : (
-
-
-
- | Navrhovatel |
- Název |
- Hlasy |
- Akce |
-
-
-
- {suggestions.map(suggestion => (
- {suggestion.description}}
- >
- setDetail(suggestion)}>
- | {suggestion.author} |
- {suggestion.title} |
- {suggestion.voteScore} |
- e.stopPropagation()}>
-
-
- {suggestion.isMine && (
-
- )}
- |
+ <>
+ {activeSuggestions.length > 0 && (
+
+
+
+ | Navrhovatel |
+ Název |
+ Hlasy |
+ Akce |
-
- ))}
-
-
+
+
+ {activeSuggestions.map(renderRow)}
+
+
+ )}
+
+ {resolvedSuggestions.length > 0 && (
+
+
Vyřešené návrhy
+
+ Tyto návrhy již byly zapracovány. Nelze pro ně hlasovat, autor je však může odstranit.
+
+
+
+
+ | Navrhovatel |
+ Název |
+ Hlasy |
+ Akce |
+
+
+
+ {resolvedSuggestions.map(renderRow)}
+
+
+
+ )}
+ >
)}
diff --git a/server/src/suggestions.ts b/server/src/suggestions.ts
index 818cf1b..a92ae86 100644
--- a/server/src/suggestions.ts
+++ b/server/src/suggestions.ts
@@ -13,6 +13,8 @@ interface StoredSuggestion {
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();
@@ -42,6 +44,7 @@ function toDto(suggestion: StoredSuggestion, login: string): Suggestion {
voteScore: suggestion.upvoters.length - suggestion.downvoters.length,
myVote,
isMine: suggestion.author === login,
+ resolved: suggestion.resolved ?? false,
};
}
@@ -105,6 +108,9 @@ export async function voteSuggestion(login: string, id: string, direction: VoteD
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
diff --git a/server/src/tests/suggestions.test.ts b/server/src/tests/suggestions.test.ts
index a5fec87..75ad0ca 100644
--- a/server/src/tests/suggestions.test.ts
+++ b/server/src/tests/suggestions.test.ts
@@ -1,6 +1,15 @@
import { resetMemoryStorage } from '../storage/memory';
+import getStorage from '../storage';
import { addSuggestion, deleteSuggestion, listSuggestions, voteSuggestion } from '../suggestions';
+/** Ručně označí návrh jako vyřešený - simuluje zásah do dat (Redis/JSON). */
+async function markResolved(id: string) {
+ const storage = getStorage();
+ const data = await storage.getData('suggestions');
+ data!.find(s => s.id === id).resolved = true;
+ await storage.setData('suggestions', data);
+}
+
const AUTHOR = 'tomas';
const VOTER = 'petr';
const OTHER = 'jana';
@@ -106,6 +115,36 @@ describe('voteSuggestion', () => {
});
});
+describe('vyřešené návrhy', () => {
+ test('listSuggestions vrací příznak resolved', async () => {
+ const id = await createSuggestion();
+ expect((await listSuggestions(AUTHOR))[0].resolved).toBe(false);
+ await markResolved(id);
+ expect((await listSuggestions(AUTHOR))[0].resolved).toBe(true);
+ });
+
+ test('pro vyřešený návrh nelze hlasovat', async () => {
+ const id = await createSuggestion();
+ await markResolved(id);
+ await expect(voteSuggestion(VOTER, id, 'up')).rejects.toThrow();
+ // skóre zůstává nezměněné (jen autorův hlas)
+ expect((await listSuggestions(VOTER))[0].voteScore).toBe(1);
+ });
+
+ test('autor může vyřešený návrh stále smazat', async () => {
+ const id = await createSuggestion();
+ await markResolved(id);
+ const list = await deleteSuggestion(AUTHOR, id);
+ expect(list).toHaveLength(0);
+ });
+
+ test('cizí uživatel nemůže smazat ani vyřešený návrh', async () => {
+ const id = await createSuggestion();
+ await markResolved(id);
+ await expect(deleteSuggestion(VOTER, id)).rejects.toThrow();
+ });
+});
+
describe('deleteSuggestion', () => {
test('autor smaže svůj návrh včetně hlasů', async () => {
const id = await createSuggestion();
diff --git a/types/schemas/_index.yml b/types/schemas/_index.yml
index 518e76f..eabd44a 100644
--- a/types/schemas/_index.yml
+++ b/types/schemas/_index.yml
@@ -294,6 +294,7 @@ Suggestion:
- description
- voteScore
- isMine
+ - resolved
properties:
id:
type: string
@@ -316,6 +317,12 @@ Suggestion:
isMine:
type: boolean
description: True, pokud návrh vytvořil přihlášený uživatel
+ resolved:
+ type: boolean
+ description: >-
+ True, pokud byl návrh označen jako vyřešený (zapracovaný). Vyřešené
+ návrhy jsou read-only - nelze pro ně hlasovat. Nastavuje se pouze
+ ručním zásahem do dat.
# --- EASTER EGGS ---
EasterEgg: