Files
CASAN/apps/okr/frontend/src/pages/CreateObjective.tsx
T
2026-07-11 15:56:31 +09:00

75 lines
3.8 KiB
TypeScript

import { useMutation, useQuery, useQueryClient } from '@tanstack/react-query';
import { useState } from 'react';
import { useForm } from 'react-hook-form';
import { useNavigate } from 'react-router-dom';
import { Button } from '../components/ui/Button.js';
import { createObjective, listUsers } from '../lib/api.js';
import { createObjectiveSchema, type CreateObjectiveFormData } from '../schemas/objective.schema.js';
export function CreateObjective(): JSX.Element {
const navigate = useNavigate();
const queryClient = useQueryClient();
const [fieldErrors, setFieldErrors] = useState<Partial<Record<keyof CreateObjectiveFormData, string>>>({});
const { register, handleSubmit } = useForm<CreateObjectiveFormData>({
defaultValues: { title: '', description: '', quarter: 'Q2/2026' },
});
const { data: users = [] } = useQuery({ queryKey: ['users'], queryFn: listUsers });
const mutation = useMutation({
mutationFn: createObjective,
onSuccess: async (objective) => {
await queryClient.invalidateQueries({ queryKey: ['objectives'] });
navigate(`/objectives/${objective.id}`);
},
});
const onSubmit = handleSubmit((values) => {
const parsed = createObjectiveSchema.safeParse(values);
if (!parsed.success) {
setFieldErrors(Object.fromEntries(parsed.error.issues.map((issue) => [issue.path[0], issue.message])));
return;
}
setFieldErrors({});
mutation.mutate(parsed.data);
});
return (
<section className="rounded-xl border border-gray-200 bg-white p-6 shadow-sm">
<h1 className="mb-6 text-xl font-semibold text-gray-800">Create Objective</h1>
<form className="max-w-2xl space-y-4" onSubmit={onSubmit}>
<label className="block">
<span className="mb-1 block text-sm font-medium text-gray-700">Title</span>
<input className="w-full rounded-lg border border-gray-300 px-3 py-2 text-sm focus:outline-none focus:ring-2 focus:ring-blue-500" {...register('title')} />
{fieldErrors.title !== undefined && <span className="mt-1 block text-sm text-orange-600">{fieldErrors.title}</span>}
</label>
<label className="block">
<span className="mb-1 block text-sm font-medium text-gray-700">Description</span>
<textarea rows={4} className="w-full rounded-lg border border-gray-300 px-3 py-2 text-sm focus:outline-none focus:ring-2 focus:ring-blue-500" {...register('description')} />
</label>
<div className="grid gap-4 md:grid-cols-2">
<label className="block">
<span className="mb-1 block text-sm font-medium text-gray-700">Owner</span>
<select className="w-full rounded-lg border border-gray-300 bg-white px-3 py-2 text-sm focus:outline-none focus:ring-2 focus:ring-blue-500" {...register('ownerId')}>
<option value="">Select owner</option>
{users.map((user) => (
<option key={user.id} value={user.id}>
{user.name}
</option>
))}
</select>
{fieldErrors.ownerId !== undefined && <span className="mt-1 block text-sm text-orange-600">{fieldErrors.ownerId}</span>}
</label>
<label className="block">
<span className="mb-1 block text-sm font-medium text-gray-700">Quarter</span>
<input className="w-full rounded-lg border border-gray-300 px-3 py-2 text-sm focus:outline-none focus:ring-2 focus:ring-blue-500" {...register('quarter')} />
{fieldErrors.quarter !== undefined && <span className="mt-1 block text-sm text-orange-600">{fieldErrors.quarter}</span>}
</label>
</div>
{mutation.isError && <div className="rounded-lg bg-orange-100 px-3 py-2 text-sm text-orange-700">Unable to create objective.</div>}
<Button type="submit" disabled={mutation.isPending}>
Save
</Button>
</form>
</section>
);
}