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);
|
||||
if (!editedTicket) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (name !== undefined) {
|
||||
editedTicket.name = name;
|
||||
}
|
||||
|
||||
if (content !== undefined) {
|
||||
editedTicket.content = content;
|
||||
}
|
||||
|
||||
if (estimate !== undefined) {
|
||||
editedTicket.estimate = estimate;
|
||||
}
|
||||
|
||||
const newTicketsValue = currentSessionData?.tickets.map((x) =>
|
||||
JSON.stringify(x),
|
||||
|
|
|
@ -12,6 +12,7 @@ interface EstimationSessionTicket {
|
|||
id: string;
|
||||
name: string;
|
||||
content: string;
|
||||
estimate?: string;
|
||||
}
|
||||
|
||||
interface SessionState {
|
||||
|
|
|
@ -1,9 +1,10 @@
|
|||
interface CreateTicketRequest {
|
||||
name: string;
|
||||
content: string;
|
||||
estimate?: string;
|
||||
}
|
||||
|
||||
interface EditTicketRequest extends CreateTicketRequest {
|
||||
interface EditTicketRequest extends Partial<CreateTicketRequest> {
|
||||
id: string;
|
||||
}
|
||||
|
||||
|
|
|
@ -43,10 +43,12 @@ const authenticatedRoute = createRoute({
|
|||
},
|
||||
component: () => {
|
||||
return (
|
||||
<>
|
||||
<div className="flex h-screen flex-col">
|
||||
<Header />
|
||||
<div className="flex-grow">
|
||||
<Outlet />
|
||||
</>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
},
|
||||
});
|
||||
|
|
|
@ -4,10 +4,11 @@ import { getRouteApi } from '@tanstack/react-router';
|
|||
import TaskSidebar from './components/TaskSidebar';
|
||||
import VoteSelection from './components/VoteSelection';
|
||||
import VoteList from './components/VoteList';
|
||||
import { Button, ButtonColor, Drawer } from '../../components';
|
||||
import { Drawer } from '../../components';
|
||||
import EditTicketForm from './components/EditTicketForm';
|
||||
import PlayerList from './components/PlayerList';
|
||||
import HtmlEmbed from '../../components/HtmlEmbed';
|
||||
import EstimationResult from './components/EstimationResult';
|
||||
|
||||
const fibonacciSequence = [1, 2, 3, 5, 8, 13, 21, 34, 55, 89, 100];
|
||||
|
||||
|
@ -44,7 +45,7 @@ const Estimation: React.FC = () => {
|
|||
} = estimationState;
|
||||
|
||||
return (
|
||||
<div className="flex h-screen">
|
||||
<div className="flex h-full">
|
||||
<TaskSidebar
|
||||
className="w-64 overflow-y-scroll bg-gray-50 p-4 dark:bg-nero-800"
|
||||
tickets={tickets}
|
||||
|
@ -76,14 +77,20 @@ const Estimation: React.FC = () => {
|
|||
|
||||
<VoteList className="mt-6" revealed={revealed} votes={votes} />
|
||||
|
||||
<div className="mt-4">
|
||||
<Button
|
||||
color={ButtonColor.Error}
|
||||
onClick={() => setRevealed(true)}
|
||||
>
|
||||
Reveal Votes
|
||||
</Button>
|
||||
</div>
|
||||
<EstimationResult
|
||||
className="mt-6"
|
||||
revealed={revealed}
|
||||
votes={votes}
|
||||
setRevealed={setRevealed}
|
||||
onSetEstimate={async (estimate: string) => {
|
||||
await updateTicket({
|
||||
id: currentTicket.id,
|
||||
estimate,
|
||||
});
|
||||
|
||||
await setActiveTicket(currentTicket.id);
|
||||
}}
|
||||
/>
|
||||
</>
|
||||
) : (
|
||||
<p>Select a task to see the details and estimate.</p>
|
||||
|
|
|
@ -6,6 +6,7 @@ import * as yup from 'yup';
|
|||
|
||||
interface EditTicketFormData {
|
||||
name: string;
|
||||
estimate?: string;
|
||||
content: string;
|
||||
}
|
||||
|
||||
|
@ -21,6 +22,7 @@ const EditTicketForm: React.FC<EditTicketFormProps> = ({
|
|||
const form = useForm({
|
||||
defaultValues: initialData ?? {
|
||||
name: '',
|
||||
estimate: '',
|
||||
content: '',
|
||||
},
|
||||
onSubmit: async ({ value }) => {
|
||||
|
@ -58,6 +60,18 @@ const EditTicketForm: React.FC<EditTicketFormProps> = ({
|
|||
/>
|
||||
)}
|
||||
</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">
|
||||
{(field) => (
|
||||
<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
|
||||
key={item.id}
|
||||
title={item.name}
|
||||
description={item.estimate && `Estimate: ${item.estimate}`}
|
||||
onClick={() => onSelectTicket(item)}
|
||||
onEdit={() => onEditTicket(item.id)}
|
||||
/>
|
||||
|
|
|
@ -10,7 +10,9 @@ interface VoteListProps {
|
|||
const VoteList: React.FC<VoteListProps> = ({ className, votes, revealed }) => {
|
||||
return (
|
||||
<div className={className}>
|
||||
{votes.length > 0 && (
|
||||
<h2 className="mb-4 text-xl font-bold">Player Votes</h2>
|
||||
)}
|
||||
<GridList
|
||||
colNum={5}
|
||||
itemComponent={({ item }, idx) => (
|
||||
|
|
Loading…
Reference in New Issue