Skip to content

Commit

Permalink
Merge branch 'develop' into use_useQuery_from_tanstack/react-query
Browse files Browse the repository at this point in the history
  • Loading branch information
AdityaJ2305 authored Feb 2, 2025
2 parents 0601108 + 6d880c7 commit 3875488
Show file tree
Hide file tree
Showing 8 changed files with 196 additions and 24 deletions.
1 change: 1 addition & 0 deletions src/Routers/routes/questionnaireRoutes.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ import { AppRoutes } from "@/Routers/AppRouter";

const QuestionnaireRoutes: AppRoutes = {
"/questionnaire": () => <QuestionnaireList />,
"/questionnaire/create": () => <QuestionnaireEditor />,
"/questionnaire/:id": ({ id }) => <QuestionnaireShow id={id} />,
"/questionnaire/:id/edit": ({ id }) => <QuestionnaireEditor id={id} />,
};
Expand Down
207 changes: 189 additions & 18 deletions src/components/Questionnaire/QuestionnaireEditor.tsx
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import { DragDropContext, Draggable, Droppable } from "@hello-pangea/dnd";
import { useMutation, useQuery } from "@tanstack/react-query";
import { Building, Check, Loader2, X } from "lucide-react";
import { useNavigate } from "raviger";
import { useEffect, useState } from "react";
import { toast } from "sonner";
Expand All @@ -15,6 +16,14 @@ import {
CollapsibleContent,
CollapsibleTrigger,
} from "@/components/ui/collapsible";
import {
Command,
CommandEmpty,
CommandGroup,
CommandInput,
CommandItem,
CommandList,
} from "@/components/ui/command";
import { Input } from "@/components/ui/input";
import { Label } from "@/components/ui/label";
import {
Expand All @@ -32,6 +41,7 @@ import Loading from "@/components/Common/Loading";

import mutate from "@/Utils/request/mutate";
import query from "@/Utils/request/query";
import organizationApi from "@/types/organization/organizationApi";
import {
AnswerOption,
EnableWhen,
Expand All @@ -46,10 +56,11 @@ import {
} from "@/types/questionnaire/questionnaire";
import questionnaireApi from "@/types/questionnaire/questionnaireApi";

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

interface QuestionnaireEditorProps {
id: string;
id?: string;
}

export default function QuestionnaireEditor({ id }: QuestionnaireEditorProps) {
Expand All @@ -58,6 +69,8 @@ export default function QuestionnaireEditor({ id }: QuestionnaireEditorProps) {
const [expandedQuestions, setExpandedQuestions] = useState<Set<string>>(
new Set(),
);
const [selectedOrgIds, setSelectedOrgIds] = useState<string[]>([]);
const [orgSearchQuery, setOrgSearchQuery] = useState("");

const {
data: initialQuestionnaire,
Expand All @@ -66,13 +79,36 @@ export default function QuestionnaireEditor({ id }: QuestionnaireEditorProps) {
} = useQuery({
queryKey: ["questionnaireDetail", id],
queryFn: query(questionnaireApi.detail, {
pathParams: { id },
pathParams: { id: id! },
}),
enabled: !!id,
});

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

const { mutate: createQuestionnaire, isPending: isCreating } = useMutation({
mutationFn: mutate(questionnaireApi.create),
onSuccess: (data: QuestionnaireDetail) => {
toast.success("Questionnaire created successfully");
navigate(`/questionnaire/${data.slug}`);
},
onError: (_error) => {
toast.error("Failed to create questionnaire");
},
});

const { mutate: updateQuestionnaire, isPending } = useMutation({
const { mutate: updateQuestionnaire, isPending: isUpdating } = useMutation({
mutationFn: mutate(questionnaireApi.update, {
pathParams: { id },
pathParams: { id: id! },
}),
onSuccess: () => {
toast.success("Questionnaire updated successfully");
Expand All @@ -83,15 +119,30 @@ export default function QuestionnaireEditor({ id }: QuestionnaireEditorProps) {
});

const [questionnaire, setQuestionnaire] =
useState<QuestionnaireDetail | null>(null);
useState<QuestionnaireDetail | null>(() => {
if (!id) {
return {
id: "",
title: "",
description: "",
status: "draft",
version: "1.0",
subject_type: "patient",
questions: [],
slug: "",
tags: [],
} as QuestionnaireDetail;
}
return null;
});

useEffect(() => {
if (initialQuestionnaire) {
setQuestionnaire(initialQuestionnaire);
}
}, [initialQuestionnaire]);

if (isLoading) return <Loading />;
if (id && isLoading) return <Loading />;
if (error) {
return (
<Alert variant="destructive">
Expand Down Expand Up @@ -122,8 +173,19 @@ export default function QuestionnaireEditor({ id }: QuestionnaireEditorProps) {
setQuestionnaire((prev) => (prev ? { ...prev, [field]: value } : null));
};

const handleSave = () => {
if (id) {
updateQuestionnaire(questionnaire);
} else {
createQuestionnaire({
...questionnaire,
organizations: selectedOrgIds,
});
}
};

const handleCancel = () => {
navigate(`/questionnaire/${id}`);
navigate(id ? `/questionnaire/${id}` : "/questionnaire");
};

const handleDragEnd = (result: any) => {
Expand All @@ -148,25 +210,31 @@ export default function QuestionnaireEditor({ id }: QuestionnaireEditorProps) {
});
};

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

return (
<div className="container mx-auto px-4 py-6">
{/* Top bar: Title + Buttons */}
<div className="mb-4 flex items-center justify-between">
<div>
<h1 className="text-2xl font-bold">Edit Questionnaire</h1>
<h1 className="text-2xl font-bold">
{id ? "Edit Questionnaire" : "Create Questionnaire"}
</h1>
<p className="text-sm text-gray-500">{questionnaire.description}</p>
</div>
<div className="flex gap-2">
<Button variant="outline" onClick={handleCancel}>
<CareIcon icon="l-arrow-left" className="mr-2 h-4 w-4" />
Cancel
</Button>
<Button
onClick={() => updateQuestionnaire(questionnaire)}
disabled={isPending}
>
<Button onClick={handleSave} disabled={isCreating || isUpdating}>
<CareIcon icon="l-save" className="mr-2 h-4 w-4" />
Save
{id ? "Save" : "Create"}
</Button>
</div>
</div>
Expand All @@ -182,7 +250,6 @@ export default function QuestionnaireEditor({ id }: QuestionnaireEditorProps) {

<TabsContent value="edit">
<div className="grid gap-6 lg:grid-cols-[300px,1fr]">
{/* Left Sidebar: Navigation */}
<div className="space-y-4">
<Card>
<CardHeader>
Expand Down Expand Up @@ -318,11 +385,100 @@ export default function QuestionnaireEditor({ id }: QuestionnaireEditorProps) {
</SelectContent>
</Select>
</div>

<div>
<Label>Organizations</Label>
{id ? (
<ManageQuestionnaireOrganizationsSheet
questionnaireId={id}
trigger={
<Button
variant="outline"
className="w-full justify-start"
>
<Building className="mr-2 h-4 w-4" />
Manage Organizations
</Button>
}
/>
) : (
<div className="space-y-4">
<div className="flex flex-wrap gap-2">
{selectedOrgIds.length > 0 ? (
availableOrganizations?.results
.filter((org) => selectedOrgIds.includes(org.id))
.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)
}
>
<X className="h-3 w-3" />
</Button>
</Badge>
))
) : (
<p className="text-sm text-muted-foreground">
No organizations selected
</p>
)}
</div>

<Command className="rounded-lg border shadow-md">
<CommandInput
placeholder="Search organizations..."
onValueChange={setOrgSearchQuery}
/>
<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>
{selectedOrgIds.includes(org.id) && (
<Check className="h-4 w-4" />
)}
</CommandItem>
))
)}
</CommandGroup>
</CommandList>
</Command>
</div>
)}
</div>
</CardContent>
</Card>
</div>

{/* Main Content */}
<div className="space-y-6">
<Card>
<CardHeader>
Expand All @@ -340,6 +496,22 @@ export default function QuestionnaireEditor({ id }: QuestionnaireEditorProps) {
/>
</div>

<div>
<Label htmlFor="slug">Slug</Label>
<Input
id="slug"
value={questionnaire.slug}
onChange={(e) =>
updateQuestionnaireField("slug", e.target.value)
}
placeholder="unique-identifier-for-questionnaire"
className="font-mono"
/>
<p className="text-sm text-muted-foreground mt-1">
A unique URL-friendly identifier for this questionnaire
</p>
</div>

<div>
<Label htmlFor="desc">Description</Label>
<Textarea
Expand Down Expand Up @@ -368,7 +540,7 @@ export default function QuestionnaireEditor({ id }: QuestionnaireEditorProps) {
onClick={() => {
const newQuestion: Question = {
id: crypto.randomUUID(),
link_id: `Q-${Date.now()}`,
link_id: `${questionnaire.questions.length + 1}`,
text: "New Question",
type: "string",
questions: [],
Expand Down Expand Up @@ -619,7 +791,6 @@ function QuestionEditor({
<Select
value={type}
onValueChange={(val: QuestionType) => {
// Reset questions array when changing from group to another type
if (val !== "group") {
updateField("type", val, { questions: [] });
} else {
Expand Down
2 changes: 1 addition & 1 deletion src/components/Questionnaire/show.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -293,7 +293,7 @@ export function QuestionnaireShow({ id }: QuestionnaireShowProps) {
</div>
</TabsContent>

<TabsContent value="preview" className="max-w-3xl mx-auto">
<TabsContent value="preview" className="mx-auto">
<Card>
<CardHeader>
<CardTitle>{questionnaire.title}</CardTitle>
Expand Down
2 changes: 1 addition & 1 deletion src/components/ui/button.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@ const buttonVariants = cva(
destructive:
"bg-red-500 text-gray-50 shadow-sm hover:bg-red-500/90 dark:bg-red-900 dark:text-gray-50 dark:hover:bg-red-900/90",
outline:
"border border-gray-400/75 bg-white shadow-sm hover:bg-gray-100 hover:text-gray-900 dark:border-gray-800 dark:bg-gray-950 dark:hover:bg-gray-800 dark:hover:text-gray-50",
"border border-gray-200 bg-white shadow-sm hover:bg-gray-100 hover:text-gray-900 dark:border-gray-800 dark:bg-gray-950 dark:hover:bg-gray-800 dark:hover:text-gray-50",
primary:
"bg-primary-700 text-white shadow hover:bg-primary-700/90 dark:bg-primary-100 dark:text-primary-900 dark:hover:bg-primary-100/90",
secondary:
Expand Down
2 changes: 1 addition & 1 deletion src/components/ui/input.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@ const Input = React.forwardRef<HTMLInputElement, React.ComponentProps<"input">>(
<input
type={type}
className={cn(
"flex w-full rounded-md border border-gray-400/75 bg-white px-3 py-2 text-base shadow-sm transition-colors file:border-0 file:bg-transparent file:text-sm file:font-medium file:text-gray-950 placeholder:text-gray-500 focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-gray-950 disabled:cursor-not-allowed disabled:opacity-50 md:text-sm dark:border-gray-800 dark:file:text-gray-50 dark:placeholder:text-gray-400 dark:focus-visible:ring-gray-300",
"flex w-full rounded-md border border-gray-200 bg-white px-3 py-2 text-base shadow-sm transition-colors file:border-0 file:bg-transparent file:text-sm file:font-medium file:text-gray-950 placeholder:text-gray-500 focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-gray-950 disabled:cursor-not-allowed disabled:opacity-50 md:text-sm dark:border-gray-800 dark:file:text-gray-50 dark:placeholder:text-gray-400 dark:focus-visible:ring-gray-300",
className,
)}
ref={ref}
Expand Down
2 changes: 1 addition & 1 deletion src/components/ui/phone-input.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -74,7 +74,7 @@ const InputComponent = React.forwardRef<
>(({ className, ...props }, ref) => (
<Input
className={cn(
"rounded-e-md rounded-s-none focus-visible:ring-0 focus-visible:outline-none focus-visible:border-gray-400/75",
"rounded-e-md rounded-s-none focus-visible:ring-0 focus-visible:outline-none focus-visible:border-gray-200",
className,
)}
{...props}
Expand Down
2 changes: 1 addition & 1 deletion src/components/ui/select.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,7 @@ const SelectTrigger = React.forwardRef<
<SelectPrimitive.Trigger
ref={ref}
className={cn(
"flex h-9 w-full items-center justify-between whitespace-nowrap rounded-md border border-gray-400/75 bg-white px-3 py-2 text-sm shadow-sm transition-colors placeholder:text-gray-500 focus:border-primary-500 focus:outline-none focus:ring-1 focus:ring-primary-900 disabled:cursor-not-allowed disabled:opacity-50 dark:border-gray-800 dark:bg-gray-950 dark:ring-offset-gray-950 dark:placeholder:text-gray-400 dark:focus-visible:ring-gray-300 [&>span]:line-clamp-1",
"flex h-9 w-full items-center justify-between whitespace-nowrap rounded-md border border-gray-200 bg-white px-3 py-2 text-sm shadow-sm transition-colors placeholder:text-gray-500 focus:border-primary-500 focus:outline-none focus:ring-1 focus:ring-primary-900 disabled:cursor-not-allowed disabled:opacity-50 dark:border-gray-800 dark:bg-gray-950 dark:ring-offset-gray-950 dark:placeholder:text-gray-400 dark:focus-visible:ring-gray-300 [&>span]:line-clamp-1",
className,
)}
{...props}
Expand Down
2 changes: 1 addition & 1 deletion src/components/ui/textarea.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@ const Textarea = React.forwardRef<
return (
<textarea
className={cn(
"flex min-h-[80px] w-full rounded-md border border-gray-400/75 bg-white px-3 py-1 text-base shadow-sm transition-colors placeholder:text-gray-500 focus-visible:ring-1 focus-visible:border-primary-500 focus-visible:outline-none focus-visible:ring-primary-500 md:text-sm disabled:opacity-50",
"flex min-h-[80px] w-full rounded-md border border-gray-200 bg-white px-3 py-1 text-base shadow-sm transition-colors placeholder:text-gray-500 focus-visible:ring-1 focus-visible:border-primary-500 focus-visible:outline-none focus-visible:ring-primary-500 md:text-sm disabled:opacity-50",
className,
)}
ref={ref}
Expand Down

0 comments on commit 3875488

Please sign in to comment.