invite member to house

This commit is contained in:
2026-02-11 22:45:33 +07:00
parent 5ffdd7454a
commit ea31b61cac
27 changed files with 545 additions and 61 deletions

View File

@@ -1,5 +1,4 @@
import { useFieldContext, useFormContext } from '@hooks/use-app-form';
import { RoleEnum } from '@service/user.schema';
import { useStore } from '@tanstack/react-form';
import { Button, buttonVariants } from '@ui/button';
import { Field, FieldError, FieldLabel } from '@ui/field';
@@ -146,12 +145,11 @@ export function Select({
label,
values,
placeholder,
isRole = false,
// isRole = false,
}: {
label: string;
values: Array<{ label: string; value: string }>;
placeholder?: string;
isRole?: boolean;
}) {
const field = useFieldContext<string>();
const errors = useStore(field.store, (state) => state.meta.errors);
@@ -163,11 +161,7 @@ export function Select({
<ShadcnSelect.Select
name={field.name}
value={String(field.state.value)}
onValueChange={(value) =>
isRole
? field.handleChange(RoleEnum.parse(value))
: field.handleChange(value)
}
onValueChange={(value) => field.handleChange(value)}
>
<ShadcnSelect.SelectTrigger aria-invalid={isInvalid}>
<ShadcnSelect.SelectValue placeholder={placeholder} />
@@ -235,6 +229,7 @@ export function SelectUser({
keyword,
onKeywordChange,
searchPlaceholder = 'Tìm theo tên hoặc email...',
selectKey = 'id',
}: {
label: string;
values: Array<{ id: string; name: string; email: string }>;
@@ -243,6 +238,7 @@ export function SelectUser({
keyword?: string;
onKeywordChange?: (value: string) => void;
searchPlaceholder?: string;
selectKey?: 'id' | 'email';
}) {
const field = useFieldContext<string>();
const errors = useStore(field.store, (state) => state.meta.errors);
@@ -262,6 +258,7 @@ export function SelectUser({
onKeywordChange={onKeywordChange}
searchPlaceholder={searchPlaceholder}
aria-invalid={isInvalid}
selectKey={selectKey}
/>
{isInvalid && <FieldError errors={errors} />}
</Field>

View File

@@ -84,7 +84,6 @@ const CreateNewHouseForm = ({ onSubmit, isPersonal = false }: FormProps) => {
if (isPersonal) {
form.setFieldValue('userId', session.user.id);
}
console.log(isPending);
}, []);
return (
@@ -111,7 +110,8 @@ const CreateNewHouseForm = ({ onSubmit, isPersonal = false }: FormProps) => {
<field.SelectUser
label={m.houses_page_form_create_for()}
values={users ?? []}
placeholder="Chọn người dùng"
placeholder={m.houses_page_form_user_select_placeholder()}
searchPlaceholder={m.houses_page_form_user_select_search_placeholder()}
keyword={userKeyword}
onKeywordChange={setUserKeyword}
/>

View File

@@ -0,0 +1,132 @@
import { Button } from '@/components/ui/button';
import { DialogClose, DialogFooter } from '@/components/ui/dialog';
import { useAppForm } from '@/hooks/use-app-form';
import useDebounced from '@/hooks/use-debounced';
import { authClient } from '@/lib/auth-client';
import { m } from '@/paraglide/messages';
import { invitationMember } from '@/service/house.api';
import {
invitationCreateBESchema,
invitationCreateFESchema,
} from '@/service/house.schema';
import { housesQueries, usersQueries } from '@/service/queries';
import { useMutation, useQuery, useQueryClient } from '@tanstack/react-query';
import { Field, FieldGroup, FieldLabel } from '@ui/field';
import { useState } from 'react';
import { toast } from 'sonner';
type FormProps = {
onSubmit: (open: boolean) => void;
};
const UserInviteMemberForm = ({ onSubmit }: FormProps) => {
const { data: activeHouse, refetch } = authClient.useActiveOrganization();
const [userKeyword, setUserKeyword] = useState('');
const debouncedUserKeyword = useDebounced(userKeyword, 300);
const { data: users } = useQuery(
usersQueries.select({ keyword: debouncedUserKeyword }, true),
);
const queryClient = useQueryClient();
if (!activeHouse) return null;
const { mutate: invitationMemberMutation, isPending } = useMutation({
mutationFn: invitationMember,
onSuccess: () => {
queryClient.invalidateQueries({
queryKey: [...housesQueries.all, 'currentUser'],
});
onSubmit(false);
refetch();
toast.success(m.houses_page_message_create_house_success(), {
richColors: true,
});
},
onError: (error: ReturnError) => {
console.error(error);
const code = error.code as Parameters<
typeof m.backend_message
>[0]['code'];
toast.error(m.backend_message({ code }), {
richColors: true,
});
},
});
const form = useAppForm({
defaultValues: {
email: '',
houseId: activeHouse.id,
role: '',
},
validators: {
onChange: invitationCreateFESchema,
onSubmit: invitationCreateFESchema,
},
onSubmit: async ({ value }) => {
invitationMemberMutation({ data: invitationCreateBESchema.parse(value) });
},
});
return (
<form
id="user-invite-member-form"
onSubmit={(e) => {
e.preventDefault();
e.stopPropagation();
form.handleSubmit();
}}
>
<FieldGroup>
<Field>
<FieldLabel htmlFor="house_name">
{m.houses_page_form_name()}:
</FieldLabel>
<div className="flex gap-2">{activeHouse.name}</div>
</Field>
<form.AppField name="email">
{(field) => (
<field.SelectUser
label={m.houses_page_form_user()}
values={users ?? []}
placeholder={m.houses_page_form_user_select_placeholder()}
searchPlaceholder={m.houses_page_form_user_select_search_placeholder()}
keyword={userKeyword}
onKeywordChange={setUserKeyword}
selectKey="email"
/>
)}
</form.AppField>
<form.AppField name="role">
{(field) => (
<field.Select
label={m.profile_form_role()}
placeholder={m.users_page_ui_select_placeholder_role()}
values={[
{ value: 'owner', label: m.role_tags({ role: 'owner' }) },
{ value: 'admin', label: m.role_tags({ role: 'admin' }) },
{ value: 'member', label: m.role_tags({ role: 'member' }) },
]}
/>
)}
</form.AppField>
<Field>
<DialogFooter>
<DialogClose asChild>
<Button variant="outline" type="button">
{m.ui_cancel_btn()}
</Button>
</DialogClose>
<form.AppForm>
<form.SubscribeButton
label={m.ui_confirm_btn()}
disabled={isPending}
/>
</form.AppForm>
</DialogFooter>
</Field>
</FieldGroup>
</form>
);
};
export default UserInviteMemberForm;

View File

@@ -2,7 +2,7 @@ import { useAppForm } from '@hooks/use-app-form';
import { m } from '@paraglide/messages';
import { usersQueries } from '@service/queries';
import { createUser } from '@service/user.api';
import { userCreateSchema } from '@service/user.schema';
import { userCreateBESchema, userCreateFESchema } from '@service/user.schema';
import { useMutation, useQueryClient } from '@tanstack/react-query';
import { Button } from '@ui/button';
import { DialogClose, DialogFooter } from '@ui/dialog';
@@ -46,11 +46,11 @@ const AdminCreateUserForm = ({ onSubmit }: FormProps) => {
role: '',
},
validators: {
onChange: userCreateSchema,
onSubmit: userCreateSchema,
onChange: userCreateFESchema,
onSubmit: userCreateFESchema,
},
onSubmit: ({ value }) => {
createUserMutation({ data: userCreateSchema.parse(value) });
createUserMutation({ data: userCreateBESchema.parse(value) });
},
});
@@ -83,7 +83,6 @@ const AdminCreateUserForm = ({ onSubmit }: FormProps) => {
<field.Select
label={m.profile_form_role()}
placeholder={m.users_page_ui_select_placeholder_role()}
isRole
values={[
{ value: 'admin', label: m.role_tags({ role: 'admin' }) },
{ value: 'user', label: m.role_tags({ role: 'user' }) },

View File

@@ -2,7 +2,10 @@ import { useAppForm } from '@hooks/use-app-form';
import { m } from '@paraglide/messages';
import { usersQueries } from '@service/queries';
import { setUserRole } from '@service/user.api';
import { userUpdateRoleSchema } from '@service/user.schema';
import {
userUpdateRoleBESchema,
userUpdateRoleSchema,
} from '@service/user.schema';
import { useMutation, useQueryClient } from '@tanstack/react-query';
import { Button } from '@ui/button';
import { DialogClose, DialogFooter } from '@ui/dialog';
@@ -52,7 +55,7 @@ const AdminSetUserRoleForm = ({ data, onSubmit }: SetRoleFormProps) => {
onSubmit: userUpdateRoleSchema,
},
onSubmit: async ({ value }) => {
updateRoleMutation({ data: value });
updateRoleMutation({ data: userUpdateRoleBESchema.parse(value) });
},
});