Skip to content

Commit

Permalink
ManageQuestionnaireOrganizationsSheet component and integrate with Qu…
Browse files Browse the repository at this point in the history
…estionnaireShow

- Introduced a new ManageQuestionnaireOrganizationsSheet component for managing organizations associated with a questionnaire.
- Integrated the new component into the QuestionnaireShow component, allowing users to add or remove organizations.
- Updated the questionnaireApi to reflect the correct request body structure for setting organizations.
- Enhanced the user interface for organization selection and management, including search functionality and visual feedback for selected organizations.
  • Loading branch information
bodhish committed Jan 2, 2025
1 parent edca4ef commit 77c8a9b
Show file tree
Hide file tree
Showing 3 changed files with 232 additions and 1 deletion.
229 changes: 229 additions & 0 deletions src/components/Questionnaire/ManageQuestionnaireOrganizationsSheet.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,229 @@
import { useMutation, useQuery, useQueryClient } from "@tanstack/react-query";
import { Building, Check, Loader2, X } from "lucide-react";
import { useEffect, useState } from "react";
import { toast } from "sonner";

import { Badge } from "@/components/ui/badge";
import { Button } from "@/components/ui/button";
import {
Command,
CommandEmpty,
CommandGroup,
CommandInput,
CommandItem,
CommandList,
} from "@/components/ui/command";
import {
Sheet,
SheetContent,
SheetDescription,
SheetFooter,
SheetHeader,
SheetTitle,
SheetTrigger,
} from "@/components/ui/sheet";

import mutate from "@/Utils/request/mutate";
import query from "@/Utils/request/query";
import organizationApi from "@/types/organization/organizationApi";
import questionnaireApi from "@/types/questionnaire/questionnaireApi";

interface Props {
questionnaireId: string;
trigger?: React.ReactNode;
}

