add function for user
- create house - edit house - delete house - list all member for active house
This commit is contained in:
@@ -49,6 +49,7 @@
|
||||
"ui_delete_btn": "Delete",
|
||||
"ui_ban_btn": "Lock",
|
||||
"ui_unban_btn": "Unlock",
|
||||
"ui_invite_btn": "Invite",
|
||||
"ui_update_password_btn": "Set password",
|
||||
"ui_change_role_btn": "Set role",
|
||||
"ui_edit_user_btn": "Edit User",
|
||||
@@ -58,6 +59,7 @@
|
||||
"ui_change_password_btn": "Change password",
|
||||
"nav_label_management": "Management",
|
||||
"nav_label_basic": "Basic",
|
||||
"nav_label_kanri": "Administrator",
|
||||
"nav_home": "Home",
|
||||
"nav_dashboard": "Dashboard",
|
||||
"nav_settings": "Settings",
|
||||
@@ -166,6 +168,9 @@
|
||||
"houses_page_message_house_not_found": "House not found!",
|
||||
"houses_page_message_update_house_success": "Updated house successfully!",
|
||||
"houses_page_message_delete_house_success": "Delete house successfully!",
|
||||
"houses_page_house_active_btn": "Active",
|
||||
"houses_user_page_message_active_house_success": "Active \"<b>{house}</b>\" successfully!",
|
||||
"houses_user_page_block_action_title": "Action",
|
||||
"backend_message": [
|
||||
{
|
||||
"match": {
|
||||
@@ -174,7 +179,8 @@
|
||||
"code=YOU_ARE_NOT_ALLOWED_TO_CHANGE_USERS_ROLE": "You are not allowed to change users role",
|
||||
"code=USER_ALREADY_EXISTS_USE_ANOTHER_EMAIL": "Email already exists. Please choose another email!",
|
||||
"code=BANNED_USER": "Your account get banned, please contact administrator for more information!",
|
||||
"code=VALIDATION_ERROR": "Some field value invalid!"
|
||||
"code=VALIDATION_ERROR": "Some field value invalid!",
|
||||
"code=USER_IS_NOT_A_MEMBER_OF_THE_ORGANIZATION": "User is not a member of the house"
|
||||
}
|
||||
}
|
||||
]
|
||||
|
||||
@@ -49,6 +49,7 @@
|
||||
"ui_delete_btn": "Xóa",
|
||||
"ui_ban_btn": "Khóa",
|
||||
"ui_unban_btn": "Mở khóa",
|
||||
"ui_invite_btn": "Mời",
|
||||
"ui_update_password_btn": "Đặt lại mật khẩu",
|
||||
"ui_change_role_btn": "Đặt lại quyền hạn",
|
||||
"ui_edit_user_btn": "Chỉnh sửa người dùng",
|
||||
@@ -59,6 +60,7 @@
|
||||
"ui_edit_house_btn": "Chỉnh sửa nhà",
|
||||
"nav_label_management": "Quản lý",
|
||||
"nav_label_basic": "Cơ bản",
|
||||
"nav_label_kanri": "Quản trị viên",
|
||||
"nav_home": "Trang chủ",
|
||||
"nav_dashboard": "Bảng điều khiển",
|
||||
"nav_settings": "Cài đặt",
|
||||
@@ -167,6 +169,9 @@
|
||||
"houses_page_message_house_not_found": "Không tìm thấy nhà này!",
|
||||
"houses_page_message_update_house_success": "Cập nhật nhà thành công!",
|
||||
"houses_page_message_delete_house_success": "Xóa nhà thành công!",
|
||||
"houses_page_house_active_btn": "Kích hoạt",
|
||||
"houses_user_page_message_active_house_success": "Kích hoạt \"<b>{house}</b>\" thành công!",
|
||||
"houses_user_page_block_action_title": "Hành động",
|
||||
"backend_message": [
|
||||
{
|
||||
"match": {
|
||||
@@ -175,7 +180,8 @@
|
||||
"code=YOU_ARE_NOT_ALLOWED_TO_CHANGE_USERS_ROLE": "Bạn không đủ quyền để chỉnh sửa quyền hạn người dùng!",
|
||||
"code=USER_ALREADY_EXISTS_USE_ANOTHER_EMAIL": "Email này đã có người sử dụng. Vui lòng chọn một email khác!",
|
||||
"code=BANNED_USER": "Bạn đã bị quản trị viên khóa tài khoản, hãy liên hệ quản trị viên để tìm hiểu thêm!",
|
||||
"code=VALIDATION_ERROR": "Có giá trị không hợp lệ!"
|
||||
"code=VALIDATION_ERROR": "Có giá trị không hợp lệ!",
|
||||
"code=USER_IS_NOT_A_MEMBER_OF_THE_ORGANIZATION": "Người dùng này không phải thành viên nhà này"
|
||||
}
|
||||
}
|
||||
]
|
||||
|
||||
@@ -112,8 +112,8 @@ const DataTable = <TData, TValue>({
|
||||
</TableBody>
|
||||
</Table>
|
||||
</div>
|
||||
<div className="flex items-center justify-between">
|
||||
<div className="hidden text-sm gap-2 xl:flex">
|
||||
<div className="flex items-center justify-between gap-2">
|
||||
<div className="hidden text-sm gap-2 lg:flex">
|
||||
<span>
|
||||
{m.common_page_show({
|
||||
count: pagination.totalItem,
|
||||
@@ -122,9 +122,12 @@ const DataTable = <TData, TValue>({
|
||||
})}
|
||||
</span>
|
||||
</div>
|
||||
<div className="flex max-w-full items-center gap-8 xl:w-ft">
|
||||
<div className="hidden items-center gap-2 lg:flex">
|
||||
<Label htmlFor="rows-per-page" className="text-sm font-medium">
|
||||
<div className="flex max-w-full items-center gap-2 justify-between w-full lg:w-fit lg:gap-8">
|
||||
<div className="items-center gap-2 flex">
|
||||
<Label
|
||||
htmlFor="rows-per-page"
|
||||
className="hidden text-sm font-medium lg:block"
|
||||
>
|
||||
{m.common_per_page()}
|
||||
</Label>
|
||||
<Select
|
||||
|
||||
@@ -1,4 +1,3 @@
|
||||
import { ReturnError } from '@/types/common';
|
||||
import { useAppForm } from '@hooks/use-app-form';
|
||||
import { m } from '@paraglide/messages';
|
||||
import { Locale, setLocale } from '@paraglide/runtime';
|
||||
@@ -22,7 +21,7 @@ const UserSettingsForm = () => {
|
||||
|
||||
const { data, isLoading } = useQuery(settingQueries.listUser());
|
||||
|
||||
const updateMutation = useMutation({
|
||||
const { mutate: updateMutation, isPending } = useMutation({
|
||||
mutationFn: updateUserSettings,
|
||||
onSuccess: (_, variables) => {
|
||||
setLocale(variables.data.language as Locale);
|
||||
@@ -51,7 +50,7 @@ const UserSettingsForm = () => {
|
||||
onChange: userSettingSchema,
|
||||
},
|
||||
onSubmit: ({ value }) => {
|
||||
updateMutation.mutate({ data: value as UserSettingInput });
|
||||
updateMutation({ data: value as UserSettingInput });
|
||||
},
|
||||
});
|
||||
|
||||
@@ -102,7 +101,10 @@ const UserSettingsForm = () => {
|
||||
</form.AppField>
|
||||
<Field>
|
||||
<form.AppForm>
|
||||
<form.SubscribeButton label={m.ui_update_btn()} />
|
||||
<form.SubscribeButton
|
||||
label={m.ui_update_btn()}
|
||||
disabled={isPending}
|
||||
/>
|
||||
</form.AppForm>
|
||||
</Field>
|
||||
</FieldGroup>
|
||||
|
||||
@@ -8,21 +8,31 @@ 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 { Spinner } from '../ui/spinner';
|
||||
|
||||
export function SubscribeButton({
|
||||
label,
|
||||
variant = 'default',
|
||||
disabled = false,
|
||||
}: {
|
||||
label: string;
|
||||
disabled?: boolean;
|
||||
} & VariantProps<typeof buttonVariants>) {
|
||||
const form = useFormContext();
|
||||
return (
|
||||
<form.Subscribe selector={(state) => state.isSubmitting}>
|
||||
{(isSubmitting) => (
|
||||
<Button type="submit" disabled={isSubmitting} variant={variant}>
|
||||
{label}
|
||||
</Button>
|
||||
)}
|
||||
{(isSubmitting) => {
|
||||
return (
|
||||
<Button
|
||||
type="submit"
|
||||
disabled={isSubmitting || disabled}
|
||||
variant={variant}
|
||||
>
|
||||
{(isSubmitting || disabled) && <Spinner data-icon="inline-start" />}
|
||||
{label}
|
||||
</Button>
|
||||
);
|
||||
}}
|
||||
</form.Subscribe>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { ReturnError } from '@/types/common';
|
||||
import { useAuth } from '@/components/auth/auth-provider';
|
||||
import { useAppForm } from '@hooks/use-app-form';
|
||||
import useDebounced from '@hooks/use-debounced';
|
||||
import { authClient } from '@lib/auth-client';
|
||||
@@ -12,14 +12,16 @@ 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 { useEffect, useState } from 'react';
|
||||
import { toast } from 'sonner';
|
||||
|
||||
type FormProps = {
|
||||
onSubmit: (open: boolean) => void;
|
||||
isPersonal?: boolean;
|
||||
};
|
||||
|
||||
const CreateNewHouseForm = ({ onSubmit }: FormProps) => {
|
||||
const CreateNewHouseForm = ({ onSubmit, isPersonal = false }: FormProps) => {
|
||||
const { data: session } = useAuth();
|
||||
const [userKeyword, setUserKeyword] = useState('');
|
||||
const debouncedUserKeyword = useDebounced(userKeyword, 300);
|
||||
const { data: users } = useQuery(
|
||||
@@ -28,11 +30,13 @@ const CreateNewHouseForm = ({ onSubmit }: FormProps) => {
|
||||
|
||||
const queryClient = useQueryClient();
|
||||
|
||||
const { mutate: createHouseMutation } = useMutation({
|
||||
const queryKey = isPersonal ? 'currentUser' : 'list';
|
||||
|
||||
const { mutate: createHouseMutation, isPending } = useMutation({
|
||||
mutationFn: createHouse,
|
||||
onSuccess: () => {
|
||||
queryClient.invalidateQueries({
|
||||
queryKey: [...housesQueries.all, 'list'],
|
||||
queryKey: [...housesQueries.all, queryKey],
|
||||
});
|
||||
onSubmit(false);
|
||||
toast.success(m.houses_page_message_create_house_success(), {
|
||||
@@ -76,6 +80,13 @@ const CreateNewHouseForm = ({ onSubmit }: FormProps) => {
|
||||
},
|
||||
});
|
||||
|
||||
useEffect(() => {
|
||||
if (isPersonal) {
|
||||
form.setFieldValue('userId', session.user.id);
|
||||
}
|
||||
console.log(isPending);
|
||||
}, []);
|
||||
|
||||
return (
|
||||
<form
|
||||
id="admin-create-house-form"
|
||||
@@ -94,17 +105,19 @@ const CreateNewHouseForm = ({ onSubmit }: FormProps) => {
|
||||
<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>
|
||||
{!isPersonal && (
|
||||
<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>
|
||||
@@ -113,7 +126,10 @@ const CreateNewHouseForm = ({ onSubmit }: FormProps) => {
|
||||
</Button>
|
||||
</DialogClose>
|
||||
<form.AppForm>
|
||||
<form.SubscribeButton label={m.ui_confirm_btn()} />
|
||||
<form.SubscribeButton
|
||||
label={m.ui_confirm_btn()}
|
||||
disabled={isPending}
|
||||
/>
|
||||
</form.AppForm>
|
||||
</DialogFooter>
|
||||
</Field>
|
||||
|
||||
@@ -1,4 +1,3 @@
|
||||
import { ReturnError } from '@/types/common';
|
||||
import { useAppForm } from '@hooks/use-app-form';
|
||||
import { authClient } from '@lib/auth-client';
|
||||
import { m } from '@paraglide/messages';
|
||||
@@ -16,18 +15,21 @@ import { toast } from 'sonner';
|
||||
type EditHouseFormProps = {
|
||||
data: OrganizationWithMembers;
|
||||
onSubmit: (open: boolean) => void;
|
||||
mutateKey: string;
|
||||
};
|
||||
|
||||
const EditHouseForm = ({ data, onSubmit }: EditHouseFormProps) => {
|
||||
const EditHouseForm = ({ data, onSubmit, mutateKey }: EditHouseFormProps) => {
|
||||
const { refetch } = authClient.useActiveOrganization();
|
||||
const queryClient = useQueryClient();
|
||||
|
||||
const { mutate: updateHouseMutation } = useMutation({
|
||||
const { mutate: updateHouseMutation, isPending } = useMutation({
|
||||
mutationFn: updateHouse,
|
||||
onSuccess: () => {
|
||||
queryClient.invalidateQueries({
|
||||
queryKey: [...housesQueries.all, 'list'],
|
||||
queryKey: [...housesQueries.all, mutateKey],
|
||||
});
|
||||
onSubmit(false);
|
||||
refetch();
|
||||
toast.success(m.houses_page_message_update_house_success(), {
|
||||
richColors: true,
|
||||
});
|
||||
@@ -104,7 +106,10 @@ const EditHouseForm = ({ data, onSubmit }: EditHouseFormProps) => {
|
||||
</Button>
|
||||
</DialogClose>
|
||||
<form.AppForm>
|
||||
<form.SubscribeButton label={m.ui_confirm_btn()} />
|
||||
<form.SubscribeButton
|
||||
label={m.ui_confirm_btn()}
|
||||
disabled={isPending}
|
||||
/>
|
||||
</form.AppForm>
|
||||
</DialogFooter>
|
||||
</Field>
|
||||
|
||||
@@ -1,4 +1,3 @@
|
||||
import { ReturnError } from '@/types/common';
|
||||
import { useAppForm } from '@hooks/use-app-form';
|
||||
import { m } from '@paraglide/messages';
|
||||
import { GearIcon } from '@phosphor-icons/react';
|
||||
@@ -22,7 +21,7 @@ const SettingsForm = () => {
|
||||
|
||||
const { data: settings, isLoading } = useQuery(settingQueries.listAdmin());
|
||||
|
||||
const updateMutation = useMutation({
|
||||
const { mutate: updateMutation, isPending } = useMutation({
|
||||
mutationFn: updateAdminSettings,
|
||||
onSuccess: () => {
|
||||
queryClient.invalidateQueries(settingQueries.listAdmin());
|
||||
@@ -51,7 +50,7 @@ const SettingsForm = () => {
|
||||
onChange: settingSchema,
|
||||
},
|
||||
onSubmit: async ({ value }) => {
|
||||
updateMutation.mutate({ data: value as SettingsInput });
|
||||
updateMutation({ data: value as SettingsInput });
|
||||
},
|
||||
});
|
||||
|
||||
@@ -89,7 +88,10 @@ const SettingsForm = () => {
|
||||
</form.AppField>
|
||||
<Field>
|
||||
<form.AppForm>
|
||||
<form.SubscribeButton label={m.ui_update_btn()} />
|
||||
<form.SubscribeButton
|
||||
label={m.ui_update_btn()}
|
||||
disabled={isPending}
|
||||
/>
|
||||
</form.AppForm>
|
||||
</Field>
|
||||
</FieldGroup>
|
||||
|
||||
@@ -1,4 +1,3 @@
|
||||
import { ReturnError } from '@/types/common';
|
||||
import { useAppForm } from '@hooks/use-app-form';
|
||||
import { m } from '@paraglide/messages';
|
||||
import { usersQueries } from '@service/queries';
|
||||
@@ -17,7 +16,7 @@ type FormProps = {
|
||||
const AdminCreateUserForm = ({ onSubmit }: FormProps) => {
|
||||
const queryClient = useQueryClient();
|
||||
|
||||
const { mutate: createUserMutation } = useMutation({
|
||||
const { mutate: createUserMutation, isPending } = useMutation({
|
||||
mutationFn: createUser,
|
||||
onSuccess: () => {
|
||||
queryClient.invalidateQueries({
|
||||
@@ -100,7 +99,10 @@ const AdminCreateUserForm = ({ onSubmit }: FormProps) => {
|
||||
</Button>
|
||||
</DialogClose>
|
||||
<form.AppForm>
|
||||
<form.SubscribeButton label={m.ui_signup_btn()} />
|
||||
<form.SubscribeButton
|
||||
label={m.ui_signup_btn()}
|
||||
disabled={isPending}
|
||||
/>
|
||||
</form.AppForm>
|
||||
</DialogFooter>
|
||||
</Field>
|
||||
|
||||
@@ -1,5 +1,4 @@
|
||||
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';
|
||||
@@ -19,7 +18,7 @@ type FormProps = {
|
||||
const AdminSetPasswordForm = ({ data, onSubmit }: FormProps) => {
|
||||
const queryClient = useQueryClient();
|
||||
|
||||
const setUserPasswordMutation = useMutation({
|
||||
const { mutate: setUserPasswordMutation, isPending } = useMutation({
|
||||
mutationFn: setUserPassword,
|
||||
onSuccess: () => {
|
||||
queryClient.invalidateQueries({
|
||||
@@ -50,7 +49,7 @@ const AdminSetPasswordForm = ({ data, onSubmit }: FormProps) => {
|
||||
onSubmit: userSetPasswordSchema,
|
||||
},
|
||||
onSubmit: async ({ value }) => {
|
||||
setUserPasswordMutation.mutate({ data: value });
|
||||
setUserPasswordMutation({ data: value });
|
||||
},
|
||||
});
|
||||
|
||||
@@ -83,7 +82,10 @@ const AdminSetPasswordForm = ({ data, onSubmit }: FormProps) => {
|
||||
</Button>
|
||||
</DialogClose>
|
||||
<form.AppForm>
|
||||
<form.SubscribeButton label={m.ui_save_btn()} />
|
||||
<form.SubscribeButton
|
||||
label={m.ui_save_btn()}
|
||||
disabled={isPending}
|
||||
/>
|
||||
</form.AppForm>
|
||||
</DialogFooter>
|
||||
</Field>
|
||||
|
||||
@@ -1,4 +1,3 @@
|
||||
import { ReturnError } from '@/types/common';
|
||||
import { useAppForm } from '@hooks/use-app-form';
|
||||
import { m } from '@paraglide/messages';
|
||||
import { usersQueries } from '@service/queries';
|
||||
@@ -24,7 +23,7 @@ const AdminSetUserRoleForm = ({ data, onSubmit }: SetRoleFormProps) => {
|
||||
role: data.role,
|
||||
};
|
||||
|
||||
const updateRoleMutation = useMutation({
|
||||
const { mutate: updateRoleMutation, isPending } = useMutation({
|
||||
mutationFn: setUserRole,
|
||||
onSuccess: () => {
|
||||
queryClient.refetchQueries({
|
||||
@@ -53,7 +52,7 @@ const AdminSetUserRoleForm = ({ data, onSubmit }: SetRoleFormProps) => {
|
||||
onSubmit: userUpdateRoleSchema,
|
||||
},
|
||||
onSubmit: async ({ value }) => {
|
||||
updateRoleMutation.mutate({ data: value });
|
||||
updateRoleMutation({ data: value });
|
||||
},
|
||||
});
|
||||
|
||||
@@ -90,7 +89,10 @@ const AdminSetUserRoleForm = ({ data, onSubmit }: SetRoleFormProps) => {
|
||||
</Button>
|
||||
</DialogClose>
|
||||
<form.AppForm>
|
||||
<form.SubscribeButton label={m.ui_save_btn()} />
|
||||
<form.SubscribeButton
|
||||
label={m.ui_save_btn()}
|
||||
disabled={isPending}
|
||||
/>
|
||||
</form.AppForm>
|
||||
</DialogFooter>
|
||||
</Field>
|
||||
|
||||
@@ -1,4 +1,3 @@
|
||||
import { ReturnError } from '@/types/common';
|
||||
import { useAppForm } from '@hooks/use-app-form';
|
||||
import { m } from '@paraglide/messages';
|
||||
import { usersQueries } from '@service/queries';
|
||||
@@ -19,7 +18,7 @@ type UpdateUserFormProps = {
|
||||
const AdminUpdateUserInfoForm = ({ data, onSubmit }: UpdateUserFormProps) => {
|
||||
const queryClient = useQueryClient();
|
||||
|
||||
const updateUserMutation = useMutation({
|
||||
const { mutate: updateUserMutation, isPending } = useMutation({
|
||||
mutationFn: updateUserInformation,
|
||||
onSuccess: () => {
|
||||
queryClient.refetchQueries({
|
||||
@@ -49,7 +48,7 @@ const AdminUpdateUserInfoForm = ({ data, onSubmit }: UpdateUserFormProps) => {
|
||||
onChange: userUpdateInfoSchema,
|
||||
},
|
||||
onSubmit: async ({ value }) => {
|
||||
updateUserMutation.mutate({ data: value });
|
||||
updateUserMutation({ data: value });
|
||||
},
|
||||
});
|
||||
|
||||
@@ -77,7 +76,10 @@ const AdminUpdateUserInfoForm = ({ data, onSubmit }: UpdateUserFormProps) => {
|
||||
</Button>
|
||||
</DialogClose>
|
||||
<form.AppForm>
|
||||
<form.SubscribeButton label={m.ui_save_btn()} />
|
||||
<form.SubscribeButton
|
||||
label={m.ui_save_btn()}
|
||||
disabled={isPending}
|
||||
/>
|
||||
</form.AppForm>
|
||||
</DialogFooter>
|
||||
</Field>
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
import { cn } from '@/lib/utils';
|
||||
import CreateNewHouseForm from '@form/house/admin-create-house-form';
|
||||
import useHasPermission from '@hooks/use-has-permission';
|
||||
import usePreventAutoFocus from '@hooks/use-prevent-auto-focus';
|
||||
@@ -14,7 +15,15 @@ import {
|
||||
} from '@ui/dialog';
|
||||
import { useState } from 'react';
|
||||
|
||||
const CreateNewHouse = () => {
|
||||
type CreateNewHouseProp = {
|
||||
isPersonal?: boolean;
|
||||
className?: string;
|
||||
};
|
||||
|
||||
const CreateNewHouse = ({
|
||||
className,
|
||||
isPersonal = false,
|
||||
}: CreateNewHouseProp) => {
|
||||
const { hasPermission, isLoading } = useHasPermission('house', 'create');
|
||||
const [_open, _setOpen] = useState(false);
|
||||
const prevent = usePreventAutoFocus();
|
||||
@@ -25,7 +34,7 @@ const CreateNewHouse = () => {
|
||||
return (
|
||||
<Dialog open={_open} onOpenChange={_setOpen}>
|
||||
<DialogTrigger asChild>
|
||||
<Button type="button" variant="default">
|
||||
<Button type="button" variant="default" className={cn(className)}>
|
||||
<PlusIcon />
|
||||
{m.nav_add_new()}
|
||||
</Button>
|
||||
@@ -44,7 +53,7 @@ const CreateNewHouse = () => {
|
||||
{m.nav_add_new()}
|
||||
</DialogDescription>
|
||||
</DialogHeader>
|
||||
<CreateNewHouseForm onSubmit={_setOpen} />
|
||||
<CreateNewHouseForm onSubmit={_setOpen} isPersonal={isPersonal} />
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
);
|
||||
|
||||
44
src/components/house/current-user-action-group.tsx
Normal file
44
src/components/house/current-user-action-group.tsx
Normal file
@@ -0,0 +1,44 @@
|
||||
import { authClient } from '@lib/auth-client';
|
||||
import { m } from '@paraglide/messages';
|
||||
import { GearIcon, PenIcon } from '@phosphor-icons/react';
|
||||
import { Card, CardContent, CardHeader, CardTitle } from '@ui/card';
|
||||
import { Button } from '../ui/button';
|
||||
import DeleteUserHouseAction from './delete-user-house-dialog';
|
||||
import EditHouseAction from './edit-house-dialog';
|
||||
|
||||
type CurrentUserActionGroupProps = {
|
||||
activeHouse: ReturnType<typeof authClient.useActiveOrganization>['data'];
|
||||
};
|
||||
|
||||
const CurrentUserActionGroup = ({
|
||||
activeHouse,
|
||||
}: CurrentUserActionGroupProps) => {
|
||||
return (
|
||||
<Card className="col-span-1">
|
||||
<CardHeader>
|
||||
<CardTitle className="flex items-center gap-2">
|
||||
<GearIcon />
|
||||
{m.houses_user_page_block_action_title()}
|
||||
</CardTitle>
|
||||
</CardHeader>
|
||||
<CardContent className="flex flex-row gap-2">
|
||||
<EditHouseAction
|
||||
data={activeHouse as OrganizationWithMembers}
|
||||
isPersonal
|
||||
>
|
||||
<Button
|
||||
type="button"
|
||||
size="icon-lg"
|
||||
className="rounded-full cursor-pointer bg-blue-500 text-white hover:bg-blue-100 hover:text-blue-600"
|
||||
>
|
||||
<PenIcon size={16} />
|
||||
<span className="sr-only">{m.ui_edit_house_btn()}</span>
|
||||
</Button>
|
||||
</EditHouseAction>
|
||||
<DeleteUserHouseAction activeHouse={activeHouse} />
|
||||
</CardContent>
|
||||
</Card>
|
||||
);
|
||||
};
|
||||
|
||||
export default CurrentUserActionGroup;
|
||||
132
src/components/house/current-user-house-list.tsx
Normal file
132
src/components/house/current-user-house-list.tsx
Normal file
@@ -0,0 +1,132 @@
|
||||
import { authClient } from '@lib/auth-client';
|
||||
import { cn } from '@lib/utils';
|
||||
import { m } from '@paraglide/messages';
|
||||
import { CheckIcon, WarehouseIcon } from '@phosphor-icons/react';
|
||||
import { housesQueries } from '@service/queries';
|
||||
import { useQuery } from '@tanstack/react-query';
|
||||
import { Button } from '@ui/button';
|
||||
import { Card, CardContent, CardHeader, CardTitle } from '@ui/card';
|
||||
import parse from 'html-react-parser';
|
||||
import { toast } from 'sonner';
|
||||
import {
|
||||
Item,
|
||||
ItemActions,
|
||||
ItemContent,
|
||||
ItemDescription,
|
||||
ItemTitle,
|
||||
} from '../ui/item';
|
||||
import { ScrollArea, ScrollBar } from '../ui/scroll-area';
|
||||
import { Skeleton } from '../ui/skeleton';
|
||||
import CreateNewHouse from './create-house-dialog';
|
||||
|
||||
type CurrentUserHouseListProps = {
|
||||
activeHouse: ReturnType<typeof authClient.useActiveOrganization>['data'];
|
||||
};
|
||||
|
||||
const CurrentUserHouseList = ({ activeHouse }: CurrentUserHouseListProps) => {
|
||||
const { data: houses } = useQuery(housesQueries.currentUser());
|
||||
|
||||
const activeHouseAction = async ({
|
||||
id,
|
||||
slug,
|
||||
}: {
|
||||
id: string;
|
||||
slug: string;
|
||||
}) => {
|
||||
const { data, error } = await authClient.organization.setActive({
|
||||
organizationId: id,
|
||||
organizationSlug: slug,
|
||||
});
|
||||
|
||||
if (error) {
|
||||
toast.error(error.message, { richColors: true });
|
||||
}
|
||||
|
||||
if (data) {
|
||||
toast.success(
|
||||
parse(
|
||||
m.houses_user_page_message_active_house_success({ house: data.name }),
|
||||
),
|
||||
{
|
||||
richColors: true,
|
||||
},
|
||||
);
|
||||
}
|
||||
};
|
||||
|
||||
if (!activeHouse || !houses) {
|
||||
return <Skeleton className="col-span-2 h-80 w-full rounded-xl" />;
|
||||
}
|
||||
|
||||
return (
|
||||
<Card className="col-span-1 lg:col-span-3">
|
||||
<CardHeader>
|
||||
<CardTitle className="text-xl flex items-center gap-2">
|
||||
<WarehouseIcon size={24} />
|
||||
{m.houses_page_ui_title()}
|
||||
<CreateNewHouse isPersonal className="ml-auto" />
|
||||
</CardTitle>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<ScrollArea className="w-full rounded-md border whitespace-nowrap bg-gray-50">
|
||||
<div className="flex w-max p-4 space-x-4">
|
||||
{houses.map((house) => {
|
||||
const isActive = house.id === activeHouse.id;
|
||||
|
||||
return (
|
||||
<Item
|
||||
variant="outline"
|
||||
className={cn('w-100 bg-white', {
|
||||
'bg-linear-to-tr from-white/2 to-green-200': isActive,
|
||||
})}
|
||||
key={house.id}
|
||||
>
|
||||
<ItemContent>
|
||||
<ItemTitle
|
||||
className="font-bold text-sm text-(--house-color)"
|
||||
style={
|
||||
{ '--house-color': house.color } as React.CSSProperties
|
||||
}
|
||||
>
|
||||
{house.name}
|
||||
</ItemTitle>
|
||||
<ItemDescription>
|
||||
<strong>{m.houses_page_ui_table_header_members()}</strong>
|
||||
:
|
||||
{house._count.members}
|
||||
</ItemDescription>
|
||||
</ItemContent>
|
||||
<ItemActions>
|
||||
<Button
|
||||
variant="outline"
|
||||
data-active={isActive}
|
||||
disabled={isActive}
|
||||
className={cn('rounded-full cursor-pointer', {
|
||||
'disabled:bg-green-500! disabled:border-green-500!':
|
||||
isActive,
|
||||
'bg-amber-50! text-amber-600! border-amber-600!':
|
||||
!isActive,
|
||||
})}
|
||||
size="icon-lg"
|
||||
onClick={() =>
|
||||
activeHouseAction({ id: house.id, slug: house.slug })
|
||||
}
|
||||
>
|
||||
<CheckIcon weight="bold" />
|
||||
<span className="sr-only">
|
||||
{m.houses_page_house_active_btn()}
|
||||
</span>
|
||||
</Button>
|
||||
</ItemActions>
|
||||
</Item>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
<ScrollBar orientation="horizontal" />
|
||||
</ScrollArea>
|
||||
</CardContent>
|
||||
</Card>
|
||||
);
|
||||
};
|
||||
|
||||
export default CurrentUserHouseList;
|
||||
66
src/components/house/current-user-member-list.tsx
Normal file
66
src/components/house/current-user-member-list.tsx
Normal file
@@ -0,0 +1,66 @@
|
||||
import { authClient } from '@lib/auth-client';
|
||||
import { m } from '@paraglide/messages';
|
||||
import { Skeleton } from '@ui/skeleton';
|
||||
import {
|
||||
Table,
|
||||
TableBody,
|
||||
TableCell,
|
||||
TableHead,
|
||||
TableHeader,
|
||||
TableRow,
|
||||
} from '@ui/table';
|
||||
import RoleBadge from '../avatar/role-badge';
|
||||
import { Item, ItemContent, ItemDescription, ItemTitle } from '../ui/item';
|
||||
|
||||
type CurrentUserMemberListProps = {
|
||||
activeHouse: ReturnType<typeof authClient.useActiveOrganization>['data'];
|
||||
};
|
||||
|
||||
const CurrentUserMemberList = ({ activeHouse }: CurrentUserMemberListProps) => {
|
||||
if (!activeHouse) {
|
||||
return <Skeleton className="col-span-2 h-80 w-full rounded-xl" />;
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="overflow-hidden rounded-md border col-span-1 lg:col-span-2 shadow-xs bg-linear-to-br from-primary/5 to-card">
|
||||
<Table className="">
|
||||
<TableHeader>
|
||||
<TableRow>
|
||||
<TableHead className="px-4 bg-primary text-white text-sm w-1/3">
|
||||
{m.houses_page_ui_table_header_name()} &{' '}
|
||||
{m.houses_page_ui_view_table_header_email()}
|
||||
</TableHead>
|
||||
<TableHead className="px-4 bg-primary text-white text-sm w-1/3">
|
||||
{m.houses_page_ui_view_table_header_role()}
|
||||
</TableHead>
|
||||
<TableHead className="px-4 bg-primary" />
|
||||
</TableRow>
|
||||
</TableHeader>
|
||||
<TableBody>
|
||||
{activeHouse.members.map((member) => (
|
||||
<TableRow key={member.user.id}>
|
||||
<TableCell className="px-4">
|
||||
<Item className="p-0">
|
||||
<ItemContent>
|
||||
<ItemTitle>{member.user.name}</ItemTitle>
|
||||
<ItemDescription>{member.user.email}</ItemDescription>
|
||||
</ItemContent>
|
||||
</Item>
|
||||
</TableCell>
|
||||
<TableCell className="px-4">
|
||||
<RoleBadge type={member.role} />
|
||||
</TableCell>
|
||||
<TableCell className="px-4">
|
||||
<div className="flex justify-end gap-2">
|
||||
{activeHouse.color}
|
||||
</div>
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
))}
|
||||
</TableBody>
|
||||
</Table>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default CurrentUserMemberList;
|
||||
@@ -1,7 +1,6 @@
|
||||
import { m } from '@/paraglide/messages';
|
||||
import { deleteHouse } from '@/service/house.api';
|
||||
import { housesQueries } from '@/service/queries';
|
||||
import { ReturnError } from '@/types/common';
|
||||
import useHasPermission from '@hooks/use-has-permission';
|
||||
import usePreventAutoFocus from '@hooks/use-prevent-auto-focus';
|
||||
import { ShieldWarningIcon, TrashIcon } from '@phosphor-icons/react';
|
||||
@@ -31,6 +30,7 @@ import parse from 'html-react-parser';
|
||||
import { useState } from 'react';
|
||||
import { toast } from 'sonner';
|
||||
import RoleBadge from '../avatar/role-badge';
|
||||
import { Spinner } from '../ui/spinner';
|
||||
|
||||
type DeleteHouseProps = {
|
||||
data: OrganizationWithMembers;
|
||||
@@ -43,7 +43,7 @@ const DeleteHouseAction = ({ data }: DeleteHouseProps) => {
|
||||
|
||||
const queryClient = useQueryClient();
|
||||
|
||||
const { mutate: deleteHouseMutation } = useMutation({
|
||||
const { mutate: deleteHouseMutation, isPending } = useMutation({
|
||||
mutationFn: deleteHouse,
|
||||
onSuccess: () => {
|
||||
queryClient.invalidateQueries({
|
||||
@@ -140,7 +140,13 @@ const DeleteHouseAction = ({ data }: DeleteHouseProps) => {
|
||||
{m.ui_cancel_btn()}
|
||||
</Button>
|
||||
</DialogClose>
|
||||
<Button variant="destructive" type="button" onClick={onConfirm}>
|
||||
<Button
|
||||
variant="destructive"
|
||||
type="button"
|
||||
onClick={onConfirm}
|
||||
disabled={isPending}
|
||||
>
|
||||
{isPending && <Spinner data-icon="inline-start" />}
|
||||
{m.ui_confirm_btn()}
|
||||
</Button>
|
||||
</DialogFooter>
|
||||
|
||||
163
src/components/house/delete-user-house-dialog.tsx
Normal file
163
src/components/house/delete-user-house-dialog.tsx
Normal file
@@ -0,0 +1,163 @@
|
||||
import { authClient } from '@/lib/auth-client';
|
||||
import { m } from '@/paraglide/messages';
|
||||
import { deleteUserHouse } from '@/service/house.api';
|
||||
import { housesQueries } from '@/service/queries';
|
||||
import useHasPermission from '@hooks/use-has-permission';
|
||||
import usePreventAutoFocus from '@hooks/use-prevent-auto-focus';
|
||||
import { ShieldWarningIcon, TrashIcon } from '@phosphor-icons/react';
|
||||
import { useMutation, useQueryClient } from '@tanstack/react-query';
|
||||
import { Button } from '@ui/button';
|
||||
import {
|
||||
Dialog,
|
||||
DialogClose,
|
||||
DialogContent,
|
||||
DialogDescription,
|
||||
DialogFooter,
|
||||
DialogHeader,
|
||||
DialogTitle,
|
||||
DialogTrigger,
|
||||
} from '@ui/dialog';
|
||||
import { Label } from '@ui/label';
|
||||
import {
|
||||
Table,
|
||||
TableBody,
|
||||
TableCell,
|
||||
TableHead,
|
||||
TableHeader,
|
||||
TableRow,
|
||||
} from '@ui/table';
|
||||
import { Tooltip, TooltipContent, TooltipTrigger } from '@ui/tooltip';
|
||||
import parse from 'html-react-parser';
|
||||
import { useState } from 'react';
|
||||
import { toast } from 'sonner';
|
||||
import RoleBadge from '../avatar/role-badge';
|
||||
import { Spinner } from '../ui/spinner';
|
||||
|
||||
type DeleteUserHouseProps = {
|
||||
activeHouse: ReturnType<typeof authClient.useActiveOrganization>['data'];
|
||||
};
|
||||
|
||||
const DeleteUserHouseAction = ({ activeHouse }: DeleteUserHouseProps) => {
|
||||
const [_open, _setOpen] = useState(false);
|
||||
const prevent = usePreventAutoFocus();
|
||||
const { hasPermission, isLoading } = useHasPermission('house', 'delete');
|
||||
|
||||
const queryClient = useQueryClient();
|
||||
|
||||
const { mutate: deleteHouseMutation, isPending } = useMutation({
|
||||
mutationFn: deleteUserHouse,
|
||||
onSuccess: () => {
|
||||
queryClient.invalidateQueries({
|
||||
queryKey: [...housesQueries.all, 'currentUser'],
|
||||
});
|
||||
_setOpen(false);
|
||||
toast.success(m.houses_page_message_delete_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,
|
||||
});
|
||||
},
|
||||
});
|
||||
|
||||
if (isLoading || !activeHouse) return null;
|
||||
|
||||
const onConfirm = async () => {
|
||||
deleteHouseMutation({ data: { id: activeHouse.id } });
|
||||
};
|
||||
|
||||
if (hasPermission) {
|
||||
return (
|
||||
<Dialog open={_open} onOpenChange={_setOpen}>
|
||||
<Tooltip>
|
||||
<TooltipTrigger asChild>
|
||||
<DialogTrigger asChild>
|
||||
<Button
|
||||
type="button"
|
||||
size="icon-lg"
|
||||
className="rounded-full cursor-pointer bg-red-500 text-white hover:bg-red-100 hover:text-red-600"
|
||||
>
|
||||
<TrashIcon size={16} />
|
||||
<span className="sr-only">{m.ui_delete_btn()}</span>
|
||||
</Button>
|
||||
</DialogTrigger>
|
||||
</TooltipTrigger>
|
||||
<TooltipContent className="bg-red-500 [&_svg]:bg-red-500 [&_svg]:fill-red-500 text-white">
|
||||
<Label>{m.ui_delete_btn()}</Label>
|
||||
</TooltipContent>
|
||||
</Tooltip>
|
||||
<DialogContent
|
||||
className="max-w-100 xl:max-w-xl"
|
||||
showCloseButton={false}
|
||||
{...prevent}
|
||||
onPointerDownOutside={(e) => e.preventDefault()}
|
||||
onEscapeKeyDown={(e) => e.preventDefault()}
|
||||
>
|
||||
<DialogHeader>
|
||||
<DialogTitle className="flex items-center gap-3 text-lg font-bold text-red-500">
|
||||
<div className="rounded-full bg-red-100 p-3">
|
||||
<ShieldWarningIcon size={30} />
|
||||
</div>
|
||||
{m.houses_page_ui_dialog_alert_delete_title({
|
||||
name: activeHouse.name,
|
||||
})}
|
||||
</DialogTitle>
|
||||
<DialogDescription className="text-red-500">
|
||||
{parse(m.houses_page_ui_dialog_alert_delete_description())}
|
||||
</DialogDescription>
|
||||
</DialogHeader>
|
||||
<div className="overflow-hidden rounded-md border">
|
||||
<Table className="bg-white">
|
||||
<TableHeader>
|
||||
<TableRow>
|
||||
<TableHead className="px-2 h-7 bg-primary text-white text-xs w-1/2">
|
||||
{m.houses_page_ui_view_table_header_email()}
|
||||
</TableHead>
|
||||
<TableHead className="px-2 h-7 bg-primary text-white text-xs w-1/2">
|
||||
{m.houses_page_ui_view_table_header_role()}
|
||||
</TableHead>
|
||||
</TableRow>
|
||||
</TableHeader>
|
||||
<TableBody>
|
||||
{activeHouse.members.map((member) => (
|
||||
<TableRow key={member.user.email}>
|
||||
<TableCell>{member.user.email}</TableCell>
|
||||
<TableCell>
|
||||
<RoleBadge type={member.role} />
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
))}
|
||||
</TableBody>
|
||||
</Table>
|
||||
</div>
|
||||
<DialogFooter className="bg-muted/50 -mx-4 -mb-4 rounded-b-xl border-t p-4">
|
||||
<DialogClose asChild>
|
||||
<Button variant="outline" type="button">
|
||||
{m.ui_cancel_btn()}
|
||||
</Button>
|
||||
</DialogClose>
|
||||
<Button
|
||||
variant="destructive"
|
||||
type="button"
|
||||
onClick={onConfirm}
|
||||
disabled={isPending}
|
||||
>
|
||||
{isPending && <Spinner data-icon="inline-start" />}
|
||||
{m.ui_confirm_btn()}
|
||||
</Button>
|
||||
</DialogFooter>
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
);
|
||||
}
|
||||
|
||||
return null;
|
||||
};
|
||||
|
||||
export default DeleteUserHouseAction;
|
||||
@@ -3,7 +3,6 @@ import useHasPermission from '@hooks/use-has-permission';
|
||||
import usePreventAutoFocus from '@hooks/use-prevent-auto-focus';
|
||||
import { m } from '@paraglide/messages';
|
||||
import { PenIcon } from '@phosphor-icons/react';
|
||||
import { Button } from '@ui/button';
|
||||
import {
|
||||
Dialog,
|
||||
DialogContent,
|
||||
@@ -18,9 +17,15 @@ import { useState } from 'react';
|
||||
|
||||
type EditHouseProps = {
|
||||
data: OrganizationWithMembers;
|
||||
children: React.ReactNode;
|
||||
isPersonal?: boolean;
|
||||
};
|
||||
|
||||
const EditHouseAction = ({ data }: EditHouseProps) => {
|
||||
const EditHouseAction = ({
|
||||
data,
|
||||
children,
|
||||
isPersonal = false,
|
||||
}: EditHouseProps) => {
|
||||
const [_open, _setOpen] = useState(false);
|
||||
const prevent = usePreventAutoFocus();
|
||||
const { hasPermission, isLoading } = useHasPermission('house', 'update');
|
||||
@@ -32,17 +37,7 @@ const EditHouseAction = ({ data }: EditHouseProps) => {
|
||||
<Dialog open={_open} onOpenChange={_setOpen}>
|
||||
<Tooltip>
|
||||
<TooltipTrigger asChild>
|
||||
<DialogTrigger asChild>
|
||||
<Button
|
||||
type="button"
|
||||
variant="ghost"
|
||||
size="icon"
|
||||
className="rounded-full cursor-pointer text-blue-500 hover:bg-blue-100 hover:text-blue-600"
|
||||
>
|
||||
<PenIcon size={16} />
|
||||
<span className="sr-only">{m.ui_edit_house_btn()}</span>
|
||||
</Button>
|
||||
</DialogTrigger>
|
||||
<DialogTrigger asChild>{children}</DialogTrigger>
|
||||
</TooltipTrigger>
|
||||
<TooltipContent className="bg-blue-500 [&_svg]:bg-blue-500 [&_svg]:fill-blue-500 text-white">
|
||||
<Label>{m.ui_edit_house_btn()}</Label>
|
||||
@@ -62,7 +57,11 @@ const EditHouseAction = ({ data }: EditHouseProps) => {
|
||||
{m.ui_edit_house_btn()}
|
||||
</DialogDescription>
|
||||
</DialogHeader>
|
||||
<EditHouseForm data={data} onSubmit={_setOpen} />
|
||||
<EditHouseForm
|
||||
data={data}
|
||||
onSubmit={_setOpen}
|
||||
mutateKey={isPersonal ? 'currentUser' : 'list'}
|
||||
/>
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
);
|
||||
|
||||
@@ -1,6 +1,8 @@
|
||||
import { m } from '@paraglide/messages';
|
||||
import { PenIcon } from '@phosphor-icons/react';
|
||||
import { ColumnDef } from '@tanstack/react-table';
|
||||
import { formatters } from '@utils/formatters';
|
||||
import { Button } from '../ui/button';
|
||||
import DeleteHouseAction from './delete-house-dialog';
|
||||
import EditHouseAction from './edit-house-dialog';
|
||||
import ViewDetailHouse from './view-house-detail-dialog';
|
||||
@@ -42,7 +44,17 @@ export const houseColumns: ColumnDef<OrganizationWithMembers>[] = [
|
||||
return (
|
||||
<div className="flex justify-end gap-2">
|
||||
<ViewDetailHouse data={row.original} />
|
||||
<EditHouseAction data={row.original} />
|
||||
<EditHouseAction data={row.original}>
|
||||
<Button
|
||||
type="button"
|
||||
variant="ghost"
|
||||
size="icon"
|
||||
className="rounded-full cursor-pointer text-blue-500 hover:bg-blue-100 hover:text-blue-600"
|
||||
>
|
||||
<PenIcon size={16} />
|
||||
<span className="sr-only">{m.ui_edit_house_btn()}</span>
|
||||
</Button>
|
||||
</EditHouseAction>
|
||||
<DeleteHouseAction data={row.original} />
|
||||
</div>
|
||||
);
|
||||
|
||||
@@ -16,6 +16,7 @@ import {
|
||||
SidebarMenuButton,
|
||||
SidebarMenuItem,
|
||||
} from '@ui/sidebar';
|
||||
import React from 'react';
|
||||
import AdminShow from '../auth/AdminShow';
|
||||
import AuthShow from '../auth/AuthShow';
|
||||
|
||||
@@ -25,94 +26,107 @@ const NAV_MAIN = [
|
||||
{
|
||||
id: '1',
|
||||
title: m.nav_label_basic(),
|
||||
isAuth: false,
|
||||
admin: false,
|
||||
items: [
|
||||
{
|
||||
title: m.nav_home(),
|
||||
path: '/',
|
||||
icon: HouseIcon,
|
||||
isAuth: true,
|
||||
admin: false,
|
||||
},
|
||||
{
|
||||
title: m.nav_dashboard(),
|
||||
path: '/dashboard',
|
||||
icon: GaugeIcon,
|
||||
isAuth: true,
|
||||
admin: false,
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
id: '2',
|
||||
title: m.nav_label_management(),
|
||||
isAuth: true,
|
||||
admin: false,
|
||||
items: [
|
||||
{
|
||||
title: m.nav_houses(),
|
||||
path: '/kanri/houses',
|
||||
icon: WarehouseIcon,
|
||||
isAuth: false,
|
||||
admin: true,
|
||||
title: m.nav_dashboard(),
|
||||
path: '/management/dashboard',
|
||||
icon: GaugeIcon,
|
||||
},
|
||||
{
|
||||
title: m.nav_houses(),
|
||||
path: '/management/houses',
|
||||
icon: WarehouseIcon,
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
id: '3',
|
||||
title: m.nav_label_kanri(),
|
||||
isAuth: true,
|
||||
admin: true,
|
||||
items: [
|
||||
{
|
||||
title: m.nav_users(),
|
||||
path: '/kanri/users',
|
||||
icon: UsersIcon,
|
||||
isAuth: false,
|
||||
admin: true,
|
||||
},
|
||||
{
|
||||
title: m.nav_houses(),
|
||||
path: '/kanri/houses',
|
||||
icon: WarehouseIcon,
|
||||
},
|
||||
{
|
||||
title: m.nav_logs(),
|
||||
path: '/kanri/logs',
|
||||
icon: CircuitryIcon,
|
||||
isAuth: false,
|
||||
admin: true,
|
||||
},
|
||||
{
|
||||
title: m.nav_settings(),
|
||||
path: '/kanri/settings',
|
||||
icon: GearIcon,
|
||||
isAuth: false,
|
||||
admin: true,
|
||||
},
|
||||
],
|
||||
},
|
||||
];
|
||||
|
||||
function EmptyComponent({ children }: { children: React.ReactNode }) {
|
||||
return <>{children}</>;
|
||||
}
|
||||
|
||||
const NavMain = () => {
|
||||
return (
|
||||
<>
|
||||
{NAV_MAIN.map((nav) => (
|
||||
<SidebarGroup key={nav.id} className="overflow-hidden">
|
||||
<SidebarGroupLabel>{nav.title}</SidebarGroupLabel>
|
||||
<SidebarGroupContent>
|
||||
<SidebarMenu>
|
||||
{nav.items.map((item) => {
|
||||
const Icon = item.icon;
|
||||
const Menu = (
|
||||
<SidebarMenuItem>
|
||||
<SidebarMenuButtonLink
|
||||
type="button"
|
||||
to={item.path}
|
||||
className="cursor-pointer"
|
||||
tooltip={item.title}
|
||||
>
|
||||
<Icon size={24} />
|
||||
{item.title}
|
||||
</SidebarMenuButtonLink>
|
||||
</SidebarMenuItem>
|
||||
);
|
||||
return item.isAuth ? (
|
||||
<AuthShow key={item.path}>{Menu}</AuthShow>
|
||||
) : item.admin ? (
|
||||
<AdminShow key={item.path}>{Menu}</AdminShow>
|
||||
) : (
|
||||
Menu
|
||||
);
|
||||
})}
|
||||
</SidebarMenu>
|
||||
</SidebarGroupContent>
|
||||
</SidebarGroup>
|
||||
))}
|
||||
{NAV_MAIN.map((nav) => {
|
||||
const { isAuth, admin } = nav;
|
||||
const Component = admin
|
||||
? AdminShow
|
||||
: isAuth
|
||||
? AuthShow
|
||||
: EmptyComponent;
|
||||
|
||||
return (
|
||||
<Component key={nav.id}>
|
||||
<SidebarGroup className="overflow-hidden">
|
||||
<SidebarGroupLabel>{nav.title}</SidebarGroupLabel>
|
||||
<SidebarGroupContent>
|
||||
<SidebarMenu>
|
||||
{nav.items.map((item) => {
|
||||
const Icon = item.icon;
|
||||
return (
|
||||
<SidebarMenuItem key={item.path}>
|
||||
<SidebarMenuButtonLink
|
||||
type="button"
|
||||
to={item.path}
|
||||
className="cursor-pointer"
|
||||
tooltip={`${nav.title} - ${item.title}`}
|
||||
>
|
||||
<Icon size={24} />
|
||||
{item.title}
|
||||
</SidebarMenuButtonLink>
|
||||
</SidebarMenuItem>
|
||||
);
|
||||
})}
|
||||
</SidebarMenu>
|
||||
</SidebarGroupContent>
|
||||
</SidebarGroup>
|
||||
</Component>
|
||||
);
|
||||
})}
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
53
src/components/ui/scroll-area.tsx
Normal file
53
src/components/ui/scroll-area.tsx
Normal file
@@ -0,0 +1,53 @@
|
||||
import * as React from "react"
|
||||
import { ScrollArea as ScrollAreaPrimitive } from "radix-ui"
|
||||
|
||||
import { cn } from "@/lib/utils"
|
||||
|
||||
function ScrollArea({
|
||||
className,
|
||||
children,
|
||||
...props
|
||||
}: React.ComponentProps<typeof ScrollAreaPrimitive.Root>) {
|
||||
return (
|
||||
<ScrollAreaPrimitive.Root
|
||||
data-slot="scroll-area"
|
||||
className={cn("relative", className)}
|
||||
{...props}
|
||||
>
|
||||
<ScrollAreaPrimitive.Viewport
|
||||
data-slot="scroll-area-viewport"
|
||||
className="focus-visible:ring-ring/50 size-full rounded-[inherit] transition-[color,box-shadow] outline-none focus-visible:ring-[3px] focus-visible:outline-1"
|
||||
>
|
||||
{children}
|
||||
</ScrollAreaPrimitive.Viewport>
|
||||
<ScrollBar />
|
||||
<ScrollAreaPrimitive.Corner />
|
||||
</ScrollAreaPrimitive.Root>
|
||||
)
|
||||
}
|
||||
|
||||
function ScrollBar({
|
||||
className,
|
||||
orientation = "vertical",
|
||||
...props
|
||||
}: React.ComponentProps<typeof ScrollAreaPrimitive.ScrollAreaScrollbar>) {
|
||||
return (
|
||||
<ScrollAreaPrimitive.ScrollAreaScrollbar
|
||||
data-slot="scroll-area-scrollbar"
|
||||
data-orientation={orientation}
|
||||
orientation={orientation}
|
||||
className={cn(
|
||||
"data-horizontal:h-2.5 data-horizontal:flex-col data-horizontal:border-t data-horizontal:border-t-transparent data-vertical:h-full data-vertical:w-2.5 data-vertical:border-l data-vertical:border-l-transparent flex touch-none p-px transition-colors select-none",
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
>
|
||||
<ScrollAreaPrimitive.ScrollAreaThumb
|
||||
data-slot="scroll-area-thumb"
|
||||
className="rounded-full bg-border relative flex-1"
|
||||
/>
|
||||
</ScrollAreaPrimitive.ScrollAreaScrollbar>
|
||||
)
|
||||
}
|
||||
|
||||
export { ScrollArea, ScrollBar }
|
||||
@@ -135,7 +135,7 @@ export function SelectUser({
|
||||
<div
|
||||
id={listboxId}
|
||||
role="listbox"
|
||||
className="border-input bg-popover text-popover-foreground absolute top-full z-50 mt-1 max-h-60 w-full min-w-[var(--radix-popper-anchor-width)] overflow-hidden rounded-lg border shadow-md"
|
||||
className="border-input bg-popover text-popover-foreground absolute top-full z-50 mt-1 max-h-60 w-full min-w-(--radix-popper-anchor-width) overflow-hidden rounded-lg border shadow-md"
|
||||
>
|
||||
<div className="border-input flex items-center gap-1 border-b px-2 py-1">
|
||||
<MagnifyingGlassIcon className="text-muted-foreground size-3.5 shrink-0" />
|
||||
|
||||
10
src/components/ui/spinner.tsx
Normal file
10
src/components/ui/spinner.tsx
Normal file
@@ -0,0 +1,10 @@
|
||||
import { cn } from "@/lib/utils"
|
||||
import { SpinnerIcon } from "@phosphor-icons/react"
|
||||
|
||||
function Spinner({ className, ...props }: React.ComponentProps<"svg">) {
|
||||
return (
|
||||
<SpinnerIcon role="status" aria-label="Loading" className={cn("size-4 animate-spin", className)} {...props} />
|
||||
)
|
||||
}
|
||||
|
||||
export { Spinner }
|
||||
@@ -1,4 +1,3 @@
|
||||
import { ReturnError } from '@/types/common';
|
||||
import usePreventAutoFocus from '@hooks/use-prevent-auto-focus';
|
||||
import { m } from '@paraglide/messages';
|
||||
import { ShieldWarningIcon } from '@phosphor-icons/react';
|
||||
@@ -17,6 +16,7 @@ import {
|
||||
} from '@ui/dialog';
|
||||
import { UserWithRole } from 'better-auth/plugins';
|
||||
import { toast } from 'sonner';
|
||||
import { Spinner } from '../ui/spinner';
|
||||
import {
|
||||
Table,
|
||||
TableBody,
|
||||
@@ -36,7 +36,7 @@ const BanUserConfirm = ({ data }: BanConfirmProps) => {
|
||||
const queryClient = useQueryClient();
|
||||
const prevent = usePreventAutoFocus();
|
||||
|
||||
const { mutate: banUserMutation } = useMutation({
|
||||
const { mutate: banUserMutation, isPending } = useMutation({
|
||||
mutationFn: banUser,
|
||||
onSuccess: () => {
|
||||
queryClient.refetchQueries({
|
||||
@@ -130,7 +130,13 @@ const BanUserConfirm = ({ data }: BanConfirmProps) => {
|
||||
{m.ui_cancel_btn()}
|
||||
</Button>
|
||||
</DialogClose>
|
||||
<Button variant="destructive" type="button" onClick={onConfirm}>
|
||||
<Button
|
||||
variant="destructive"
|
||||
type="button"
|
||||
onClick={onConfirm}
|
||||
disabled={isPending}
|
||||
>
|
||||
{isPending && <Spinner data-icon="inline-start" />}
|
||||
{m.ui_confirm_btn()}
|
||||
</Button>
|
||||
</DialogFooter>
|
||||
|
||||
@@ -1,4 +1,3 @@
|
||||
import { ReturnError } from '@/types/common';
|
||||
import useHasPermission from '@hooks/use-has-permission';
|
||||
import usePreventAutoFocus from '@hooks/use-prevent-auto-focus';
|
||||
import { m } from '@paraglide/messages';
|
||||
@@ -23,7 +22,15 @@ import { Tooltip, TooltipContent, TooltipTrigger } from '@ui/tooltip';
|
||||
import { UserWithRole } from 'better-auth/plugins';
|
||||
import { useState } from 'react';
|
||||
import { toast } from 'sonner';
|
||||
import DisplayBreakLineMessage from '../DisplayBreakLineMessage';
|
||||
import { Spinner } from '../ui/spinner';
|
||||
import {
|
||||
Table,
|
||||
TableBody,
|
||||
TableCell,
|
||||
TableHead,
|
||||
TableHeader,
|
||||
TableRow,
|
||||
} from '../ui/table';
|
||||
|
||||
type UnbanUserProps = {
|
||||
data: UserWithRole;
|
||||
@@ -38,7 +45,7 @@ const UnbanUserAction = ({ data }: UnbanUserProps) => {
|
||||
const [_open, _setOpen] = useState(false);
|
||||
const prevent = usePreventAutoFocus();
|
||||
|
||||
const { mutate: unbanMutation } = useMutation({
|
||||
const { mutate: unbanMutation, isPending } = useMutation({
|
||||
mutationFn: unbanUser,
|
||||
onSuccess: () => {
|
||||
queryClient.refetchQueries({
|
||||
@@ -107,19 +114,47 @@ const UnbanUserAction = ({ data }: UnbanUserProps) => {
|
||||
{m.users_page_ui_dialog_alert_title()}
|
||||
</DialogDescription>
|
||||
</DialogHeader>
|
||||
<DisplayBreakLineMessage>
|
||||
{m.users_page_ui_dialog_alert_description({
|
||||
name: data.name,
|
||||
email: data.email,
|
||||
})}
|
||||
</DisplayBreakLineMessage>
|
||||
<div className="overflow-hidden rounded-md border">
|
||||
<Table className="bg-white">
|
||||
<TableHeader>
|
||||
<TableRow className="bg-primary">
|
||||
<TableHead
|
||||
className="px-2 h-7 text-white text-xs"
|
||||
colSpan={2}
|
||||
>
|
||||
{m.users_page_ui_dialog_alert_description_title()}
|
||||
</TableHead>
|
||||
</TableRow>
|
||||
</TableHeader>
|
||||
<TableBody>
|
||||
<TableRow>
|
||||
<TableCell className="font-bold">
|
||||
{m.users_page_ui_table_header_name()}:
|
||||
</TableCell>
|
||||
<TableCell>{data.name}</TableCell>
|
||||
</TableRow>
|
||||
<TableRow>
|
||||
<TableCell className="font-bold">
|
||||
{m.users_page_ui_table_header_email()}:
|
||||
</TableCell>
|
||||
<TableCell>{data.email}</TableCell>
|
||||
</TableRow>
|
||||
</TableBody>
|
||||
</Table>
|
||||
</div>
|
||||
<DialogFooter className="bg-muted/50 -mx-4 -mb-4 rounded-b-xl border-t p-4">
|
||||
<DialogClose asChild>
|
||||
<Button variant="outline" type="button">
|
||||
{m.ui_cancel_btn()}
|
||||
</Button>
|
||||
</DialogClose>
|
||||
<Button variant="destructive" type="button" onClick={onConfirm}>
|
||||
<Button
|
||||
variant="destructive"
|
||||
type="button"
|
||||
onClick={onConfirm}
|
||||
disabled={isPending}
|
||||
>
|
||||
{isPending && <Spinner data-icon="inline-start" />}
|
||||
{m.ui_confirm_btn()}
|
||||
</Button>
|
||||
</DialogFooter>
|
||||
|
||||
@@ -9,7 +9,7 @@ import {
|
||||
owner,
|
||||
} from '@lib/auth/organization-permissions';
|
||||
import { ac, admin, user } from '@lib/auth/permissions';
|
||||
import { createAuditLog } from '@service/repository';
|
||||
import { createAuditLog, getInitialOrganization } from '@service/repository';
|
||||
import { betterAuth } from 'better-auth';
|
||||
import { prismaAdapter } from 'better-auth/adapters/prisma';
|
||||
import { admin as adminPlugin, organization } from 'better-auth/plugins';
|
||||
@@ -117,6 +117,15 @@ export const auth = betterAuth({
|
||||
},
|
||||
session: {
|
||||
create: {
|
||||
before: async (session) => {
|
||||
const organization = await getInitialOrganization(session.userId);
|
||||
return {
|
||||
data: {
|
||||
...session,
|
||||
activeOrganizationId: organization?.id,
|
||||
},
|
||||
};
|
||||
},
|
||||
after: async (session, context) => {
|
||||
if (context?.path.includes('/sign-in')) {
|
||||
await createAuditLog({
|
||||
|
||||
@@ -14,11 +14,14 @@ import { Route as appIndexRouteImport } from './routes/(app)/index'
|
||||
import { Route as authSignInRouteImport } from './routes/(auth)/sign-in'
|
||||
import { Route as appauthRouteRouteImport } from './routes/(app)/(auth)/route'
|
||||
import { Route as ApiAuthSplatRouteImport } from './routes/api.auth.$'
|
||||
import { Route as appauthDashboardRouteImport } from './routes/(app)/(auth)/dashboard'
|
||||
import { Route as appauthManagementRouteRouteImport } from './routes/(app)/(auth)/management/route'
|
||||
import { Route as appauthKanriRouteRouteImport } from './routes/(app)/(auth)/kanri/route'
|
||||
import { Route as appauthAccountRouteRouteImport } from './routes/(app)/(auth)/account/route'
|
||||
import { Route as appauthManagementIndexRouteImport } from './routes/(app)/(auth)/management/index'
|
||||
import { Route as appauthKanriIndexRouteImport } from './routes/(app)/(auth)/kanri/index'
|
||||
import { Route as appauthAccountIndexRouteImport } from './routes/(app)/(auth)/account/index'
|
||||
import { Route as appauthManagementHousesRouteImport } from './routes/(app)/(auth)/management/houses'
|
||||
import { Route as appauthManagementDashboardRouteImport } from './routes/(app)/(auth)/management/dashboard'
|
||||
import { Route as appauthKanriUsersRouteImport } from './routes/(app)/(auth)/kanri/users'
|
||||
import { Route as appauthKanriSettingsRouteImport } from './routes/(app)/(auth)/kanri/settings'
|
||||
import { Route as appauthKanriLogsRouteImport } from './routes/(app)/(auth)/kanri/logs'
|
||||
@@ -50,9 +53,9 @@ const ApiAuthSplatRoute = ApiAuthSplatRouteImport.update({
|
||||
path: '/api/auth/$',
|
||||
getParentRoute: () => rootRouteImport,
|
||||
} as any)
|
||||
const appauthDashboardRoute = appauthDashboardRouteImport.update({
|
||||
id: '/dashboard',
|
||||
path: '/dashboard',
|
||||
const appauthManagementRouteRoute = appauthManagementRouteRouteImport.update({
|
||||
id: '/management',
|
||||
path: '/management',
|
||||
getParentRoute: () => appauthRouteRoute,
|
||||
} as any)
|
||||
const appauthKanriRouteRoute = appauthKanriRouteRouteImport.update({
|
||||
@@ -65,6 +68,11 @@ const appauthAccountRouteRoute = appauthAccountRouteRouteImport.update({
|
||||
path: '/account',
|
||||
getParentRoute: () => appauthRouteRoute,
|
||||
} as any)
|
||||
const appauthManagementIndexRoute = appauthManagementIndexRouteImport.update({
|
||||
id: '/',
|
||||
path: '/',
|
||||
getParentRoute: () => appauthManagementRouteRoute,
|
||||
} as any)
|
||||
const appauthKanriIndexRoute = appauthKanriIndexRouteImport.update({
|
||||
id: '/',
|
||||
path: '/',
|
||||
@@ -75,6 +83,17 @@ const appauthAccountIndexRoute = appauthAccountIndexRouteImport.update({
|
||||
path: '/',
|
||||
getParentRoute: () => appauthAccountRouteRoute,
|
||||
} as any)
|
||||
const appauthManagementHousesRoute = appauthManagementHousesRouteImport.update({
|
||||
id: '/houses',
|
||||
path: '/houses',
|
||||
getParentRoute: () => appauthManagementRouteRoute,
|
||||
} as any)
|
||||
const appauthManagementDashboardRoute =
|
||||
appauthManagementDashboardRouteImport.update({
|
||||
id: '/dashboard',
|
||||
path: '/dashboard',
|
||||
getParentRoute: () => appauthManagementRouteRoute,
|
||||
} as any)
|
||||
const appauthKanriUsersRoute = appauthKanriUsersRouteImport.update({
|
||||
id: '/users',
|
||||
path: '/users',
|
||||
@@ -117,7 +136,7 @@ export interface FileRoutesByFullPath {
|
||||
'/': typeof appIndexRoute
|
||||
'/account': typeof appauthAccountRouteRouteWithChildren
|
||||
'/kanri': typeof appauthKanriRouteRouteWithChildren
|
||||
'/dashboard': typeof appauthDashboardRoute
|
||||
'/management': typeof appauthManagementRouteRouteWithChildren
|
||||
'/api/auth/$': typeof ApiAuthSplatRoute
|
||||
'/account/change-password': typeof appauthAccountChangePasswordRoute
|
||||
'/account/profile': typeof appauthAccountProfileRoute
|
||||
@@ -126,13 +145,15 @@ export interface FileRoutesByFullPath {
|
||||
'/kanri/logs': typeof appauthKanriLogsRoute
|
||||
'/kanri/settings': typeof appauthKanriSettingsRoute
|
||||
'/kanri/users': typeof appauthKanriUsersRoute
|
||||
'/management/dashboard': typeof appauthManagementDashboardRoute
|
||||
'/management/houses': typeof appauthManagementHousesRoute
|
||||
'/account/': typeof appauthAccountIndexRoute
|
||||
'/kanri/': typeof appauthKanriIndexRoute
|
||||
'/management/': typeof appauthManagementIndexRoute
|
||||
}
|
||||
export interface FileRoutesByTo {
|
||||
'/sign-in': typeof authSignInRoute
|
||||
'/': typeof appIndexRoute
|
||||
'/dashboard': typeof appauthDashboardRoute
|
||||
'/api/auth/$': typeof ApiAuthSplatRoute
|
||||
'/account/change-password': typeof appauthAccountChangePasswordRoute
|
||||
'/account/profile': typeof appauthAccountProfileRoute
|
||||
@@ -141,8 +162,11 @@ export interface FileRoutesByTo {
|
||||
'/kanri/logs': typeof appauthKanriLogsRoute
|
||||
'/kanri/settings': typeof appauthKanriSettingsRoute
|
||||
'/kanri/users': typeof appauthKanriUsersRoute
|
||||
'/management/dashboard': typeof appauthManagementDashboardRoute
|
||||
'/management/houses': typeof appauthManagementHousesRoute
|
||||
'/account': typeof appauthAccountIndexRoute
|
||||
'/kanri': typeof appauthKanriIndexRoute
|
||||
'/management': typeof appauthManagementIndexRoute
|
||||
}
|
||||
export interface FileRoutesById {
|
||||
__root__: typeof rootRouteImport
|
||||
@@ -152,7 +176,7 @@ export interface FileRoutesById {
|
||||
'/(app)/': typeof appIndexRoute
|
||||
'/(app)/(auth)/account': typeof appauthAccountRouteRouteWithChildren
|
||||
'/(app)/(auth)/kanri': typeof appauthKanriRouteRouteWithChildren
|
||||
'/(app)/(auth)/dashboard': typeof appauthDashboardRoute
|
||||
'/(app)/(auth)/management': typeof appauthManagementRouteRouteWithChildren
|
||||
'/api/auth/$': typeof ApiAuthSplatRoute
|
||||
'/(app)/(auth)/account/change-password': typeof appauthAccountChangePasswordRoute
|
||||
'/(app)/(auth)/account/profile': typeof appauthAccountProfileRoute
|
||||
@@ -161,8 +185,11 @@ export interface FileRoutesById {
|
||||
'/(app)/(auth)/kanri/logs': typeof appauthKanriLogsRoute
|
||||
'/(app)/(auth)/kanri/settings': typeof appauthKanriSettingsRoute
|
||||
'/(app)/(auth)/kanri/users': typeof appauthKanriUsersRoute
|
||||
'/(app)/(auth)/management/dashboard': typeof appauthManagementDashboardRoute
|
||||
'/(app)/(auth)/management/houses': typeof appauthManagementHousesRoute
|
||||
'/(app)/(auth)/account/': typeof appauthAccountIndexRoute
|
||||
'/(app)/(auth)/kanri/': typeof appauthKanriIndexRoute
|
||||
'/(app)/(auth)/management/': typeof appauthManagementIndexRoute
|
||||
}
|
||||
export interface FileRouteTypes {
|
||||
fileRoutesByFullPath: FileRoutesByFullPath
|
||||
@@ -171,7 +198,7 @@ export interface FileRouteTypes {
|
||||
| '/'
|
||||
| '/account'
|
||||
| '/kanri'
|
||||
| '/dashboard'
|
||||
| '/management'
|
||||
| '/api/auth/$'
|
||||
| '/account/change-password'
|
||||
| '/account/profile'
|
||||
@@ -180,13 +207,15 @@ export interface FileRouteTypes {
|
||||
| '/kanri/logs'
|
||||
| '/kanri/settings'
|
||||
| '/kanri/users'
|
||||
| '/management/dashboard'
|
||||
| '/management/houses'
|
||||
| '/account/'
|
||||
| '/kanri/'
|
||||
| '/management/'
|
||||
fileRoutesByTo: FileRoutesByTo
|
||||
to:
|
||||
| '/sign-in'
|
||||
| '/'
|
||||
| '/dashboard'
|
||||
| '/api/auth/$'
|
||||
| '/account/change-password'
|
||||
| '/account/profile'
|
||||
@@ -195,8 +224,11 @@ export interface FileRouteTypes {
|
||||
| '/kanri/logs'
|
||||
| '/kanri/settings'
|
||||
| '/kanri/users'
|
||||
| '/management/dashboard'
|
||||
| '/management/houses'
|
||||
| '/account'
|
||||
| '/kanri'
|
||||
| '/management'
|
||||
id:
|
||||
| '__root__'
|
||||
| '/(app)'
|
||||
@@ -205,7 +237,7 @@ export interface FileRouteTypes {
|
||||
| '/(app)/'
|
||||
| '/(app)/(auth)/account'
|
||||
| '/(app)/(auth)/kanri'
|
||||
| '/(app)/(auth)/dashboard'
|
||||
| '/(app)/(auth)/management'
|
||||
| '/api/auth/$'
|
||||
| '/(app)/(auth)/account/change-password'
|
||||
| '/(app)/(auth)/account/profile'
|
||||
@@ -214,8 +246,11 @@ export interface FileRouteTypes {
|
||||
| '/(app)/(auth)/kanri/logs'
|
||||
| '/(app)/(auth)/kanri/settings'
|
||||
| '/(app)/(auth)/kanri/users'
|
||||
| '/(app)/(auth)/management/dashboard'
|
||||
| '/(app)/(auth)/management/houses'
|
||||
| '/(app)/(auth)/account/'
|
||||
| '/(app)/(auth)/kanri/'
|
||||
| '/(app)/(auth)/management/'
|
||||
fileRoutesById: FileRoutesById
|
||||
}
|
||||
export interface RootRouteChildren {
|
||||
@@ -261,11 +296,11 @@ declare module '@tanstack/react-router' {
|
||||
preLoaderRoute: typeof ApiAuthSplatRouteImport
|
||||
parentRoute: typeof rootRouteImport
|
||||
}
|
||||
'/(app)/(auth)/dashboard': {
|
||||
id: '/(app)/(auth)/dashboard'
|
||||
path: '/dashboard'
|
||||
fullPath: '/dashboard'
|
||||
preLoaderRoute: typeof appauthDashboardRouteImport
|
||||
'/(app)/(auth)/management': {
|
||||
id: '/(app)/(auth)/management'
|
||||
path: '/management'
|
||||
fullPath: '/management'
|
||||
preLoaderRoute: typeof appauthManagementRouteRouteImport
|
||||
parentRoute: typeof appauthRouteRoute
|
||||
}
|
||||
'/(app)/(auth)/kanri': {
|
||||
@@ -282,6 +317,13 @@ declare module '@tanstack/react-router' {
|
||||
preLoaderRoute: typeof appauthAccountRouteRouteImport
|
||||
parentRoute: typeof appauthRouteRoute
|
||||
}
|
||||
'/(app)/(auth)/management/': {
|
||||
id: '/(app)/(auth)/management/'
|
||||
path: '/'
|
||||
fullPath: '/management/'
|
||||
preLoaderRoute: typeof appauthManagementIndexRouteImport
|
||||
parentRoute: typeof appauthManagementRouteRoute
|
||||
}
|
||||
'/(app)/(auth)/kanri/': {
|
||||
id: '/(app)/(auth)/kanri/'
|
||||
path: '/'
|
||||
@@ -296,6 +338,20 @@ declare module '@tanstack/react-router' {
|
||||
preLoaderRoute: typeof appauthAccountIndexRouteImport
|
||||
parentRoute: typeof appauthAccountRouteRoute
|
||||
}
|
||||
'/(app)/(auth)/management/houses': {
|
||||
id: '/(app)/(auth)/management/houses'
|
||||
path: '/houses'
|
||||
fullPath: '/management/houses'
|
||||
preLoaderRoute: typeof appauthManagementHousesRouteImport
|
||||
parentRoute: typeof appauthManagementRouteRoute
|
||||
}
|
||||
'/(app)/(auth)/management/dashboard': {
|
||||
id: '/(app)/(auth)/management/dashboard'
|
||||
path: '/dashboard'
|
||||
fullPath: '/management/dashboard'
|
||||
preLoaderRoute: typeof appauthManagementDashboardRouteImport
|
||||
parentRoute: typeof appauthManagementRouteRoute
|
||||
}
|
||||
'/(app)/(auth)/kanri/users': {
|
||||
id: '/(app)/(auth)/kanri/users'
|
||||
path: '/users'
|
||||
@@ -384,16 +440,34 @@ const appauthKanriRouteRouteChildren: appauthKanriRouteRouteChildren = {
|
||||
const appauthKanriRouteRouteWithChildren =
|
||||
appauthKanriRouteRoute._addFileChildren(appauthKanriRouteRouteChildren)
|
||||
|
||||
interface appauthManagementRouteRouteChildren {
|
||||
appauthManagementDashboardRoute: typeof appauthManagementDashboardRoute
|
||||
appauthManagementHousesRoute: typeof appauthManagementHousesRoute
|
||||
appauthManagementIndexRoute: typeof appauthManagementIndexRoute
|
||||
}
|
||||
|
||||
const appauthManagementRouteRouteChildren: appauthManagementRouteRouteChildren =
|
||||
{
|
||||
appauthManagementDashboardRoute: appauthManagementDashboardRoute,
|
||||
appauthManagementHousesRoute: appauthManagementHousesRoute,
|
||||
appauthManagementIndexRoute: appauthManagementIndexRoute,
|
||||
}
|
||||
|
||||
const appauthManagementRouteRouteWithChildren =
|
||||
appauthManagementRouteRoute._addFileChildren(
|
||||
appauthManagementRouteRouteChildren,
|
||||
)
|
||||
|
||||
interface appauthRouteRouteChildren {
|
||||
appauthAccountRouteRoute: typeof appauthAccountRouteRouteWithChildren
|
||||
appauthKanriRouteRoute: typeof appauthKanriRouteRouteWithChildren
|
||||
appauthDashboardRoute: typeof appauthDashboardRoute
|
||||
appauthManagementRouteRoute: typeof appauthManagementRouteRouteWithChildren
|
||||
}
|
||||
|
||||
const appauthRouteRouteChildren: appauthRouteRouteChildren = {
|
||||
appauthAccountRouteRoute: appauthAccountRouteRouteWithChildren,
|
||||
appauthKanriRouteRoute: appauthKanriRouteRouteWithChildren,
|
||||
appauthDashboardRoute: appauthDashboardRoute,
|
||||
appauthManagementRouteRoute: appauthManagementRouteRouteWithChildren,
|
||||
}
|
||||
|
||||
const appauthRouteRouteWithChildren = appauthRouteRoute._addFileChildren(
|
||||
|
||||
@@ -14,6 +14,7 @@ import { useState } from 'react';
|
||||
|
||||
export const Route = createFileRoute('/(app)/(auth)/kanri/houses')({
|
||||
component: RouteComponent,
|
||||
staticData: { breadcrumb: () => m.nav_houses() },
|
||||
});
|
||||
|
||||
function RouteComponent() {
|
||||
@@ -54,7 +55,7 @@ function RouteComponent() {
|
||||
</CardTitle>
|
||||
</CardHeader>
|
||||
<CardContent className="flex flex-col gap-4">
|
||||
<div className="flex items-center justify-between">
|
||||
<div className="flex items-center justify-between gap-2">
|
||||
<SearchInput
|
||||
keywords={searchKeyword}
|
||||
setKeyword={setSearchKeyword}
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import { m } from '@paraglide/messages';
|
||||
import { createFileRoute } from '@tanstack/react-router';
|
||||
|
||||
export const Route = createFileRoute('/(app)/(auth)/dashboard')({
|
||||
export const Route = createFileRoute('/(app)/(auth)/management/dashboard')({
|
||||
component: RouteComponent,
|
||||
staticData: { breadcrumb: () => m.nav_dashboard() },
|
||||
});
|
||||
30
src/routes/(app)/(auth)/management/houses.tsx
Normal file
30
src/routes/(app)/(auth)/management/houses.tsx
Normal file
@@ -0,0 +1,30 @@
|
||||
import CurrentUserActionGroup from '@/components/house/current-user-action-group';
|
||||
import CurrentUserHouseList from '@/components/house/current-user-house-list';
|
||||
import CurrentUserMemberList from '@/components/house/current-user-member-list';
|
||||
import { m } from '@/paraglide/messages';
|
||||
import { housesQueries } from '@/service/queries';
|
||||
import { authClient } from '@lib/auth-client';
|
||||
import { useQuery } from '@tanstack/react-query';
|
||||
import { createFileRoute } from '@tanstack/react-router';
|
||||
|
||||
export const Route = createFileRoute('/(app)/(auth)/management/houses')({
|
||||
component: RouteComponent,
|
||||
staticData: { breadcrumb: () => m.nav_houses() },
|
||||
});
|
||||
|
||||
function RouteComponent() {
|
||||
const { data: houses } = useQuery(housesQueries.currentUser());
|
||||
const { data: activeHouse } = authClient.useActiveOrganization();
|
||||
|
||||
if (!activeHouse || !houses) return null;
|
||||
|
||||
return (
|
||||
<div className="@container/main p-4">
|
||||
<div className="*:data-[slot=card]:from-primary/5 *:data-[slot=card]:to-card dark:*:data-[slot=card]:bg-card *:data-[slot=card]:bg-linear-to-br *:data-[slot=card]:shadow-xs grid grid-cols-1 @xl/main:grid-cols-2 @5xl/main:grid-cols-3 gap-4">
|
||||
<CurrentUserHouseList activeHouse={activeHouse} />
|
||||
<CurrentUserMemberList activeHouse={activeHouse} />
|
||||
<CurrentUserActionGroup activeHouse={activeHouse} />
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
9
src/routes/(app)/(auth)/management/index.tsx
Normal file
9
src/routes/(app)/(auth)/management/index.tsx
Normal file
@@ -0,0 +1,9 @@
|
||||
import { createFileRoute } from '@tanstack/react-router'
|
||||
|
||||
export const Route = createFileRoute('/(app)/(auth)/management/')({
|
||||
component: RouteComponent,
|
||||
})
|
||||
|
||||
function RouteComponent() {
|
||||
return <div>Hello "/(app)/(auth)/management/"!</div>
|
||||
}
|
||||
6
src/routes/(app)/(auth)/management/route.tsx
Normal file
6
src/routes/(app)/(auth)/management/route.tsx
Normal file
@@ -0,0 +1,6 @@
|
||||
import { m } from '@/paraglide/messages';
|
||||
import { createFileRoute } from '@tanstack/react-router';
|
||||
|
||||
export const Route = createFileRoute('/(app)/(auth)/management')({
|
||||
staticData: { breadcrumb: () => m.nav_label_management() },
|
||||
});
|
||||
@@ -23,7 +23,7 @@ function RouteComponent() {
|
||||
}, [language]);
|
||||
return (
|
||||
<AuthProvider>
|
||||
<SidebarProvider defaultOpen={false}>
|
||||
<SidebarProvider>
|
||||
<AppSidebar />
|
||||
<SidebarInset>
|
||||
<Header />
|
||||
|
||||
@@ -4,6 +4,7 @@ import { DB_TABLE, LOG_ACTION } from '@/types/enum';
|
||||
import { auth } from '@lib/auth';
|
||||
import { authMiddleware } from '@lib/middleware';
|
||||
import { createServerFn } from '@tanstack/react-start';
|
||||
import { getRequestHeaders } from '@tanstack/react-start/server';
|
||||
import { parseError } from '@utils/helper';
|
||||
import {
|
||||
baseHouse,
|
||||
@@ -43,6 +44,7 @@ export const getAllHouse = createServerFn({ method: 'GET' })
|
||||
role: true,
|
||||
user: {
|
||||
select: {
|
||||
id: true,
|
||||
name: true,
|
||||
email: true,
|
||||
image: true,
|
||||
@@ -74,6 +76,43 @@ export const getAllHouse = createServerFn({ method: 'GET' })
|
||||
}
|
||||
});
|
||||
|
||||
export const getCurrentUserHouses = createServerFn({ method: 'GET' })
|
||||
.middleware([authMiddleware])
|
||||
.handler(async ({ context: { user } }) => {
|
||||
try {
|
||||
const houses = await prisma.organization.findMany({
|
||||
where: { members: { some: { userId: user.id } } },
|
||||
orderBy: { createdAt: 'asc' },
|
||||
include: {
|
||||
_count: {
|
||||
select: {
|
||||
members: true,
|
||||
},
|
||||
},
|
||||
members: {
|
||||
select: {
|
||||
role: true,
|
||||
user: {
|
||||
select: {
|
||||
id: true,
|
||||
name: true,
|
||||
email: true,
|
||||
image: true,
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
return houses;
|
||||
} catch (error) {
|
||||
console.error(error);
|
||||
const { message, code } = parseError(error);
|
||||
throw { message, code };
|
||||
}
|
||||
});
|
||||
|
||||
export const createHouse = createServerFn({ method: 'POST' })
|
||||
.middleware([authMiddleware])
|
||||
.inputValidator(houseCreateBESchema)
|
||||
@@ -177,3 +216,40 @@ export const deleteHouse = createServerFn({ method: 'POST' })
|
||||
throw { message, code };
|
||||
}
|
||||
});
|
||||
|
||||
export const deleteUserHouse = createServerFn({ method: 'POST' })
|
||||
.middleware([authMiddleware])
|
||||
.inputValidator(baseHouse)
|
||||
.handler(async ({ data, context: { user } }) => {
|
||||
try {
|
||||
const currentHouse = await prisma.organization.findUnique({
|
||||
where: { id: data.id },
|
||||
});
|
||||
if (!currentHouse) throw Error('House not found');
|
||||
|
||||
const headers = getRequestHeaders();
|
||||
const result = await auth.api.deleteOrganization({
|
||||
body: {
|
||||
organizationId: data.id, // required
|
||||
},
|
||||
headers,
|
||||
});
|
||||
|
||||
if (result) {
|
||||
await createAuditLog({
|
||||
action: LOG_ACTION.DELETE,
|
||||
tableName: DB_TABLE.ORGANIZATION,
|
||||
recordId: result?.id,
|
||||
oldValue: JSON.stringify(currentHouse),
|
||||
newValue: '',
|
||||
userId: user.id,
|
||||
});
|
||||
}
|
||||
|
||||
return result;
|
||||
} catch (error) {
|
||||
console.error(error);
|
||||
const { message, code } = parseError(error);
|
||||
throw { message, code };
|
||||
}
|
||||
});
|
||||
|
||||
@@ -27,6 +27,10 @@ export const houseCreateBESchema = houseCreateSchema.extend({
|
||||
slug: z.string().nonempty(m.common_is_required({ field: 'Slug' })),
|
||||
});
|
||||
|
||||
export const houseCreateByUserBESchema = houseCreateBESchema.omit({
|
||||
userId: true,
|
||||
});
|
||||
|
||||
export const houseEditSchema = baseHouse.extend({
|
||||
name: z
|
||||
.string()
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import { getSession } from '@lib/auth/session';
|
||||
import { queryOptions } from '@tanstack/react-query';
|
||||
import { getAllAudit } from './audit.api';
|
||||
import { getAllHouse } from './house.api';
|
||||
import { getAllHouse, getCurrentUserHouses } from './house.api';
|
||||
import {
|
||||
getAdminSettings,
|
||||
getCurrentUserLanguage,
|
||||
@@ -69,4 +69,9 @@ export const housesQueries = {
|
||||
queryKey: [...housesQueries.all, 'list', params],
|
||||
queryFn: () => getAllHouse({ data: params }),
|
||||
}),
|
||||
currentUser: () =>
|
||||
queryOptions({
|
||||
queryKey: [...housesQueries.all, 'currentUser'],
|
||||
queryFn: () => getCurrentUserHouses(),
|
||||
}),
|
||||
};
|
||||
|
||||
@@ -54,3 +54,16 @@ export const createAuditLog = async (data: Omit<Audit, 'id' | 'createdAt'>) => {
|
||||
throw { message, code };
|
||||
}
|
||||
};
|
||||
|
||||
export const getInitialOrganization = async (userId: string) => {
|
||||
const organization = await prisma.organization.findFirst({
|
||||
where: {
|
||||
members: {
|
||||
some: {
|
||||
userId,
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
return organization;
|
||||
};
|
||||
|
||||
4
src/types/common.d.ts
vendored
4
src/types/common.d.ts
vendored
@@ -1,4 +0,0 @@
|
||||
export interface ReturnError extends Error {
|
||||
message: string;
|
||||
code: string;
|
||||
}
|
||||
|
||||
6
src/types/db.d.ts
vendored
6
src/types/db.d.ts
vendored
@@ -19,6 +19,7 @@ declare global {
|
||||
role: true;
|
||||
user: {
|
||||
select: {
|
||||
id: true;
|
||||
name: true;
|
||||
email: true;
|
||||
image: true;
|
||||
@@ -28,4 +29,9 @@ declare global {
|
||||
};
|
||||
};
|
||||
}>;
|
||||
|
||||
type ReturnError = Error & {
|
||||
message: string;
|
||||
code: string;
|
||||
};
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user