Allow user to set estimate after vote
This commit is contained in:
parent
06d70bbd70
commit
36cef1ab04
|
@ -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;
|
||||||
}
|
}
|
||||||
|
|
||||||
editedTicket.name = name;
|
if (name !== undefined) {
|
||||||
editedTicket.content = content;
|
editedTicket.name = name;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (content !== undefined) {
|
||||||
|
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),
|
||||||
|
|
|
@ -12,6 +12,7 @@ interface EstimationSessionTicket {
|
||||||
id: string;
|
id: string;
|
||||||
name: string;
|
name: string;
|
||||||
content: string;
|
content: string;
|
||||||
|
estimate?: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
interface SessionState {
|
interface SessionState {
|
||||||
|
|
|
@ -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;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -43,10 +43,12 @@ const authenticatedRoute = createRoute({
|
||||||
},
|
},
|
||||||
component: () => {
|
component: () => {
|
||||||
return (
|
return (
|
||||||
<>
|
<div className="flex h-screen flex-col">
|
||||||
<Header />
|
<Header />
|
||||||
<Outlet />
|
<div className="flex-grow">
|
||||||
</>
|
<Outlet />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
|
@ -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>
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -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;
|
|
@ -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)}
|
||||||
/>
|
/>
|
||||||
|
|
|
@ -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}>
|
||||||
<h2 className="mb-4 text-xl font-bold">Player Votes</h2>
|
{votes.length > 0 && (
|
||||||
|
<h2 className="mb-4 text-xl font-bold">Player Votes</h2>
|
||||||
|
)}
|
||||||
<GridList
|
<GridList
|
||||||
colNum={5}
|
colNum={5}
|
||||||
itemComponent={({ item }, idx) => (
|
itemComponent={({ item }, idx) => (
|
||||||
|
|
Loading…
Reference in New Issue