Skip to content

Commit

Permalink
Merge branch 'dev' into feat/ticket-testing
Browse files Browse the repository at this point in the history
  • Loading branch information
ZaguePrime authored Sep 29, 2024
2 parents bb319f7 + b19567d commit ad5314d
Show file tree
Hide file tree
Showing 37 changed files with 1,474 additions and 98 deletions.
2 changes: 2 additions & 0 deletions admin-frontend/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -25,10 +25,12 @@
"class-variance-authority": "^0.7.0",
"clsx": "^2.1.1",
"dagre": "^0.8.5",
"date-fns": "^4.1.0",
"jwt-decode": "^4.0.0",
"lucide-react": "^0.436.0",
"next": "14.2.7",
"react": "^18",
"react-day-picker": "8.10.1",
"react-dom": "^18",
"react-hook-form": "^7.53.0",
"tailwind-merge": "^2.5.2",
Expand Down
56 changes: 41 additions & 15 deletions admin-frontend/src/app/(pages)/disputes/modal.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,11 @@ import { Card, CardContent, CardDescription, CardHeader, CardTitle } from "@/com
import { Input } from "@/components/ui/input";
import { Textarea } from "@/components/ui/textarea";
import { Label } from "@/components/ui/label";
import { DisputeStatusBadge, ExpertStatusBadge } from "@/components/admin/status-badge";
import {
DisputeStatusBadge,
ExpertStatusBadge,
StatusBadge,
} from "@/components/admin/status-badge";
import {
type UserDetails,
type DisputeDetails,
Expand All @@ -34,6 +38,8 @@ import { DisputeStatusDropdown, ObjectionStatusDropdown } from "@/components/adm
import { changeObjectionStatus, getExpertObjections } from "@/lib/api/expert";
import { DISPUTE_DETAILS_KEY, DISPUTE_LIST_KEY } from "@/lib/constants";
import { ObjectionListResponse, ObjectionStatus } from "@/lib/types/experts";
import StateSelect from "@/components/workflow/state-select";
import DeadlineSelect from "@/components/workflow/deadline-select";

export default function DisputeDetails({ id: disputeId }: { id: number }) {
const { toast } = useToast();
Expand Down Expand Up @@ -82,22 +88,42 @@ export default function DisputeDetails({ id: disputeId }: { id: number }) {
</Button>
</DialogClose>
</div>
<div className="flex gap-2 items-center">
<DisputeStatusDropdown
initialValue={details.data.status}
onSelect={(val) => status.mutate(val)}
disabled={status.isPending}
>
<DisputeStatusBadge dropdown variant={details.data.status}>
{details.data.status}
</DisputeStatusBadge>
</DisputeStatusDropdown>
<span>{details.data.date_filed}</span>
</div>
<div className="flex gap-2 items-start">
<div className="grid grid-cols-2 gap-2 mr-auto">
<strong>Status:</strong>
<DisputeStatusDropdown
initialValue={details.data.status}
onSelect={(val) => status.mutate(val)}
disabled={status.isPending}
>
<DisputeStatusBadge dropdown variant={details.data.status}>
{details.data.status}
</DisputeStatusBadge>
</DisputeStatusDropdown>
</div>

<p>Case Number: {details.data.id}</p>
<div className="grid grid-cols-2 gap-x-2">
<strong className="text-right">Filed:</strong>
<span>{details.data.date_filed}</span>
<strong className="text-right">Case number:</strong>
<span>{details.data.id}</span>
</div>
</div>
</DialogHeader>
<div className="overflow-y-auto grow space-y-6 pr-3">
<Card>
<CardHeader>
<CardTitle>Workflow</CardTitle>
<CardDescription>Manage the active workflow of the dispute.</CardDescription>
</CardHeader>
<CardContent className="grid grid-cols-2">
<strong>Current State</strong>
<strong>State deadline</strong>
<StateSelect dispute={disputeId} />
<DeadlineSelect dispute={disputeId} />
</CardContent>
</Card>

<Card>
<CardHeader>
<CardTitle>Overview</CardTitle>
Expand Down Expand Up @@ -257,7 +283,7 @@ function Objection({
const { toast } = useToast();
const client = useQueryClient();
const statusMut = useMutation({
mutationFn: (data: ObjectionStatus) => changeObjectionStatus(disputeId, id, data),
mutationFn: (data: ObjectionStatus) => changeObjectionStatus(id, data),

onSuccess: (data, variables) => {
client.setQueryData(
Expand Down
21 changes: 16 additions & 5 deletions admin-frontend/src/components/dispute/workflow-filter.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,9 @@ import {
SelectLabel,
SelectItem,
} from "@/components/ui/select";
import { getWorkflowList } from "@/lib/api/workflow";
import { WORKFLOW_LIST_KEY } from "@/lib/constants";
import { useQuery } from "@tanstack/react-query";

export default function WorkflowFilter({
initialValue,
Expand All @@ -17,6 +20,14 @@ export default function WorkflowFilter({
onValueChange?: (id: string | undefined) => void;
initialValue?: string;
}) {
const query = useQuery({
queryKey: [WORKFLOW_LIST_KEY],
queryFn: async () => {
const { workflows } = await getWorkflowList({});
return workflows;
},
});

return (
<Select
defaultValue={initialValue ?? "none"}
Expand All @@ -29,11 +40,11 @@ export default function WorkflowFilter({
<SelectGroup>
<SelectLabel>Workflow</SelectLabel>
<SelectItem value={"none"}>No workflow</SelectItem>
<SelectItem value={"1"}>Workflow #1</SelectItem>
<SelectItem value={"2"}>Workflow #2</SelectItem>
<SelectItem value={"3"}>Workflow #3</SelectItem>
<SelectItem value={"4"}>Workflow #4</SelectItem>
<SelectItem value={"5"}>Workflow #5</SelectItem>
{query.data?.map((wf) => (
<SelectItem key={wf.id} value={wf.id.toString()}>
{wf.name}
</SelectItem>
))}
</SelectGroup>
</SelectContent>
</Select>
Expand Down
60 changes: 60 additions & 0 deletions admin-frontend/src/components/ui/calendar.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,60 @@
"use client";

import * as React from "react";
import { ChevronLeft, ChevronRight } from "lucide-react";
import { DayPicker } from "react-day-picker";

import { cn } from "@/lib/utils";
import { buttonVariants } from "@/components/ui/button";

export type CalendarProps = React.ComponentProps<typeof DayPicker>;

function Calendar({ className, classNames, showOutsideDays = true, ...props }: CalendarProps) {
return (
<DayPicker
showOutsideDays={showOutsideDays}
className={cn("p-3", className)}
classNames={{
months: "flex flex-col sm:flex-row space-y-4 sm:space-x-4 sm:space-y-0",
month: "space-y-4",
caption: "flex justify-center pt-1 relative items-center",
caption_label: "text-sm font-medium",
nav: "space-x-1 flex items-center",
nav_button: cn(
buttonVariants({ variant: "ghost" }),
"aspect-square opacity-50 hover:opacity-100"
),
nav_button_previous: "absolute left-1",
nav_button_next: "absolute right-1",
table: "w-full border-collapse space-y-1",
head_row: "flex",
head_cell: "text-slate-500 rounded-md w-9 font-normal text-[0.8rem] dark:text-slate-400",
row: "flex w-full mt-2",
cell: "h-9 w-9 text-center text-sm p-0 relative [&:has([aria-selected].day-range-end)]:rounded-r-md [&:has([aria-selected].day-outside)]:bg-slate-100/50 [&:has([aria-selected])]:bg-slate-100 first:[&:has([aria-selected])]:rounded-l-md last:[&:has([aria-selected])]:rounded-r-md focus-within:relative focus-within:z-20 dark:[&:has([aria-selected].day-outside)]:bg-slate-800/50 dark:[&:has([aria-selected])]:bg-slate-800",
day: cn(
buttonVariants({ variant: "ghost" }),
"h-9 w-9 p-0 justify-center font-normal aria-selected:opacity-100"
),
day_range_end: "day-range-end",
day_selected:
"bg-primary-500 text-white focus:bg-primary-500 dark:bg-slate-50 dark:text-slate-900 dark:hover:bg-slate-50 dark:hover:text-slate-900 dark:focus:bg-slate-50 dark:focus:text-slate-900",
day_today: "bg-primary-500/20 text-slate-900 dark:bg-slate-800 dark:text-slate-50",
day_outside:
"day-outside text-slate-500 opacity-50 aria-selected:bg-slate-100/50 aria-selected:text-slate-500 aria-selected:opacity-30 dark:text-slate-400 dark:aria-selected:bg-slate-800/50 dark:aria-selected:text-slate-400",
day_disabled: "text-slate-500 opacity-50 dark:text-slate-400",
day_range_middle:
"aria-selected:bg-slate-100 aria-selected:text-slate-900 dark:aria-selected:bg-slate-800 dark:aria-selected:text-slate-50",
day_hidden: "invisible",
...classNames,
}}
components={{
IconLeft: ({ ...props }) => <ChevronLeft className="h-4 w-4" />,
IconRight: ({ ...props }) => <ChevronRight className="h-4 w-4" />,
}}
{...props}
/>
);
}
Calendar.displayName = "Calendar";

export { Calendar };
103 changes: 103 additions & 0 deletions admin-frontend/src/components/workflow/deadline-select.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,103 @@
"use client";

import { changeDisputeDeadline, getDisputeWorkflow } from "@/lib/api/dispute";
import {} from "@/lib/api/workflow";
import { WORKFLOW_STATES_KEY } from "@/lib/constants";
import { useErrorToast } from "@/lib/hooks/use-query-toast";
import { useToast } from "@/lib/hooks/use-toast";
import { cn } from "@/lib/utils";
import { Popover, PopoverTrigger, PopoverContent } from "@/components/ui/popover";
import { Calendar } from "@/components/ui/calendar";
import { useMutation, useQuery, useQueryClient } from "@tanstack/react-query";
import { CalendarIcon } from "lucide-react";
import { useEffect, useState } from "react";
import { Button } from "../ui/button";
import { format } from "date-fns";
import { DateBefore } from "react-day-picker";

export default function DeadlineSelect({ dispute }: { dispute: number }) {
const query = useQuery({
queryKey: [WORKFLOW_STATES_KEY, dispute],
queryFn: () => getDisputeWorkflow(dispute),
});
useErrorToast(query.error, "Failed to fetch dispute workflow");

const { toast } = useToast();
const client = useQueryClient();
const currentDeadline = useMutation({
mutationFn: (state: Date) => changeDisputeDeadline(dispute, state),
onSuccess: (data, variables) => {
client.invalidateQueries({
queryKey: [WORKFLOW_STATES_KEY, dispute],
});
},
onError: (error) => {
toast({
variant: "error",
title: "Something went wrong",
description: error?.message,
});
},
});

function onValueChange(value: Date | undefined) {
if (!value) {
return;
}
if (value.toISOString() === query.data!.current_deadline) {
return;
}
currentDeadline.mutate(value);
}

const [date, setDate] = useState<Date>();
useEffect(() => {
const { data } = query;
if (!data) {
setDate(undefined);
return;
}

if (!data.definition.states[data.current_state].timer) {
setDate(undefined);
return;
}

if (data.current_deadline) {
setDate(new Date(data.current_deadline));
}
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [query.data]);

const beforeMatcher: DateBefore = { before: new Date() };
return (
<Popover>
<PopoverTrigger asChild>
<Button
disabled={!date}
variant={"outline"}
className={cn(
"w-[280px] bg-surface-light-100 dark:bg-surface-dark-900 justify-start text-left font-normal",
!date && "text-muted-foreground"
)}
>
<CalendarIcon className="mr-2 h-4 w-4" />
{date ? (
format(date, "PPP")
) : (
<span>No deadline {query.isSuccess && !date && "(not supported)"}</span>
)}
</Button>
</PopoverTrigger>
<PopoverContent className="w-auto p-0">
<Calendar
disabled={beforeMatcher}
mode="single"
selected={date}
onSelect={onValueChange}
initialFocus
/>
</PopoverContent>
</Popover>
);
}
83 changes: 83 additions & 0 deletions admin-frontend/src/components/workflow/state-select.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,83 @@
"use client";

import {
Select,
SelectTrigger,
SelectValue,
SelectContent,
SelectGroup,
SelectLabel,
SelectItem,
} from "@/components/ui/select";
import { changeDisputeState, getDisputeWorkflow } from "@/lib/api/dispute";
import {} from "@/lib/api/workflow";
import { WORKFLOW_STATES_KEY } from "@/lib/constants";
import { useErrorToast } from "@/lib/hooks/use-query-toast";
import { useToast } from "@/lib/hooks/use-toast";
import { useMutation, useQuery, useQueryClient } from "@tanstack/react-query";
import { useMemo } from "react";

export default function StateSelect({ dispute }: { dispute: number }) {
const query = useQuery({
queryKey: [WORKFLOW_STATES_KEY, dispute],
queryFn: () => getDisputeWorkflow(dispute),
});
useErrorToast(query.error, "Failed to fetch dispute workflow");

const states = useMemo(() => {
if (query.data) {
return Object.entries(query.data.definition.states).map(([id, st]) => ({
id,
label: st.label,
}));
}
return undefined;
}, [query.data]);

const { toast } = useToast();
const client = useQueryClient();
const currentState = useMutation({
mutationFn: (state: string) => changeDisputeState(dispute, state),
onSuccess: (data, variables) => {
client.invalidateQueries({
queryKey: [WORKFLOW_STATES_KEY, dispute],
});
},
onError: (error) => {
toast({
variant: "error",
title: "Something went wrong",
description: error?.message,
});
},
});

function onValueChange(value: string) {
if (value === query.data!.current_state) {
return;
}
currentState.mutate(value);
}

return (
<Select
disabled={!query.isSuccess || currentState.isPending}
value={query.data?.current_state}
onValueChange={onValueChange}
>
<SelectTrigger className="w-[180px]">
<SelectValue />
</SelectTrigger>
<SelectContent>
<SelectGroup>
<SelectLabel>Workflow state</SelectLabel>
{states?.map(({ id, label }) => (
<SelectItem key={id} value={id}>
{label}
</SelectItem>
))}
</SelectGroup>
</SelectContent>
</Select>
);
}
Loading

0 comments on commit ad5314d

Please sign in to comment.