Allow user to set estimate after vote

This commit is contained in:
Pijus Kamandulis 2024-10-16 00:06:30 +03:00
parent 06d70bbd70
commit 36cef1ab04
9 changed files with 156 additions and 18 deletions

View File

@ -141,14 +141,28 @@ export const EstimationContextProvider = (props: PropsWithChildren) => {
); );
}; };
const updateTicket = async ({ id, name, content }: EditTicketRequest) => { const updateTicket = async ({
id,
name,
content,
estimate,
}: EditTicketRequest) => {
const editedTicket = currentSessionData?.tickets.find((x) => x.id === id); const editedTicket = currentSessionData?.tickets.find((x) => x.id === id);
if (!editedTicket) { if (!editedTicket) {
return; return;
} }
if (name !== undefined) {
editedTicket.name = name; editedTicket.name = name;
}
if (content !== undefined) {
editedTicket.content = content; editedTicket.content = content;
}
if (estimate !== undefined) {
editedTicket.estimate = estimate;
}
const newTicketsValue = currentSessionData?.tickets.map((x) => const newTicketsValue = currentSessionData?.tickets.map((x) =>
JSON.stringify(x), JSON.stringify(x),

View File

@ -12,6 +12,7 @@ interface EstimationSessionTicket {
id: string; id: string;
name: string; name: string;
content: string; content: string;
estimate?: string;
} }
interface SessionState { interface SessionState {

View File

@ -1,9 +1,10 @@
interface CreateTicketRequest { interface CreateTicketRequest {
name: string; name: string;
content: string; content: string;
estimate?: string;
} }
interface EditTicketRequest extends CreateTicketRequest { interface EditTicketRequest extends Partial<CreateTicketRequest> {
id: string; id: string;
} }

View File

@ -43,10 +43,12 @@ const authenticatedRoute = createRoute({
}, },
component: () => { component: () => {
return ( return (
<> <div className="flex h-screen flex-col">
<Header /> <Header />
<div className="flex-grow">
<Outlet /> <Outlet />
</> </div>
</div>
); );
}, },
}); });

View File

@ -4,10 +4,11 @@ import { getRouteApi } from '@tanstack/react-router';
import TaskSidebar from './components/TaskSidebar'; import TaskSidebar from './components/TaskSidebar';
import VoteSelection from './components/VoteSelection'; import VoteSelection from './components/VoteSelection';
import VoteList from './components/VoteList'; import VoteList from './components/VoteList';
import { Button, ButtonColor, Drawer } from '../../components'; import { Drawer } from '../../components';
import EditTicketForm from './components/EditTicketForm'; import EditTicketForm from './components/EditTicketForm';
import PlayerList from './components/PlayerList'; import PlayerList from './components/PlayerList';
import HtmlEmbed from '../../components/HtmlEmbed'; import HtmlEmbed from '../../components/HtmlEmbed';
import EstimationResult from './components/EstimationResult';
const fibonacciSequence = [1, 2, 3, 5, 8, 13, 21, 34, 55, 89, 100]; const fibonacciSequence = [1, 2, 3, 5, 8, 13, 21, 34, 55, 89, 100];
@ -44,7 +45,7 @@ const Estimation: React.FC = () => {
} = estimationState; } = estimationState;
return ( return (
<div className="flex h-screen"> <div className="flex h-full">
<TaskSidebar <TaskSidebar
className="w-64 overflow-y-scroll bg-gray-50 p-4 dark:bg-nero-800" className="w-64 overflow-y-scroll bg-gray-50 p-4 dark:bg-nero-800"
tickets={tickets} tickets={tickets}
@ -76,14 +77,20 @@ const Estimation: React.FC = () => {
<VoteList className="mt-6" revealed={revealed} votes={votes} /> <VoteList className="mt-6" revealed={revealed} votes={votes} />
<div className="mt-4"> <EstimationResult
<Button className="mt-6"
color={ButtonColor.Error} revealed={revealed}
onClick={() => setRevealed(true)} votes={votes}
> setRevealed={setRevealed}
Reveal Votes onSetEstimate={async (estimate: string) => {
</Button> await updateTicket({
</div> id: currentTicket.id,
estimate,
});
await setActiveTicket(currentTicket.id);
}}
/>
</> </>
) : ( ) : (
<p>Select a task to see the details and estimate.</p> <p>Select a task to see the details and estimate.</p>

View File

@ -6,6 +6,7 @@ import * as yup from 'yup';
interface EditTicketFormData { interface EditTicketFormData {
name: string; name: string;
estimate?: string;
content: string; content: string;
} }
@ -21,6 +22,7 @@ const EditTicketForm: React.FC<EditTicketFormProps> = ({
const form = useForm({ const form = useForm({
defaultValues: initialData ?? { defaultValues: initialData ?? {
name: '', name: '',
estimate: '',
content: '', content: '',
}, },
onSubmit: async ({ value }) => { onSubmit: async ({ value }) => {
@ -58,6 +60,18 @@ const EditTicketForm: React.FC<EditTicketFormProps> = ({
/> />
)} )}
</form.Field> </form.Field>
<form.Field name="estimate">
{(field) => (
<Input
label="Estimate"
name={field.name}
value={field.state.value}
onBlur={field.handleBlur}
onChange={(e) => field.handleChange(e.target.value)}
errors={field.state.meta.errors}
/>
)}
</form.Field>
<form.Field name="content"> <form.Field name="content">
{(field) => ( {(field) => (
<RichEditor <RichEditor

View File

@ -0,0 +1,96 @@
import { useForm } from '@tanstack/react-form';
import { Button, ButtonColor, Input } from '../../../components';
import { PlayerVote } from '../../../lib/types/entityModels';
import { yupValidator } from '@tanstack/yup-form-adapter';
import * as yup from 'yup';
interface VoteListProps {
className?: string;
votes: PlayerVote[];
revealed: boolean;
setRevealed: (revealed: boolean) => Promise<void>;
onSetEstimate: (estimate: string) => Promise<void>;
}
const EstimationResult: React.FC<VoteListProps> = ({
className,
revealed,
votes,
setRevealed,
onSetEstimate,
}) => {
const form = useForm({
defaultValues: {
estimate: '',
},
onSubmit: async ({ value }) => {
await onSetEstimate(value.estimate);
},
validators: {
onChange: yup.object({
estimate: yup.string().label('Estimate').max(10).required(),
}),
},
validatorAdapter: yupValidator(),
});
if (!revealed) {
return votes.length > 0 ? (
<div className="mt-4">
<Button color={ButtonColor.Error} onClick={() => setRevealed(true)}>
Reveal Votes
</Button>
</div>
) : null;
}
const numericVotes = votes
.map((vote) => parseFloat(vote.estimate))
.filter((vote) => !isNaN(vote));
const averageVote =
numericVotes.length > 0
? numericVotes.reduce((sum, vote) => sum + vote, 0) / numericVotes.length
: null;
return (
<div className={className}>
{averageVote !== null && (
<div className="mb-4 text-lg font-semibold">
Average Vote: {averageVote.toFixed(2)}
</div>
)}
<form
className="flex items-end gap-4"
onSubmit={(e) => {
e.preventDefault();
e.stopPropagation();
form.handleSubmit();
}}
>
<form.Field name="estimate">
{(field) => (
<Input
label="Estimate"
name={field.name}
value={field.state.value}
onBlur={field.handleBlur}
onChange={(e) => field.handleChange(e.target.value)}
errors={field.state.meta.errors}
/>
)}
</form.Field>
<form.Subscribe selector={(state) => [state.canSubmit]}>
{([canSubmit]) => (
<Button type="submit" disabled={!canSubmit}>
Save
</Button>
)}
</form.Subscribe>
</form>
</div>
);
};
export default EstimationResult;

View File

@ -25,6 +25,7 @@ const TaskSidebar: React.FC<TaskSidebarProps> = ({
<Card <Card
key={item.id} key={item.id}
title={item.name} title={item.name}
description={item.estimate && `Estimate: ${item.estimate}`}
onClick={() => onSelectTicket(item)} onClick={() => onSelectTicket(item)}
onEdit={() => onEditTicket(item.id)} onEdit={() => onEditTicket(item.id)}
/> />

View File

@ -10,7 +10,9 @@ interface VoteListProps {
const VoteList: React.FC<VoteListProps> = ({ className, votes, revealed }) => { const VoteList: React.FC<VoteListProps> = ({ className, votes, revealed }) => {
return ( return (
<div className={className}> <div className={className}>
{votes.length > 0 && (
<h2 className="mb-4 text-xl font-bold">Player Votes</h2> <h2 className="mb-4 text-xl font-bold">Player Votes</h2>
)}
<GridList <GridList
colNum={5} colNum={5}
itemComponent={({ item }, idx) => ( itemComponent={({ item }, idx) => (