import { ISSUE_STATUS_MAX_LENGTH, type OrganisationMemberResponse, type OrganisationResponse, } from "@issue/shared"; import { ChevronDown, ChevronUp, Plus, X } from "lucide-react"; import type { ReactNode } from "react"; import { useCallback, useEffect, useState } from "react"; import { AddMemberDialog } from "@/components/add-member-dialog"; import { OrganisationSelect } from "@/components/organisation-select"; import { useAuthenticatedSession } from "@/components/session-provider"; import SmallUserDisplay from "@/components/small-user-display"; import StatusTag from "@/components/status-tag"; import { Button } from "@/components/ui/button"; import { ConfirmDialog } from "@/components/ui/confirm-dialog"; import { Dialog, DialogContent, DialogHeader, DialogTitle, DialogTrigger } from "@/components/ui/dialog"; import { Input } from "@/components/ui/input"; import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "@/components/ui/select"; import { Tabs, TabsContent, TabsList, TabsTrigger } from "@/components/ui/tabs"; import { issue, organisation } from "@/lib/server"; function OrganisationsDialog({ trigger, organisations, selectedOrganisation, setSelectedOrganisation, refetchOrganisations, }: { trigger?: ReactNode; organisations: OrganisationResponse[]; selectedOrganisation: OrganisationResponse | null; setSelectedOrganisation: (organisation: OrganisationResponse | null) => void; refetchOrganisations: (options?: { selectOrganisationId?: number }) => Promise; }) { const { user } = useAuthenticatedSession(); const [open, setOpen] = useState(false); const [activeTab, setActiveTab] = useState("info"); const [members, setMembers] = useState([]); const [statuses, setStatuses] = useState([]); const [isCreatingStatus, setIsCreatingStatus] = useState(false); const [newStatusName, setNewStatusName] = useState(""); const [statusError, setStatusError] = useState(null); const [statusToRemove, setStatusToRemove] = useState(null); const [reassignToStatus, setReassignToStatus] = useState(""); const [confirmDialog, setConfirmDialog] = useState<{ open: boolean; title: string; message: string; confirmText: string; processingText: string; variant: "default" | "destructive"; onConfirm: () => Promise; }>({ open: false, title: "", message: "", confirmText: "", processingText: "", variant: "default", onConfirm: async () => {}, }); const isAdmin = selectedOrganisation?.OrganisationMember.role === "owner" || selectedOrganisation?.OrganisationMember.role === "admin"; const refetchMembers = useCallback(async () => { if (!selectedOrganisation) return; try { await organisation.members({ organisationId: selectedOrganisation.Organisation.id, onSuccess: (data) => { const members = data as OrganisationMemberResponse[]; const roleOrder: Record = { owner: 0, admin: 1, member: 2 }; members.sort((a, b) => { const roleA = roleOrder[a.OrganisationMember.role] ?? 3; const roleB = roleOrder[b.OrganisationMember.role] ?? 3; if (roleA !== roleB) return roleA - roleB; return a.User.name.localeCompare(b.User.name); }); setMembers(members); }, onError: (error) => { console.error(error); setMembers([]); }, }); } catch (err) { console.error("error fetching members:", err); setMembers([]); } }, [selectedOrganisation]); const handleRoleChange = (memberUserId: number, memberName: string, currentRole: string) => { if (!selectedOrganisation) return; const action = currentRole === "admin" ? "demote" : "promote"; const newRole = currentRole === "admin" ? "member" : "admin"; setConfirmDialog({ open: true, title: action === "promote" ? "Promote Member" : "Demote Member", message: `Are you sure you want to ${action} ${memberName} to ${newRole}?`, confirmText: action === "promote" ? "Promote" : "Demote", processingText: action === "promote" ? "Promoting..." : "Demoting...", variant: action === "demote" ? "destructive" : "default", onConfirm: async () => { try { await organisation.updateMemberRole({ organisationId: selectedOrganisation.Organisation.id, userId: memberUserId, role: newRole, onSuccess: () => { closeConfirmDialog(); void refetchMembers(); }, onError: (error) => { console.error(error); }, }); } catch (err) { console.error(err); } }, }); }; const closeConfirmDialog = () => { setConfirmDialog((prev) => ({ ...prev, open: false })); }; const handleRemoveMember = (memberUserId: number, memberName: string) => { if (!selectedOrganisation) return; setConfirmDialog({ open: true, title: "Remove Member", message: `Are you sure you want to remove ${memberName} from this organisation?`, confirmText: "Remove", processingText: "Removing...", variant: "destructive", onConfirm: async () => { try { await organisation.removeMember({ organisationId: selectedOrganisation.Organisation.id, userId: memberUserId, onSuccess: () => { closeConfirmDialog(); void refetchMembers(); }, onError: (error) => { console.error(error); }, }); } catch (err) { console.error(err); } }, }); }; useEffect(() => { if (selectedOrganisation) { const orgStatuses = (selectedOrganisation.Organisation as unknown as { statuses: string[] }) .statuses; setStatuses( Array.isArray(orgStatuses) ? orgStatuses : ["TO DO", "IN PROGRESS", "REVIEW", "DONE"], ); } }, [selectedOrganisation]); const updateStatuses = async (newStatuses: string[]) => { if (!selectedOrganisation) return; try { await organisation.update({ organisationId: selectedOrganisation.Organisation.id, statuses: newStatuses, onSuccess: () => { setStatuses(newStatuses); void refetchOrganisations(); }, onError: (error) => { console.error("error updating statuses:", error); }, }); } catch (err) { console.error("error updating statuses:", err); } }; const handleCreateStatus = async () => { const trimmed = newStatusName.trim().toUpperCase(); if (!trimmed) return; if (trimmed.length > ISSUE_STATUS_MAX_LENGTH) { setStatusError(`status must be <= ${ISSUE_STATUS_MAX_LENGTH} characters`); return; } if (statuses.includes(trimmed)) { setNewStatusName(""); setIsCreatingStatus(false); setStatusError(null); return; } const newStatuses = [...statuses, trimmed]; await updateStatuses(newStatuses); setNewStatusName(""); setIsCreatingStatus(false); setStatusError(null); }; const handleRemoveStatusClick = (status: string) => { if (statuses.length <= 1) return; setStatusToRemove(status); const remaining = statuses.filter((s) => s !== status); setReassignToStatus(remaining[0] || ""); }; const moveStatus = async (status: string, direction: "up" | "down") => { const currentIndex = statuses.indexOf(status); if (currentIndex === -1) return; const nextIndex = direction === "up" ? currentIndex - 1 : currentIndex + 1; if (nextIndex < 0 || nextIndex >= statuses.length) return; const nextStatuses = [...statuses]; [nextStatuses[currentIndex], nextStatuses[nextIndex]] = [ nextStatuses[nextIndex], nextStatuses[currentIndex], ]; await updateStatuses(nextStatuses); }; const confirmRemoveStatus = async () => { if (!statusToRemove || !reassignToStatus || !selectedOrganisation) return; await issue.replaceStatus({ organisationId: selectedOrganisation.Organisation.id, oldStatus: statusToRemove, newStatus: reassignToStatus, onSuccess: async () => { const newStatuses = statuses.filter((s) => s !== statusToRemove); await updateStatuses(newStatuses); setStatusToRemove(null); setReassignToStatus(""); }, onError: (error) => { console.error("error replacing status:", error); }, }); }; useEffect(() => { if (!open) return; void refetchMembers(); }, [open, refetchMembers]); return ( {trigger || ( )} Organisations
{selectedOrganisation ? (
{ setSelectedOrganisation(org); localStorage.setItem( "selectedOrganisationId", `${org?.Organisation.id}`, ); }} onCreateOrganisation={async (organisationId) => { await refetchOrganisations({ selectOrganisationId: organisationId }); }} contentClass={ "data-[side=bottom]:translate-y-2 data-[side=bottom]:translate-x-0.25" } /> Info Users Issues

{selectedOrganisation.Organisation.name}

Slug: {selectedOrganisation.Organisation.slug}

Role: {selectedOrganisation.OrganisationMember.role}

{selectedOrganisation.Organisation.description ? (

{selectedOrganisation.Organisation.description}

) : (

No description

)}

{members.length} Member{members.length !== 1 ? "s" : ""}

{members.map((member) => (
{member.OrganisationMember.role}
{isAdmin && member.OrganisationMember.role !== "owner" && member.User.id !== user.id && ( <> )}
))}
{isAdmin && ( m.User.username)} onSuccess={refetchMembers} trigger={ } /> )}

Issue Statuses

{statuses.map((status, index) => (
{isAdmin && (
{statuses.length > 1 && ( )}
)}
))}
{isAdmin && (isCreatingStatus ? (
{ setNewStatusName(e.target.value); if (statusError) setStatusError(null); }} placeholder="Status name" className="flex-1" onKeyDown={(e) => { if (e.key === "Enter") { void handleCreateStatus(); } else if (e.key === "Escape") { setIsCreatingStatus(false); setNewStatusName(""); setStatusError(null); } }} autoFocus />
{statusError && (

{statusError}

)}
) : ( ))}
) : (
{ setSelectedOrganisation(org); localStorage.setItem("selectedOrganisationId", `${org?.Organisation.id}`); }} onCreateOrganisation={async (organisationId) => { await refetchOrganisations({ selectOrganisationId: organisationId }); }} contentClass={ "data-[side=bottom]:translate-y-2 data-[side=bottom]:translate-x-0.25" } />

No organisations yet.

)} setConfirmDialog((prev) => ({ ...prev, open }))} onConfirm={confirmDialog.onConfirm} title={confirmDialog.title} processingText={confirmDialog.processingText} message={confirmDialog.message} confirmText={confirmDialog.confirmText} variant={confirmDialog.variant} /> {/* Status removal dialog with reassignment */} { if (!open) { setStatusToRemove(null); setReassignToStatus(""); } }} > Remove Status

Are you sure you want to remove the "{statusToRemove}" status? Which status would you like issues with this status to be set to?

); } export default OrganisationsDialog;