Added house function: add, update, view (admin function)

update npm package
This commit is contained in:
2026-02-05 21:10:45 +07:00
parent 018f693998
commit 7b14b30320
104 changed files with 3447 additions and 2518 deletions

View File

@@ -1,14 +1,11 @@
import { useAppForm } from '@/hooks/use-app-form';
import { authClient } from '@/lib/auth-client';
import { m } from '@/paraglide/messages';
import {
ChangePassword,
ChangePasswordFormSchema,
} from '@/service/user.schema';
import { useAppForm } from '@hooks/use-app-form';
import { authClient } from '@lib/auth-client';
import { m } from '@paraglide/messages';
import { KeyIcon } from '@phosphor-icons/react';
import { ChangePassword, ChangePasswordFormSchema } from '@service/user.schema';
import { Card, CardContent, CardHeader, CardTitle } from '@ui/card';
import { Field, FieldGroup } from '@ui/field';
import { toast } from 'sonner';
import { Card, CardContent, CardHeader, CardTitle } from '../ui/card';
import { Field, FieldGroup } from '../ui/field';
const defaultValues: ChangePassword = {
currentPassword: '',

View File

@@ -1,18 +1,18 @@
import { useAppForm } from '@/hooks/use-app-form';
import { authClient } from '@/lib/auth-client';
import { m } from '@/paraglide/messages';
import { uploadProfileImage } from '@/service/profile.api';
import { ProfileInput, profileUpdateSchema } from '@/service/profile.schema';
import { useAuth } from '@components/auth/auth-provider';
import AvatarUser from '@components/avatar/avatar-user';
import RoleBadge from '@components/avatar/role-badge';
import { useAppForm } from '@hooks/use-app-form';
import { authClient } from '@lib/auth-client';
import { m } from '@paraglide/messages';
import { UserCircleIcon } from '@phosphor-icons/react';
import { uploadProfileImage } from '@service/profile.api';
import { ProfileInput, profileUpdateSchema } from '@service/profile.schema';
import { useQueryClient } from '@tanstack/react-query';
import { Card, CardContent, CardHeader, CardTitle } from '@ui/card';
import { Field, FieldGroup, FieldLabel } from '@ui/field';
import { Input } from '@ui/input';
import { useRef } from 'react';
import { toast } from 'sonner';
import { useAuth } from '../auth/auth-provider';
import AvatarUser from '../avatar/avatar-user';
import RoleBadge from '../avatar/role-badge';
import { Card, CardContent, CardHeader, CardTitle } from '../ui/card';
import { Field, FieldGroup, FieldLabel } from '../ui/field';
import { Input } from '../ui/input';
const defaultValues: ProfileInput = {
name: '',

View File

@@ -1,17 +1,17 @@
import { useAppForm } from '@/hooks/use-app-form';
import { m } from '@/paraglide/messages';
import { Locale, setLocale } from '@/paraglide/runtime';
import { settingQueries } from '@/service/queries';
import { updateUserSettings } from '@/service/setting.api';
import { UserSettingInput, userSettingSchema } from '@/service/setting.schema';
import { ReturnError } from '@/types/common';
import { useAppForm } from '@hooks/use-app-form';
import { m } from '@paraglide/messages';
import { Locale, setLocale } from '@paraglide/runtime';
import { GearIcon } from '@phosphor-icons/react';
import { settingQueries } from '@service/queries';
import { updateUserSettings } from '@service/setting.api';
import { UserSettingInput, userSettingSchema } from '@service/setting.schema';
import { useMutation, useQuery, useQueryClient } from '@tanstack/react-query';
import { Card, CardContent, CardHeader, CardTitle } from '@ui/card';
import { Field, FieldGroup } from '@ui/field';
import { Skeleton } from '@ui/skeleton';
import { useEffect } from 'react';
import { toast } from 'sonner';
import { Card, CardContent, CardHeader, CardTitle } from '../ui/card';
import { Field, FieldGroup } from '../ui/field';
import { Skeleton } from '../ui/skeleton';
const defaultValues: UserSettingInput = {
language: '',
@@ -35,7 +35,10 @@ const UserSettingsForm = () => {
},
onError: (error: ReturnError) => {
console.error(error);
toast.error(m.backend_message({ code: error.code }), {
const code = error.code as Parameters<
typeof m.backend_message
>[0]['code'];
toast.error(m.backend_message({ code }), {
richColors: true,
});
},

View File

@@ -1,12 +1,13 @@
import { useFieldContext, useFormContext } from '@/hooks/use-app-form';
import { RoleEnum } from '@/service/user.schema';
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';
import { Input } from '@ui/input';
import * as ShadcnSelect from '@ui/select';
import { SelectUser as SelectUserUI } from '@ui/select-user';
import { Textarea } from '@ui/textarea';
import { type VariantProps } from 'class-variance-authority';
import { Button, buttonVariants } from '../ui/button';
import { Field, FieldError, FieldLabel } from '../ui/field';
import { Input } from '../ui/input';
import * as ShadcnSelect from '../ui/select';
import { Textarea } from '../ui/textarea';
export function SubscribeButton({
label,
@@ -216,3 +217,43 @@ export function SelectNumber({
</Field>
);
}
export function SelectUser({
label,
values,
placeholder,
keyword,
onKeywordChange,
searchPlaceholder = 'Tìm theo tên hoặc email...',
}: {
label: string;
values: Array<{ id: string; name: string; email: string }>;
placeholder?: string;
/** Khi truyền cùng onKeywordChange: tìm kiếm theo API (keyword gửi lên server) */
keyword?: string;
onKeywordChange?: (value: string) => void;
searchPlaceholder?: string;
}) {
const field = useFieldContext<string>();
const errors = useStore(field.store, (state) => state.meta.errors);
const isInvalid = field.state.meta.isTouched && !field.state.meta.isValid;
return (
<Field data-invalid={isInvalid} className="col-span-2">
<FieldLabel htmlFor={field.name}>{label}:</FieldLabel>
<SelectUserUI
name={field.name}
id={field.name}
value={field.state.value}
onValueChange={(userId) => field.handleChange(userId)}
values={values}
placeholder={placeholder}
keyword={keyword}
onKeywordChange={onKeywordChange}
searchPlaceholder={searchPlaceholder}
aria-invalid={isInvalid}
/>
{isInvalid && <FieldError errors={errors} />}
</Field>
);
}

View File

@@ -0,0 +1,125 @@
import { ReturnError } from '@/types/common';
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 { createHouse } from '@service/house.api';
import { houseCreateSchema } from '@service/house.schema';
import { housesQueries, usersQueries } from '@service/queries';
import { uuid } from '@tanstack/react-form';
import { useMutation, useQuery, useQueryClient } from '@tanstack/react-query';
import { Button } from '@ui/button';
import { DialogClose, DialogFooter } from '@ui/dialog';
import { Field, FieldGroup } from '@ui/field';
import { slugify } from '@utils/helper';
import { useState } from 'react';
import { toast } from 'sonner';
type FormProps = {
onSubmit: (open: boolean) => void;
};
const CreateNewHouseForm = ({ onSubmit }: FormProps) => {
const [userKeyword, setUserKeyword] = useState('');
const debouncedUserKeyword = useDebounced(userKeyword, 300);
const { data: users } = useQuery(
usersQueries.select({ keyword: debouncedUserKeyword }),
);
const queryClient = useQueryClient();
const { mutate: createHouseMutation } = useMutation({
mutationFn: createHouse,
onSuccess: () => {
queryClient.invalidateQueries({
queryKey: [...housesQueries.all, 'list'],
});
onSubmit(false);
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: {
name: '',
userId: '',
color: '#000000',
},
validators: {
onChange: houseCreateSchema,
onSubmit: houseCreateSchema,
},
onSubmit: async ({ value }) => {
const slug = `${slugify(value.name)}-${uuid().slice(0, 5)}`;
const { data, error } = await authClient.organization.checkSlug({
slug,
});
if (error) {
toast.error(error.message, {
richColors: true,
});
}
if (data?.status) {
createHouseMutation({ data: { ...value, slug } });
}
},
});
return (
<form
id="admin-create-house-form"
onSubmit={(e) => {
e.preventDefault();
e.stopPropagation();
form.handleSubmit();
}}
>
<FieldGroup>
<form.AppField name="name">
{(field) => <field.TextField label={m.houses_page_form_name()} />}
</form.AppField>
<form.AppField name="color">
{(field) => (
<field.TextField type="color" label={m.houses_page_form_color()} />
)}
</form.AppField>
<form.AppField name="userId">
{(field) => (
<field.SelectUser
label={m.houses_page_form_create_for()}
values={users ?? []}
placeholder="Chọn người dùng"
keyword={userKeyword}
onKeywordChange={setUserKeyword}
/>
)}
</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()} />
</form.AppForm>
</DialogFooter>
</Field>
</FieldGroup>
</form>
);
};
export default CreateNewHouseForm;

View File

@@ -0,0 +1,116 @@
import { ReturnError } from '@/types/common';
import { useAppForm } from '@hooks/use-app-form';
import { authClient } from '@lib/auth-client';
import { m } from '@paraglide/messages';
import { updateHouse } from '@service/house.api';
import { houseEditSchema } from '@service/house.schema';
import { housesQueries } from '@service/queries';
import { uuid } from '@tanstack/react-form';
import { useMutation, useQueryClient } from '@tanstack/react-query';
import { Button } from '@ui/button';
import { DialogClose, DialogFooter } from '@ui/dialog';
import { Field, FieldGroup } from '@ui/field';
import { slugify } from '@utils/helper';
import { toast } from 'sonner';
type EditHouseFormProps = {
data: OrganizationWithMembers;
onSubmit: (open: boolean) => void;
};
const EditHouseForm = ({ data, onSubmit }: EditHouseFormProps) => {
const queryClient = useQueryClient();
const { mutate: updateHouseMutation } = useMutation({
mutationFn: updateHouse,
onSuccess: () => {
queryClient.invalidateQueries({
queryKey: [...housesQueries.all, 'list'],
});
onSubmit(false);
toast.success(m.houses_page_message_update_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: {
id: data.id,
name: data.name,
color: data.color || '#000000',
},
validators: {
onChange: houseEditSchema,
onSubmit: houseEditSchema,
},
onSubmit: async ({ value }) => {
const slug = `${slugify(value.name)}-${uuid().slice(0, 5)}`;
const { data, error } = await authClient.organization.checkSlug({
slug,
});
if (error) {
toast.error(error.message, {
richColors: true,
});
}
if (data?.status) {
updateHouseMutation({ data: { ...value, slug } });
}
},
});
return (
<form
id="admin-create-house-form"
onSubmit={(e) => {
e.preventDefault();
e.stopPropagation();
form.handleSubmit();
}}
>
<FieldGroup>
<form.AppField name="name">
{(field) => <field.TextField label={m.houses_page_form_name()} />}
</form.AppField>
<form.AppField name="color">
{(field) => (
<field.TextField type="color" label={m.houses_page_form_color()} />
)}
</form.AppField>
<div className="flex flex-row items-center gap-2">
<span className="font-medium">{m.houses_page_form_user()}:</span>
<span>
{
data.members.filter((member) => member.role === 'owner')[0].user
.name
}
</span>
</div>
<Field>
<DialogFooter>
<DialogClose asChild>
<Button variant="outline" type="button">
{m.ui_cancel_btn()}
</Button>
</DialogClose>
<form.AppForm>
<form.SubscribeButton label={m.ui_confirm_btn()} />
</form.AppForm>
</DialogFooter>
</Field>
</FieldGroup>
</form>
);
};
export default EditHouseForm;

View File

@@ -1,15 +1,15 @@
import { useAppForm } from '@/hooks/use-app-form';
import { m } from '@/paraglide/messages';
import { settingQueries } from '@/service/queries';
import { updateAdminSettings } from '@/service/setting.api';
import { settingSchema, SettingsInput } from '@/service/setting.schema';
import { ReturnError } from '@/types/common';
import { useAppForm } from '@hooks/use-app-form';
import { m } from '@paraglide/messages';
import { GearIcon } from '@phosphor-icons/react';
import { settingQueries } from '@service/queries';
import { updateAdminSettings } from '@service/setting.api';
import { settingSchema, SettingsInput } from '@service/setting.schema';
import { useMutation, useQuery, useQueryClient } from '@tanstack/react-query';
import { Card, CardContent, CardHeader, CardTitle } from '@ui/card';
import { Field, FieldGroup } from '@ui/field';
import { Skeleton } from '@ui/skeleton';
import { toast } from 'sonner';
import { Card, CardContent, CardHeader, CardTitle } from '../ui/card';
import { Field, FieldGroup } from '../ui/field';
import { Skeleton } from '../ui/skeleton';
const defaultValues: SettingsInput = {
site_name: '',
@@ -32,9 +32,10 @@ const SettingsForm = () => {
},
onError: (error: ReturnError) => {
console.error(error);
toast.error(m.backend_message({ code: error.code }), {
richColors: true,
});
const code = error.code as Parameters<
typeof m.backend_message
>[0]['code'];
toast.error(m.backend_message({ code }), { richColors: true });
},
});

View File

@@ -1,13 +1,13 @@
import { useAppForm } from '@/hooks/use-app-form';
import { authClient } from '@/lib/auth-client';
import { m } from '@/paraglide/messages';
import { useAppForm } from '@hooks/use-app-form';
import { authClient } from '@lib/auth-client';
import { m } from '@paraglide/messages';
import { useQueryClient } from '@tanstack/react-query';
import { createLink, useNavigate } from '@tanstack/react-router';
import { Button } from '@ui/button';
import { Card, CardContent, CardHeader, CardTitle } from '@ui/card';
import { Field, FieldGroup } from '@ui/field';
import { toast } from 'sonner';
import z from 'zod';
import { Button } from '../ui/button';
import { Card, CardContent, CardHeader, CardTitle } from '../ui/card';
import { Field, FieldGroup } from '../ui/field';
const SignInFormSchema = z.object({
email: z

View File

@@ -1,15 +1,15 @@
import { m } from '@/paraglide/messages';
import { m } from '@paraglide/messages';
import { createLink, Link } from '@tanstack/react-router';
import { Button } from '../ui/button';
import { Button } from '@ui/button';
import {
Card,
CardContent,
CardDescription,
CardHeader,
CardTitle,
} from '../ui/card';
import { Field, FieldDescription, FieldGroup, FieldLabel } from '../ui/field';
import { Input } from '../ui/input';
} from '@ui/card';
import { Field, FieldDescription, FieldGroup, FieldLabel } from '@ui/field';
import { Input } from '@ui/input';
const ButtonLink = createLink(Button);

View File

@@ -1,13 +1,13 @@
import { useAppForm } from '@/hooks/use-app-form';
import { m } from '@/paraglide/messages';
import { userBanSchema } from '@/service/user.schema';
import { useBanContext } from '@components/user/ban-user-dialog';
import { useAppForm } from '@hooks/use-app-form';
import { m } from '@paraglide/messages';
import { WarningIcon } from '@phosphor-icons/react';
import { userBanSchema } from '@service/user.schema';
import { Alert, AlertDescription, AlertTitle } from '@ui/alert';
import { Button } from '@ui/button';
import { DialogClose, DialogFooter } from '@ui/dialog';
import { Field, FieldGroup } from '@ui/field';
import { UserWithRole } from 'better-auth/plugins';
import { Alert, AlertDescription, AlertTitle } from '../ui/alert';
import { Button } from '../ui/button';
import { DialogClose, DialogFooter } from '../ui/dialog';
import { Field, FieldGroup } from '../ui/field';
import { useBanContext } from '../user/ban-user-dialog';
type FormProps = {
data: UserWithRole;

View File

@@ -1,14 +1,14 @@
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 { ReturnError } from '@/types/common';
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 { useMutation, useQueryClient } from '@tanstack/react-query';
import { Button } from '@ui/button';
import { DialogClose, DialogFooter } from '@ui/dialog';
import { Field, FieldGroup } from '@ui/field';
import { toast } from 'sonner';
import { Button } from '../ui/button';
import { DialogClose, DialogFooter } from '../ui/dialog';
import { Field, FieldGroup } from '../ui/field';
type FormProps = {
onSubmit: (open: boolean) => void;
@@ -30,7 +30,10 @@ const AdminCreateUserForm = ({ onSubmit }: FormProps) => {
},
onError: (error: ReturnError) => {
console.error(error);
toast.error(m.backend_message({ code: error.code }), {
const code = error.code as Parameters<
typeof m.backend_message
>[0]['code'];
toast.error(m.backend_message({ code }), {
richColors: true,
});
},

View File

@@ -1,15 +1,15 @@
import { useAppForm } from '@/hooks/use-app-form';
import { m } from '@/paraglide/messages';
import { usersQueries } from '@/service/queries';
import { setUserPassword } from '@/service/user.api';
import { userSetPasswordSchema } from '@/service/user.schema';
import { Button } from '@/components/ui/button';
import { ReturnError } from '@/types/common';
import { useAppForm } from '@hooks/use-app-form';
import { m } from '@paraglide/messages';
import { usersQueries } from '@service/queries';
import { setUserPassword } from '@service/user.api';
import { userSetPasswordSchema } from '@service/user.schema';
import { useMutation, useQueryClient } from '@tanstack/react-query';
import { DialogClose, DialogFooter } from '@ui/dialog';
import { Field, FieldGroup } from '@ui/field';
import { UserWithRole } from 'better-auth/plugins';
import { toast } from 'sonner';
import { Button } from '../ui/button';
import { DialogClose, DialogFooter } from '../ui/dialog';
import { Field, FieldGroup } from '../ui/field';
type FormProps = {
data: UserWithRole;
@@ -32,7 +32,10 @@ const AdminSetPasswordForm = ({ data, onSubmit }: FormProps) => {
},
onError: (error: ReturnError) => {
console.error(error);
toast.error(m.backend_message({ code: error.code }), {
const code = error.code as Parameters<
typeof m.backend_message
>[0]['code'];
toast.error(m.backend_message({ code }), {
richColors: true,
});
},

View File

@@ -1,15 +1,15 @@
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 { ReturnError } from '@/types/common';
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 { useMutation, useQueryClient } from '@tanstack/react-query';
import { Button } from '@ui/button';
import { DialogClose, DialogFooter } from '@ui/dialog';
import { Field, FieldGroup } from '@ui/field';
import { UserWithRole } from 'better-auth/plugins';
import { toast } from 'sonner';
import { Button } from '../ui/button';
import { DialogClose, DialogFooter } from '../ui/dialog';
import { Field, FieldGroup } from '../ui/field';
type SetRoleFormProps = {
data: UserWithRole;
@@ -37,7 +37,10 @@ const AdminSetUserRoleForm = ({ data, onSubmit }: SetRoleFormProps) => {
},
onError: (error: ReturnError) => {
console.error(error);
toast.error(m.backend_message({ code: error.code }), {
const code = error.code as Parameters<
typeof m.backend_message
>[0]['code'];
toast.error(m.backend_message({ code }), {
richColors: true,
});
},

View File

@@ -1,15 +1,15 @@
import { useAppForm } from '@/hooks/use-app-form';
import { m } from '@/paraglide/messages';
import { usersQueries } from '@/service/queries';
import { updateUserInformation } from '@/service/user.api';
import { userUpdateInfoSchema } from '@/service/user.schema';
import { ReturnError } from '@/types/common';
import { useAppForm } from '@hooks/use-app-form';
import { m } from '@paraglide/messages';
import { usersQueries } from '@service/queries';
import { updateUserInformation } from '@service/user.api';
import { userUpdateInfoSchema } from '@service/user.schema';
import { useMutation, useQueryClient } from '@tanstack/react-query';
import { Button } from '@ui/button';
import { DialogClose, DialogFooter } from '@ui/dialog';
import { Field, FieldGroup } from '@ui/field';
import { UserWithRole } from 'better-auth/plugins';
import { toast } from 'sonner';
import { Button } from '../ui/button';
import { DialogClose, DialogFooter } from '../ui/dialog';
import { Field, FieldGroup } from '../ui/field';
type UpdateUserFormProps = {
data: UserWithRole;
@@ -32,7 +32,10 @@ const AdminUpdateUserInfoForm = ({ data, onSubmit }: UpdateUserFormProps) => {
},
onError: (error: ReturnError) => {
console.error(error);
toast.error(m.backend_message({ code: error.code }), {
const code = error.code as Parameters<
typeof m.backend_message
>[0]['code'];
toast.error(m.backend_message({ code }), {
richColors: true,
});
},