EventBuzz_Console/src/app/components/participants/ParticipantBulkDialogs.tsx

284 lines
13 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

import React, { useEffect, useState } from "react";
import { Check, X } from "lucide-react";
import type { Event } from "../../../services/api/types";
import type { ParticipantListRecord } from "../../../services/api/participantLists";
import { tagChipStyle } from "./participantTagUtils";
export type BulkDialogKind = null | "addTags" | "removeTags" | "assignEvents" | "newList" | "addToList";
export function ParticipantBulkDialogs({
bulkDialog,
bulkBusy,
selectedCount,
selectedTagValues,
onToggleTag,
availableTags,
removeTagOptions,
tagScope = "global",
newListName,
onNewListNameChange,
newListDesc,
onNewListDescChange,
eventsPick,
eventPickIds,
onToggleEventPick,
fixedLists,
addToListId,
onAddToListIdChange,
onClose,
onApplyTags,
onAssignEvents,
onCreateList,
onAddToList,
}: {
bulkDialog: BulkDialogKind;
bulkBusy: boolean;
selectedCount: number;
selectedTagValues: string[];
onToggleTag: (tag: string) => void;
availableTags: string[];
removeTagOptions: string[];
/** Event page = tags stored on this event membership; global = org-wide root tags. */
tagScope?: "event" | "global";
newListName: string;
onNewListNameChange: (v: string) => void;
newListDesc: string;
onNewListDescChange: (v: string) => void;
eventsPick: Event[];
eventPickIds: Set<string>;
onToggleEventPick: (id: string) => void;
fixedLists: ParticipantListRecord[];
addToListId: string;
onAddToListIdChange: (id: string) => void;
onClose: () => void;
onApplyTags: (action: "add_tags" | "remove_tags", overrideTags?: string[]) => void;
onAssignEvents: () => void;
onCreateList: () => void;
onAddToList: () => void;
}) {
const [manualTagInput, setManualTagInput] = useState("");
useEffect(() => {
if (bulkDialog !== "addTags" && bulkDialog !== "removeTags") setManualTagInput("");
}, [bulkDialog]);
const addManualTags = () => {
const tokens = manualTagInput
.split(/[\n,;]+/)
.map((token) => token.trim())
.filter(Boolean);
if (!tokens.length) return [];
tokens.forEach((token) => {
if (!selectedTagValues.includes(token)) onToggleTag(token);
});
setManualTagInput("");
return Array.from(new Set([...selectedTagValues, ...tokens]));
};
const overlay = (children: React.ReactNode) => (
<div
role="dialog"
aria-modal="true"
style={{ position: "fixed", inset: 0, background: "rgba(0,0,0,0.45)", zIndex: 205, display: "flex", alignItems: "center", justifyContent: "center" }}
onClick={() => !bulkBusy && onClose()}
>
<div
style={{ background: "var(--card)", borderRadius: "var(--radius-card)", padding: "20px", width: "min(440px, 92vw)", maxHeight: "80vh", overflow: "auto", boxShadow: "var(--shadow-xl)" }}
onClick={(e) => e.stopPropagation()}
>
{children}
</div>
</div>
);
const footer = (primary: React.ReactNode) => (
<div style={{ display: "flex", justifyContent: "flex-end", gap: "8px", marginTop: "14px" }}>
<button type="button" disabled={bulkBusy} onClick={onClose} style={{ padding: "7px 14px", borderRadius: "var(--radius-button)", border: "1px solid var(--border)", background: "var(--card)", cursor: "pointer", fontSize: "var(--text-sm)" }}>Cancel</button>
{primary}
</div>
);
if (bulkDialog === "addTags" || bulkDialog === "removeTags") {
const tagOptions = bulkDialog === "removeTags" ? removeTagOptions : availableTags;
const emptyMessage =
bulkDialog === "removeTags"
? "No tags are currently assigned to the selected participants."
: tagScope === "event"
? "No tags are available yet for this event."
: "No tags are available yet for this organisation.";
const scopeHint =
tagScope === "event"
? bulkDialog === "addTags"
? "Tags apply to this event only (also shown on the person’s global record, not on other events)."
: "Removes tags from this event only. Organisation-wide tags of the same name stay visible here."
: bulkDialog === "addTags"
? "Tags apply organisation-wide and appear on every event for these people."
: "Removes the tag from the person entirely (organisation-wide and any event-specific copies).";
return overlay(
<>
<h3 style={{ fontFamily: "var(--font-inter)", fontSize: "var(--text-base)", margin: "0 0 8px" }}>{bulkDialog === "addTags" ? "Add tags" : "Remove tags"}</h3>
<p style={{ fontSize: "var(--text-xs)", color: "var(--muted)", margin: "0 0 8px", fontFamily: "var(--font-noto-sans)" }}>Type a tag or select one below to apply to {selectedCount} selected participant(s).</p>
<p style={{ fontSize: "var(--text-xs)", color: "var(--muted)", margin: "0 0 12px", fontFamily: "var(--font-noto-sans)" }}>{scopeHint}</p>
<input
value={manualTagInput}
onChange={(e) => setManualTagInput(e.target.value)}
onKeyDown={(e) => {
if (e.key === "Enter" || e.key === "," || e.key === ";") {
e.preventDefault();
addManualTags();
}
}}
placeholder={bulkDialog === "addTags" ? "Type a tag and press Enter" : "Type a tag and press Enter"}
style={{ width: "100%", padding: "8px 12px", borderRadius: "var(--radius-button)", border: "1px solid var(--border)", fontSize: "var(--text-sm)", boxSizing: "border-box", marginBottom: "10px" }}
/>
{selectedTagValues.length > 0 && (
<div style={{ marginBottom: "10px" }}>
<div style={{ fontSize: "11px", fontFamily: "var(--font-noto-sans)", fontWeight: 600, color: "var(--muted)", textTransform: "uppercase", letterSpacing: "0.04em", marginBottom: "6px" }}>Selected</div>
<div style={{ display: "flex", flexWrap: "wrap", gap: "8px" }}>
{selectedTagValues.map((tag) => {
const tc = tagChipStyle(tag);
return (
<button
key={tag}
type="button"
onClick={() => onToggleTag(tag)}
style={{
display: "inline-flex",
alignItems: "center",
gap: "6px",
padding: "6px 10px",
borderRadius: "999px",
border: "1px solid var(--border)",
background: tc.bg,
color: tc.color,
cursor: "pointer",
fontSize: "var(--text-sm)",
fontFamily: "var(--font-noto-sans)",
}}
>
<X size={12} />
{tag}
</button>
);
})}
</div>
</div>
)}
{tagOptions.length === 0 ? (
<p style={{ fontSize: "var(--text-sm)", color: "var(--muted)", margin: "0 0 8px" }}>{emptyMessage}</p>
) : (
<div>
<div style={{ fontSize: "11px", fontFamily: "var(--font-noto-sans)", fontWeight: 600, color: "var(--muted)", textTransform: "uppercase", letterSpacing: "0.04em", marginBottom: "6px" }}></div>
<div style={{ display: "flex", flexWrap: "wrap", gap: "8px", marginBottom: "10px" }}>
{tagOptions
.filter(tag => !selectedTagValues.includes(tag))
.map((tag) => {
const active = selectedTagValues.includes(tag);
const tc = tagChipStyle(tag);
return (
<button
key={tag}
type="button"
onClick={() => onToggleTag(tag)}
style={{
display: "inline-flex",
alignItems: "center",
gap: "6px",
padding: "6px 10px",
borderRadius: "999px",
border: `1px solid ${active ? "var(--primary)" : "var(--border)"}`,
background: active ? tc.bg : "var(--card)",
color: active ? tc.color : "var(--card-foreground)",
cursor: "pointer",
fontSize: "var(--text-sm)",
fontFamily: "var(--font-noto-sans)",
boxShadow: active ? `inset 0 0 0 1px ${tc.color}25` : "none",
}}
>
{active ? <Check size={12} /> : null}
{tag}
</button>
);
})}
</div>
</div>
)}
{footer(
<button
type="button"
disabled={bulkBusy || (selectedTagValues.length === 0 && !manualTagInput.trim())}
onClick={() => {
const manualTokens = addManualTags();
const effectiveTags = manualTokens.length ? manualTokens : selectedTagValues;
onApplyTags(bulkDialog === "addTags" ? "add_tags" : "remove_tags", effectiveTags);
}}
style={{ padding: "7px 14px", borderRadius: "var(--radius-button)", border: "none", background: "var(--primary)", color: "var(--primary-foreground)", cursor: "pointer", fontSize: "var(--text-sm)" }}
>
Apply
</button>
)}
</>
);
}
if (bulkDialog === "assignEvents") {
return overlay(
<>
<h3 style={{ fontFamily: "var(--font-inter)", fontSize: "var(--text-base)", margin: "0 0 8px" }}>Assign to event(s)</h3>
<p style={{ fontSize: "var(--text-xs)", color: "var(--muted)", margin: "0 0 12px", fontFamily: "var(--font-noto-sans)" }}>{selectedCount} people · adds pending membership where missing.</p>
<div style={{ maxHeight: "240px", overflowY: "auto", border: "1px solid var(--border)", borderRadius: "var(--radius-button)", padding: "8px" }}>
{eventsPick.map((ev) => (
<label key={ev._id} style={{ display: "flex", alignItems: "center", gap: "8px", padding: "6px 4px", cursor: "pointer", fontSize: "var(--text-sm)", fontFamily: "var(--font-noto-sans)" }}>
<input type="checkbox" checked={eventPickIds.has(ev._id)} onChange={() => onToggleEventPick(ev._id)} />
{ev.name}
</label>
))}
</div>
{footer(
<button type="button" disabled={bulkBusy || eventPickIds.size === 0} onClick={onAssignEvents} style={{ padding: "7px 14px", borderRadius: "var(--radius-button)", border: "none", background: "var(--primary)", color: "var(--primary-foreground)", cursor: "pointer", fontSize: "var(--text-sm)" }}>Assign</button>
)}
</>
);
}
if (bulkDialog === "newList") {
return overlay(
<>
<h3 style={{ fontFamily: "var(--font-inter)", fontSize: "var(--text-base)", margin: "0 0 8px" }}>New custom list</h3>
<p style={{ fontSize: "var(--text-xs)", color: "var(--muted)", margin: "0 0 12px", fontFamily: "var(--font-noto-sans)" }}>Fixed list with {selectedCount} selected people.</p>
<input value={newListName} onChange={(e) => onNewListNameChange(e.target.value)} placeholder="List name" style={{ width: "100%", padding: "8px 12px", borderRadius: "var(--radius-button)", border: "1px solid var(--border)", marginBottom: "8px", boxSizing: "border-box", fontSize: "var(--text-sm)" }} />
<textarea value={newListDesc} onChange={(e) => onNewListDescChange(e.target.value)} placeholder="Description (optional)" rows={3} style={{ width: "100%", padding: "8px 12px", borderRadius: "var(--radius-button)", border: "1px solid var(--border)", boxSizing: "border-box", fontSize: "var(--text-sm)", resize: "vertical" }} />
{footer(
<button type="button" disabled={bulkBusy || !newListName.trim()} onClick={onCreateList} style={{ padding: "7px 14px", borderRadius: "var(--radius-button)", border: "none", background: "var(--primary)", color: "var(--primary-foreground)", cursor: "pointer", fontSize: "var(--text-sm)" }}>Create</button>
)}
</>
);
}
if (bulkDialog === "addToList") {
return overlay(
<>
<h3 style={{ fontFamily: "var(--font-inter)", fontSize: "var(--text-base)", margin: "0 0 8px" }}>Add to existing list</h3>
<p style={{ fontSize: "var(--text-xs)", color: "var(--muted)", margin: "0 0 12px", fontFamily: "var(--font-noto-sans)" }}>Append {selectedCount} people to a fixed list.</p>
{fixedLists.length === 0 ? (
<p style={{ fontSize: "var(--text-sm)", color: "var(--muted)" }}>No fixed lists yet. Create one first.</p>
) : (
<select value={addToListId} onChange={(e) => onAddToListIdChange(e.target.value)} style={{ width: "100%", padding: "8px 12px", borderRadius: "var(--radius-button)", border: "1px solid var(--border)", fontSize: "var(--text-sm)" }}>
{fixedLists.map((L) => (
<option key={L._id} value={L._id}>{L.name}</option>
))}
</select>
)}
{footer(
<button type="button" disabled={bulkBusy || fixedLists.length === 0 || !addToListId} onClick={onAddToList} style={{ padding: "7px 14px", borderRadius: "var(--radius-button)", border: "none", background: "var(--primary)", color: "var(--primary-foreground)", cursor: "pointer", fontSize: "var(--text-sm)" }}>Add</button>
)}
</>
);
}
return null;
}