ssvc-rimsting-vote/src/components/admin/VoteOptionsManager.tsx
2025-03-02 21:09:39 +00:00

441 lines
18 KiB
TypeScript

'use client';
import { useState, useRef, useEffect } from 'react';
// Function to generate a random ID
function generateRandomId(existingIds: string[]): string {
const characters = 'abcdefghijklmnopqrstuvwxyz0123456789';
const length = 8;
let result: string;
do {
result = '';
for (let i = 0; i < length; i++) {
result += characters.charAt(Math.floor(Math.random() * characters.length));
}
} while (existingIds.includes(result));
return result;
}
interface VoteOptionsManagerProps {
voteOptions: { id: string; label: string }[];
onVoteOptionsChange: (newOptions: { id: string; label: string }[]) => void;
}
export default function VoteOptionsManager({ voteOptions, onVoteOptionsChange }: VoteOptionsManagerProps) {
const [showVoteOptions, setShowVoteOptions] = useState(false);
const [newOptionId, setNewOptionId] = useState('');
const [newOptionLabel, setNewOptionLabel] = useState('');
const [isLoading, setIsLoading] = useState(false);
const [error, setError] = useState<string | null>(null);
const [editingOptionId, setEditingOptionId] = useState<string | null>(null);
const [editedLabel, setEditedLabel] = useState('');
const editInputRef = useRef<HTMLInputElement>(null);
const labelInputRef = useRef<HTMLInputElement>(null);
// Generate a random ID when the component mounts or when the form is reset
useEffect(() => {
if (!newOptionId && voteOptions) {
const existingIds = voteOptions.map(option => option.id);
setNewOptionId(generateRandomId(existingIds));
}
}, [newOptionId, voteOptions]);
// Add a new vote option
const handleAddVoteOption = async () => {
if (!newOptionId || !newOptionLabel) {
setError('Bitte geben Sie sowohl eine ID als auch ein Label für die neue Option ein');
return;
}
// Check if ID already exists
if (voteOptions.some(option => option.id === newOptionId)) {
setError('Eine Option mit dieser ID existiert bereits');
return;
}
setIsLoading(true);
setError(null);
try {
// Add to local state
const updatedOptions = [...voteOptions, { id: newOptionId, label: newOptionLabel }];
// Save to server
const response = await fetch('/api/editable-text', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify({
id: 'vote-options',
content: updatedOptions
}),
});
if (!response.ok) {
throw new Error('Fehler beim Speichern der Abstimmungsoptionen');
}
// Update parent component state
onVoteOptionsChange(updatedOptions);
// Reset form and generate a new random ID
setNewOptionLabel('');
const existingIds = updatedOptions.map(option => option.id);
setNewOptionId(generateRandomId(existingIds));
} catch (err) {
setError(err instanceof Error ? err.message : 'Ein Fehler ist aufgetreten');
} finally {
setIsLoading(false);
}
};
// Start editing a vote option
const handleEditVoteOption = (option: { id: string; label: string }) => {
setEditingOptionId(option.id);
setEditedLabel(option.label);
// Focus the input field after it's rendered
setTimeout(() => {
if (editInputRef.current) {
editInputRef.current.focus();
}
}, 0);
};
// Save the edited vote option
const handleSaveVoteOption = async () => {
if (!editingOptionId || !editedLabel.trim()) {
setError('Das Label darf nicht leer sein');
return;
}
setIsLoading(true);
setError(null);
try {
// Update in local state
const updatedOptions = voteOptions.map(option =>
option.id === editingOptionId
? { ...option, label: editedLabel.trim() }
: option
);
// Save to server
const response = await fetch('/api/editable-text', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify({
id: 'vote-options',
content: updatedOptions
}),
});
if (!response.ok) {
throw new Error('Fehler beim Speichern der Abstimmungsoptionen');
}
// Update parent component state
onVoteOptionsChange(updatedOptions);
// Exit edit mode
setEditingOptionId(null);
} catch (err) {
setError(err instanceof Error ? err.message : 'Ein Fehler ist aufgetreten');
} finally {
setIsLoading(false);
}
};
// Cancel editing
const handleCancelEdit = () => {
setEditingOptionId(null);
};
// Move option up in the list
const handleMoveOptionUp = async (index: number) => {
if (index <= 0) return; // Already at the top
setIsLoading(true);
setError(null);
try {
// Create a new array with the option moved up
const updatedOptions = [...voteOptions];
[updatedOptions[index - 1], updatedOptions[index]] = [updatedOptions[index], updatedOptions[index - 1]];
// Save to server
const response = await fetch('/api/editable-text', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify({
id: 'vote-options',
content: updatedOptions
}),
});
if (!response.ok) {
throw new Error('Fehler beim Speichern der Abstimmungsoptionen');
}
// Update parent component state
onVoteOptionsChange(updatedOptions);
} catch (err) {
setError(err instanceof Error ? err.message : 'Ein Fehler ist aufgetreten');
} finally {
setIsLoading(false);
}
};
// Move option down in the list
const handleMoveOptionDown = async (index: number) => {
if (index >= voteOptions.length - 1) return; // Already at the bottom
setIsLoading(true);
setError(null);
try {
// Create a new array with the option moved down
const updatedOptions = [...voteOptions];
[updatedOptions[index], updatedOptions[index + 1]] = [updatedOptions[index + 1], updatedOptions[index]];
// Save to server
const response = await fetch('/api/editable-text', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify({
id: 'vote-options',
content: updatedOptions
}),
});
if (!response.ok) {
throw new Error('Fehler beim Speichern der Abstimmungsoptionen');
}
// Update parent component state
onVoteOptionsChange(updatedOptions);
} catch (err) {
setError(err instanceof Error ? err.message : 'Ein Fehler ist aufgetreten');
} finally {
setIsLoading(false);
}
};
// Remove a vote option
const handleRemoveVoteOption = async (optionId: string) => {
// Don't allow removing if there are less than 2 options
if (voteOptions.length <= 1) {
setError('Es muss mindestens eine Abstimmungsoption vorhanden sein');
return;
}
// Show confirmation dialog
const optionToRemove = voteOptions.find(option => option.id === optionId);
if (!confirm(`Sind Sie sicher, dass Sie die Option "${optionToRemove?.label}" entfernen möchten? Bestehende Abstimmungen mit dieser Option werden nicht gelöscht, aber in den Statistiken möglicherweise nicht mehr korrekt angezeigt.`)) {
return;
}
setIsLoading(true);
setError(null);
try {
// Remove from local state
const updatedOptions = voteOptions.filter(option => option.id !== optionId);
// Save to server
const response = await fetch('/api/editable-text', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify({
id: 'vote-options',
content: updatedOptions
}),
});
if (!response.ok) {
throw new Error('Fehler beim Speichern der Abstimmungsoptionen');
}
// Update parent component state
onVoteOptionsChange(updatedOptions);
} catch (err) {
setError(err instanceof Error ? err.message : 'Ein Fehler ist aufgetreten');
} finally {
setIsLoading(false);
}
};
return (
<div className="mb-6">
<div className="flex justify-between items-center mb-4">
<h2 className="text-xl font-bold text-[#0057a6]">Abstimmungsoptionen</h2>
<button
onClick={() => setShowVoteOptions(!showVoteOptions)}
className="text-sm text-[#0057a6] hover:underline"
>
{showVoteOptions ? 'Ausblenden' : 'Bearbeiten'}
</button>
</div>
{showVoteOptions && (
<div className="bg-[#e6f0fa] p-4 mb-4">
<h3 className="font-medium text-[#0057a6] mb-3">Aktuelle Optionen</h3>
<div className="space-y-2 mb-4">
{voteOptions.map((option, index) => (
<div key={option.id} className="flex items-center justify-between p-2 bg-white">
<div className="flex items-center space-x-2">
<div className="flex flex-col">
<button
onClick={() => handleMoveOptionUp(index)}
className="text-gray-600 hover:text-gray-800 disabled:opacity-30"
disabled={index === 0}
title="Nach oben verschieben"
>
</button>
<button
onClick={() => handleMoveOptionDown(index)}
className="text-gray-600 hover:text-gray-800 disabled:opacity-30"
disabled={index === voteOptions.length - 1}
title="Nach unten verschieben"
>
</button>
</div>
<div>
{editingOptionId === option.id ? (
<div className="flex items-center">
<span className="font-medium mr-2">{option.id}:</span>
<input
ref={editInputRef}
type="text"
value={editedLabel}
onChange={(e) => setEditedLabel(e.target.value)}
className="px-2 py-1 border border-gray-300 focus:outline-none focus:border-[#0057a6]"
onKeyDown={(e) => {
if (e.key === 'Enter') handleSaveVoteOption();
if (e.key === 'Escape') handleCancelEdit();
}}
/>
<button
onClick={handleSaveVoteOption}
className="ml-2 text-green-600 hover:text-green-800"
title="Speichern"
>
</button>
<button
onClick={handleCancelEdit}
className="ml-1 text-gray-600 hover:text-gray-800"
title="Abbrechen"
>
</button>
</div>
) : (
<div>
<span className="font-medium">{option.id}:</span> {option.label}
</div>
)}
</div>
</div>
<div className="flex space-x-2">
{editingOptionId !== option.id && (
<button
onClick={() => handleEditVoteOption(option)}
className="text-blue-600 hover:text-blue-800"
title="Option bearbeiten"
>
</button>
)}
<button
onClick={() => handleRemoveVoteOption(option.id)}
className="text-red-600 hover:text-red-800"
title="Option entfernen"
>
</button>
</div>
</div>
))}
</div>
<h3 className="font-medium text-[#0057a6] mb-2">Neue Option hinzufügen</h3>
<div className="space-y-3">
<div>
<label htmlFor="newOptionId" className="block text-sm font-medium text-gray-700 mb-1">
Option ID (z.B. &quot;yes&quot;, &quot;no&quot;)
</label>
<input
type="text"
id="newOptionId"
value={newOptionId}
onChange={(e) => setNewOptionId(e.target.value)}
className="w-full px-3 py-2 border border-gray-300 focus:outline-none focus:border-[#0057a6]"
placeholder="option-id"
/>
<p className="text-xs text-gray-500 mt-1">
Die ID wird intern verwendet und sollte kurz und eindeutig sein.
</p>
</div>
<div>
<label htmlFor="newOptionLabel" className="block text-sm font-medium text-gray-700 mb-1">
Anzeigename
</label>
<input
type="text"
id="newOptionLabel"
ref={labelInputRef}
value={newOptionLabel}
onChange={(e) => setNewOptionLabel(e.target.value)}
className="w-full px-3 py-2 border border-gray-300 focus:outline-none focus:border-[#0057a6]"
placeholder="Anzeigename der Option"
onFocus={() => {
// Generate a random ID if the field is empty
if (!newOptionId) {
const existingIds = voteOptions.map(option => option.id);
setNewOptionId(generateRandomId(existingIds));
}
}}
/>
<p className="text-xs text-gray-500 mt-1">
Der Anzeigename wird den Abstimmenden angezeigt.
</p>
</div>
{error && (
<div className="text-red-500 text-sm">{error}</div>
)}
<button
onClick={handleAddVoteOption}
disabled={isLoading}
className="ssvc-button w-full disabled:opacity-50"
>
{isLoading ? 'Wird hinzugefügt...' : 'Option hinzufügen'}
</button>
</div>
<div className="mt-4 text-sm text-gray-600">
<p className="font-medium">Hinweis:</p>
<p>Das Ändern der Abstimmungsoptionen wirkt sich nur auf neue Abstimmungen aus. Bestehende Abstimmungsdaten bleiben unverändert.</p>
</div>
</div>
)}
</div>
);
}