|
|
|
|
@@ -1,5 +1,5 @@
|
|
|
|
|
import { useState, useCallback } from 'react';
|
|
|
|
|
import { useAuth } from '../../contexts/AuthContext';
|
|
|
|
|
import { useState, useCallback, useEffect } from 'react';
|
|
|
|
|
import { useAuth, getToken } from '../../contexts/AuthContext';
|
|
|
|
|
import { formatDateTime } from '../../utils/format';
|
|
|
|
|
import type { EventPost, EventType, Poll, UserRole } from '../../types';
|
|
|
|
|
|
|
|
|
|
@@ -244,10 +244,27 @@ function EventCard({
|
|
|
|
|
|
|
|
|
|
// ── Main Component ─────────────────────────────────────────────────────────────
|
|
|
|
|
|
|
|
|
|
function apiFetch<T>(path: string, options: RequestInit = {}): Promise<T> {
|
|
|
|
|
const token = getToken();
|
|
|
|
|
return fetch(`/api${path}`, {
|
|
|
|
|
...options,
|
|
|
|
|
headers: {
|
|
|
|
|
'Content-Type': 'application/json',
|
|
|
|
|
...(token ? { Authorization: `Bearer ${token}` } : {}),
|
|
|
|
|
...(options.headers as Record<string, string> ?? {}),
|
|
|
|
|
},
|
|
|
|
|
}).then(async (res) => {
|
|
|
|
|
if (!res.ok) {
|
|
|
|
|
const body = await res.json().catch(() => ({}));
|
|
|
|
|
throw new Error(body.error ?? `Request failed (${res.status})`);
|
|
|
|
|
}
|
|
|
|
|
return res.json() as Promise<T>;
|
|
|
|
|
});
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
export default function IntranetEvents() {
|
|
|
|
|
const { user } = useAuth();
|
|
|
|
|
const [events, setEvents] = useState<EventPost[]>([]);
|
|
|
|
|
const [polls, setPolls] = useState<Poll[]>([]);
|
|
|
|
|
const [showCreateForm, setShowCreateForm] = useState(false);
|
|
|
|
|
|
|
|
|
|
// Form state
|
|
|
|
|
@@ -261,124 +278,78 @@ export default function IntranetEvents() {
|
|
|
|
|
const [error, setError] = useState('');
|
|
|
|
|
const [posting, setPosting] = useState(false);
|
|
|
|
|
|
|
|
|
|
useEffect(() => {
|
|
|
|
|
apiFetch<{ events: EventPost[] }>('/events?limit=50')
|
|
|
|
|
.then((res) => setEvents(res.events))
|
|
|
|
|
.catch(() => setEvents([]));
|
|
|
|
|
}, []);
|
|
|
|
|
|
|
|
|
|
const handleVote = useCallback(
|
|
|
|
|
(pollId: string, optionId: string) => {
|
|
|
|
|
if (!user) return;
|
|
|
|
|
|
|
|
|
|
setPolls((prevPolls) =>
|
|
|
|
|
prevPolls.map((poll) => {
|
|
|
|
|
if (poll.id !== pollId) return poll;
|
|
|
|
|
|
|
|
|
|
const hasVotedForOption = poll.options.some((opt) =>
|
|
|
|
|
opt.votedUserIds.includes(user.id)
|
|
|
|
|
);
|
|
|
|
|
|
|
|
|
|
return {
|
|
|
|
|
...poll,
|
|
|
|
|
options: poll.options.map((opt) => {
|
|
|
|
|
if (opt.id === optionId) {
|
|
|
|
|
// Add vote to this option
|
|
|
|
|
return {
|
|
|
|
|
...opt,
|
|
|
|
|
votes: opt.votedUserIds.includes(user.id) ? opt.votes : opt.votes + 1,
|
|
|
|
|
votedUserIds: opt.votedUserIds.includes(user.id)
|
|
|
|
|
? opt.votedUserIds
|
|
|
|
|
: [...opt.votedUserIds, user.id],
|
|
|
|
|
};
|
|
|
|
|
} else if (!poll.allowMultipleVotes && hasVotedForOption) {
|
|
|
|
|
// Remove vote from other options if single vote
|
|
|
|
|
return {
|
|
|
|
|
...opt,
|
|
|
|
|
votes: opt.votedUserIds.includes(user.id) ? opt.votes - 1 : opt.votes,
|
|
|
|
|
votedUserIds: opt.votedUserIds.filter((id) => id !== user.id),
|
|
|
|
|
};
|
|
|
|
|
}
|
|
|
|
|
return opt;
|
|
|
|
|
}),
|
|
|
|
|
};
|
|
|
|
|
})
|
|
|
|
|
);
|
|
|
|
|
async (pollId: string, optionId: string) => {
|
|
|
|
|
const event = events.find((e) => e.pollId === pollId || e.poll?.id === pollId);
|
|
|
|
|
if (!event || !user) return;
|
|
|
|
|
try {
|
|
|
|
|
const updated = await apiFetch<EventPost>(`/events/${event.id}/vote`, {
|
|
|
|
|
method: 'POST',
|
|
|
|
|
body: JSON.stringify({ optionIds: [optionId] }),
|
|
|
|
|
});
|
|
|
|
|
setEvents((prev) => prev.map((e) => (e.id === updated.id ? updated : e)));
|
|
|
|
|
} catch {
|
|
|
|
|
// silently ignore
|
|
|
|
|
}
|
|
|
|
|
},
|
|
|
|
|
[user]
|
|
|
|
|
[events, user]
|
|
|
|
|
);
|
|
|
|
|
|
|
|
|
|
const handleSubmit = useCallback(async () => {
|
|
|
|
|
// Validation
|
|
|
|
|
if (!title.trim()) {
|
|
|
|
|
setError('Title is required.');
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
if (!content.trim()) {
|
|
|
|
|
setError('Content is required.');
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
if (!title.trim()) { setError('Title is required.'); return; }
|
|
|
|
|
if (!content.trim()) { setError('Content is required.'); return; }
|
|
|
|
|
if (createPoll) {
|
|
|
|
|
if (!pollQuestion.trim()) {
|
|
|
|
|
setError('Poll question is required.');
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
if (!pollQuestion.trim()) { setError('Poll question is required.'); return; }
|
|
|
|
|
const validOptions = pollOptions.filter((opt) => opt.trim());
|
|
|
|
|
if (validOptions.length < 2) {
|
|
|
|
|
setError('Poll must have at least 2 options.');
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
if (validOptions.length < 2) { setError('Poll must have at least 2 options.'); return; }
|
|
|
|
|
}
|
|
|
|
|
if (!user) return;
|
|
|
|
|
|
|
|
|
|
setError('');
|
|
|
|
|
setPosting(true);
|
|
|
|
|
await new Promise((r) => setTimeout(r, 300));
|
|
|
|
|
|
|
|
|
|
const newEventId = `evt${Date.now()}`;
|
|
|
|
|
let newPollId: string | undefined;
|
|
|
|
|
|
|
|
|
|
// Create poll if needed
|
|
|
|
|
if (createPoll) {
|
|
|
|
|
newPollId = `poll${Date.now()}`;
|
|
|
|
|
const validOptions = pollOptions.filter((opt) => opt.trim());
|
|
|
|
|
const newPoll: Poll = {
|
|
|
|
|
id: newPollId,
|
|
|
|
|
eventId: newEventId,
|
|
|
|
|
question: pollQuestion.trim(),
|
|
|
|
|
options: validOptions.map((opt, idx) => ({
|
|
|
|
|
id: `opt${Date.now()}_${idx}`,
|
|
|
|
|
text: opt.trim(),
|
|
|
|
|
votes: 0,
|
|
|
|
|
votedUserIds: [],
|
|
|
|
|
})),
|
|
|
|
|
isActive: true,
|
|
|
|
|
allowMultipleVotes: false,
|
|
|
|
|
createdAt: new Date().toISOString(),
|
|
|
|
|
try {
|
|
|
|
|
const body: Record<string, unknown> = {
|
|
|
|
|
type: createPoll ? 'poll' : eventType,
|
|
|
|
|
title: title.trim(),
|
|
|
|
|
content: content.trim(),
|
|
|
|
|
isPublic,
|
|
|
|
|
};
|
|
|
|
|
setPolls((prev) => [newPoll, ...prev]);
|
|
|
|
|
|
|
|
|
|
if (createPoll) {
|
|
|
|
|
body.poll = {
|
|
|
|
|
question: pollQuestion.trim(),
|
|
|
|
|
options: pollOptions.filter((o) => o.trim()).map((o) => ({ text: o.trim() })),
|
|
|
|
|
};
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
const created = await apiFetch<EventPost>('/events', {
|
|
|
|
|
method: 'POST',
|
|
|
|
|
body: JSON.stringify(body),
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
setEvents((prev) => [created, ...prev]);
|
|
|
|
|
|
|
|
|
|
// Reset form
|
|
|
|
|
setTitle('');
|
|
|
|
|
setContent('');
|
|
|
|
|
setEventType('announcement');
|
|
|
|
|
setIsPublic(true);
|
|
|
|
|
setCreatePoll(false);
|
|
|
|
|
setPollQuestion('');
|
|
|
|
|
setPollOptions(['', '']);
|
|
|
|
|
setShowCreateForm(false);
|
|
|
|
|
} catch (err) {
|
|
|
|
|
setError(err instanceof Error ? err.message : 'Failed to create event.');
|
|
|
|
|
} finally {
|
|
|
|
|
setPosting(false);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Create event
|
|
|
|
|
const newEvent: EventPost = {
|
|
|
|
|
id: newEventId,
|
|
|
|
|
type: createPoll ? 'poll' : eventType,
|
|
|
|
|
title: title.trim(),
|
|
|
|
|
content: content.trim(),
|
|
|
|
|
authorId: user.id,
|
|
|
|
|
authorName: user.username,
|
|
|
|
|
authorRole: user.role,
|
|
|
|
|
createdAt: new Date().toISOString(),
|
|
|
|
|
isPublic,
|
|
|
|
|
pollId: newPollId,
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
setEvents((prev) => [newEvent, ...prev]);
|
|
|
|
|
|
|
|
|
|
// Reset form
|
|
|
|
|
setTitle('');
|
|
|
|
|
setContent('');
|
|
|
|
|
setEventType('announcement');
|
|
|
|
|
setIsPublic(true);
|
|
|
|
|
setCreatePoll(false);
|
|
|
|
|
setPollQuestion('');
|
|
|
|
|
setPollOptions(['', '']);
|
|
|
|
|
setPosting(false);
|
|
|
|
|
setShowCreateForm(false);
|
|
|
|
|
}, [title, content, eventType, isPublic, createPoll, pollQuestion, pollOptions, user]);
|
|
|
|
|
|
|
|
|
|
return (
|
|
|
|
|
@@ -711,10 +682,9 @@ export default function IntranetEvents() {
|
|
|
|
|
|
|
|
|
|
{/* Events List */}
|
|
|
|
|
<div style={{ display: 'flex', flexDirection: 'column', gap: '1rem' }}>
|
|
|
|
|
{events.map((event) => {
|
|
|
|
|
const poll = event.pollId ? polls.find((p) => p.id === event.pollId) : undefined;
|
|
|
|
|
return <EventCard key={event.id} event={event} poll={poll} onVote={handleVote} />;
|
|
|
|
|
})}
|
|
|
|
|
{events.map((event) => (
|
|
|
|
|
<EventCard key={event.id} event={event} poll={event.poll ?? undefined} onVote={handleVote} />
|
|
|
|
|
))}
|
|
|
|
|
</div>
|
|
|
|
|
</div>
|
|
|
|
|
);
|
|
|
|
|
|