export default function ManageQuestionnaireOrganizationsSheet({
questionnaireId,
trigger,
}: Props) {
const queryClient = useQueryClient();
const [open, setOpen] = useState(false);
const [searchQuery, setSearchQuery] = useState("");
const [selectedIds, setSelectedIds] = useState<string[]>([]);

const { data: organizations, isLoading } = useQuery({
queryKey: ["questionnaire", questionnaireId, "organizations"],
queryFn: query(questionnaireApi.getOrganizations, {
pathParams: { id: questionnaireId },
}),
enabled: open,
});

const { data: availableOrganizations, isLoading: isLoadingOrganizations } =
useQuery({
queryKey: ["organizations", searchQuery],
queryFn: query(organizationApi.list, {
queryParams: {
org_type: "role",
name: searchQuery || undefined,
},
}),
enabled: open,
});

const { mutate: setOrganizations, isPending: isUpdating } = useMutation({
mutationFn: (organizations: string[]) =>
mutate(questionnaireApi.setOrganizations, {
pathParams: { id: questionnaireId },
body: { organizations: organizations },
})({ organizations: organizations }),
onSuccess: () => {
queryClient.invalidateQueries({
queryKey: ["questionnaire", questionnaireId, "organizations"],
});
toast.success("Organizations updated successfully");
setOpen(false);
},
});

// Initialize selected IDs when organizations are loaded
useEffect(() => {
if (organizations?.results) {
setSelectedIds(organizations.results.map((org) => org.id));
}
}, [organizations?.results]);

const handleToggleOrganization = (orgId: string) => {
setSelectedIds((current) =>
current.includes(orgId)
? current.filter((id) => id !== orgId)
: [...current, orgId],
);
};

const handleSave = () => {
setOrganizations(selectedIds);
};

const selectedOrganizations = organizations?.results.filter((org) =>
selectedIds.includes(org.id),
);

const hasChanges =
JSON.stringify(organizations?.results.map((org) => org.id).sort()) !==
JSON.stringify(selectedIds.sort());

return (
<Sheet open={open} onOpenChange={setOpen}>
<SheetTrigger asChild>
{trigger || (
<Button variant="outline" size="sm">
<Building className="mr-2 h-4 w-4" />
Manage Organizations
</Button>
)}
</SheetTrigger>
<SheetContent>
<SheetHeader>
<SheetTitle>Manage Organizations</SheetTitle>
<SheetDescription>
Add or remove organizations from this questionnaire
</SheetDescription>
</SheetHeader>

<div className="space-y-6 py-4">
{/* Selected Organizations */}
<div className="space-y-4">
<h3 className="text-sm font-medium">Selected Organizations</h3>
<div className="flex flex-wrap gap-2">
{selectedOrganizations?.map((org) => (
<Badge
key={org.id}
variant="secondary"
className="flex items-center gap-1"
>
{org.name}
<Button
variant="ghost"
size="icon"
className="h-4 w-4 p-0 hover:bg-transparent"
onClick={() => handleToggleOrganization(org.id)}
disabled={isUpdating}
>
<X className="h-3 w-3" />
</Button>
</Badge>
))}
{!isLoading &&
(!selectedOrganizations ||
selectedOrganizations.length === 0) && (
<p className="text-sm text-muted-foreground">
No organizations selected
</p>
)}
</div>
</div>

{/* Organization Selector */}
<div className="space-y-4">
<h3 className="text-sm font-medium">Add Organizations</h3>
<Command className="rounded-lg border shadow-md">
<CommandInput
placeholder="Search organizations..."
onValueChange={setSearchQuery}
/>
<CommandList>
<CommandEmpty>No organizations found.</CommandEmpty>
<CommandGroup>
{isLoadingOrganizations ? (
<div className="flex items-center justify-center py-6">
<Loader2 className="h-6 w-6 animate-spin" />
</div>
) : (
availableOrganizations?.results.map((org) => (
<CommandItem
key={org.id}
value={org.id}
onSelect={() => handleToggleOrganization(org.id)}
>
<div className="flex flex-1 items-center gap-2">
<Building className="h-4 w-4" />
<span>{org.name}</span>
{org.description && (
<span className="text-xs text-muted-foreground">
- {org.description}
</span>
)}
</div>
{selectedIds.includes(org.id) && (
<Check className="h-4 w-4" />
)}
</CommandItem>
))
)}
</CommandGroup>
</CommandList>
</Command>
</div>
</div>

<SheetFooter className="absolute bottom-0 left-0 right-0 p-4 border-t bg-background">
<div className="flex w-full justify-end gap-4">
<Button
variant="outline"
onClick={() => {
if (organizations?.results) {
setSelectedIds(organizations.results.map((org) => org.id));
}
setOpen(false);
}}
>
Cancel
</Button>
<Button onClick={handleSave} disabled={isUpdating || !hasChanges}>
{isUpdating ? (
<>
<Loader2 className="mr-2 h-4 w-4 animate-spin" />
Saving...
</>
) : (
"Save Changes"
)}
</Button>
</div>
</SheetFooter>
</SheetContent>
</Sheet>
);
}
2 changes: 2 additions & 0 deletions src/components/Questionnaire/show.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ import type { Question } from "@/types/questionnaire/question";
import questionnaireApi from "@/types/questionnaire/questionnaireApi";

import Loading from "../Common/Loading";
import ManageQuestionnaireOrganizationsSheet from "./ManageQuestionnaireOrganizationsSheet";
import { QuestionnaireForm } from "./QuestionnaireForm";

interface QuestionnaireShowProps {
Expand Down Expand Up @@ -106,6 +107,7 @@ export function QuestionnaireShow({ id }: QuestionnaireShowProps) {
<p className="text-gray-600">{questionnaire.description}</p>
</div>
<div className="flex gap-2">
<ManageQuestionnaireOrganizationsSheet questionnaireId={id} />
<Button variant="outline" onClick={() => navigate("/questionnaire")}>
<CareIcon icon="l-arrow-left" className="mr-2 h-4 w-4" />
Back to List
Expand Down
2 changes: 1 addition & 1 deletion src/types/questionnaire/questionnaireApi.ts
Original file line number Diff line number Diff line change
Expand Up @@ -70,6 +70,6 @@ export default {
path: "/api/v1/questionnaire/{id}/set_organizations/",
method: HttpMethod.POST,
TRes: Type<PaginatedResponse<Organization>>(),
TBody: {} as { organization: string[] },
TBody: {} as { organizations: string[] },
},
};

0 comments on commit 77c8a9b

Please sign in to comment.