added leave house, remove member
This commit is contained in:
@@ -55,6 +55,8 @@
|
|||||||
"ui_save_btn": "Save changes",
|
"ui_save_btn": "Save changes",
|
||||||
"ui_update_btn": "Update",
|
"ui_update_btn": "Update",
|
||||||
"ui_delete_btn": "Delete",
|
"ui_delete_btn": "Delete",
|
||||||
|
"ui_leave_btn": "Leave",
|
||||||
|
"ui_remove_btn": "Remove",
|
||||||
"ui_ban_btn": "Lock",
|
"ui_ban_btn": "Lock",
|
||||||
"ui_unban_btn": "Unlock",
|
"ui_unban_btn": "Unlock",
|
||||||
"ui_invite_btn": "Invite",
|
"ui_invite_btn": "Invite",
|
||||||
@@ -174,6 +176,12 @@
|
|||||||
"houses_page_form_user_select_search_placeholder": "Search by name or email...",
|
"houses_page_form_user_select_search_placeholder": "Search by name or email...",
|
||||||
"houses_page_ui_dialog_alert_delete_title": "Delete house: {name}?",
|
"houses_page_ui_dialog_alert_delete_title": "Delete house: {name}?",
|
||||||
"houses_page_ui_dialog_alert_delete_description": "This action cannot be undone! It will delete all related data like: <b>Box</b>, <b>Item</b>. Please think carefully!",
|
"houses_page_ui_dialog_alert_delete_description": "This action cannot be undone! It will delete all related data like: <b>Box</b>, <b>Item</b>. Please think carefully!",
|
||||||
|
"houses_page_ui_dialog_alert_leave_title": "You want to leave this house?",
|
||||||
|
"houses_page_ui_dialog_alert_leave_description": "When you leave, you should contact the owner if you want to join back! Are you sure?",
|
||||||
|
"houses_page_ui_dialog_alert_remove_title": "Do you want to remove {name} from this house?",
|
||||||
|
"houses_page_ui_dialog_alert_remove_description": "Once removed, you must invite them again and wait for the member to accept!",
|
||||||
|
"houses_page_message_leave_house_success": "Leaved house successfully!",
|
||||||
|
"houses_page_message_remove_member_success": "Removed member successfully!",
|
||||||
"houses_page_message_create_house_success": "Created house successfully!",
|
"houses_page_message_create_house_success": "Created house successfully!",
|
||||||
"houses_page_message_house_not_found": "House not found!",
|
"houses_page_message_house_not_found": "House not found!",
|
||||||
"houses_page_message_update_house_success": "Updated house successfully!",
|
"houses_page_message_update_house_success": "Updated house successfully!",
|
||||||
|
|||||||
@@ -28,7 +28,7 @@
|
|||||||
"role=admin": "Quản lý",
|
"role=admin": "Quản lý",
|
||||||
"role=user": "Người dùng",
|
"role=user": "Người dùng",
|
||||||
"role=member": "Thành viên",
|
"role=member": "Thành viên",
|
||||||
"role=owner": "Người sở hữu"
|
"role=owner": "Chủ nhà"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
@@ -55,6 +55,8 @@
|
|||||||
"ui_save_btn": "Lưu thay đổi",
|
"ui_save_btn": "Lưu thay đổi",
|
||||||
"ui_update_btn": "Cập nhật",
|
"ui_update_btn": "Cập nhật",
|
||||||
"ui_delete_btn": "Xóa",
|
"ui_delete_btn": "Xóa",
|
||||||
|
"ui_leave_btn": "Rời đi",
|
||||||
|
"ui_remove_btn": "Xóa",
|
||||||
"ui_ban_btn": "Khóa",
|
"ui_ban_btn": "Khóa",
|
||||||
"ui_unban_btn": "Mở khóa",
|
"ui_unban_btn": "Mở khóa",
|
||||||
"ui_invite_btn": "Mời",
|
"ui_invite_btn": "Mời",
|
||||||
@@ -178,6 +180,12 @@
|
|||||||
"houses_page_form_user_select_search_placeholder": "Tìm theo tên hoặc email...",
|
"houses_page_form_user_select_search_placeholder": "Tìm theo tên hoặc email...",
|
||||||
"houses_page_ui_dialog_alert_delete_title": "Bạn muốn xóa nhà này: {name}?",
|
"houses_page_ui_dialog_alert_delete_title": "Bạn muốn xóa nhà này: {name}?",
|
||||||
"houses_page_ui_dialog_alert_delete_description": "Thao tác này không thể hoàn tác! Nó sẽ xóa hết mọi dữ liệu liên quan như: <b>Hộp chứa</b>, <b>Vật Phẩm</b>. Xin suy tính kỹ lưỡng!",
|
"houses_page_ui_dialog_alert_delete_description": "Thao tác này không thể hoàn tác! Nó sẽ xóa hết mọi dữ liệu liên quan như: <b>Hộp chứa</b>, <b>Vật Phẩm</b>. Xin suy tính kỹ lưỡng!",
|
||||||
|
"houses_page_ui_dialog_alert_leave_title": "Bạn muốn rời khỏi nhà này?",
|
||||||
|
"houses_page_ui_dialog_alert_leave_description": "Một khi rời khỏi, bạn phải liên hệ chủ nhà nếu muốn được thêm vào lại! Bạn chắc chắn muốn rời khỏi nhà này?",
|
||||||
|
"houses_page_ui_dialog_alert_remove_title": "Bạn muốn xóa {name} khỏi nhà?",
|
||||||
|
"houses_page_ui_dialog_alert_remove_description": "Một khi bị xóa, bạn phải mời lại và đợi thành viên đồng ý!",
|
||||||
|
"houses_page_message_leave_house_success": "Rời khỏi nhà thành công!",
|
||||||
|
"houses_page_message_remove_member_success": "Xóa thành viên thành công!",
|
||||||
"houses_page_message_create_house_success": "Tạo nhà thành công!",
|
"houses_page_message_create_house_success": "Tạo nhà thành công!",
|
||||||
"houses_page_message_house_not_found": "Không tìm thấy nhà này!",
|
"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_update_house_success": "Cập nhật nhà thành công!",
|
||||||
|
|||||||
@@ -22,7 +22,7 @@ export const userData = [
|
|||||||
email: 'raysam024@gmail.com',
|
email: 'raysam024@gmail.com',
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: 'Raysam',
|
name: 'Sam Miu',
|
||||||
email: 'juines.liu@gmail.com',
|
email: 'juines.liu@gmail.com',
|
||||||
},
|
},
|
||||||
];
|
];
|
||||||
|
|||||||
@@ -5,7 +5,7 @@ import Notification from './Notification';
|
|||||||
import RouterBreadcrumb from './sidebar/router-breadcrumb';
|
import RouterBreadcrumb from './sidebar/router-breadcrumb';
|
||||||
|
|
||||||
export default function Header() {
|
export default function Header() {
|
||||||
const { data: session } = useAuth();
|
const { session } = useAuth();
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
|
|||||||
@@ -4,7 +4,7 @@ import { useQuery } from '@tanstack/react-query';
|
|||||||
import { createContext, useContext, useMemo } from 'react';
|
import { createContext, useContext, useMemo } from 'react';
|
||||||
|
|
||||||
export type UserContext = {
|
export type UserContext = {
|
||||||
data: ClientSession;
|
session: ClientSession;
|
||||||
isAuth: boolean;
|
isAuth: boolean;
|
||||||
isAdmin: boolean;
|
isAdmin: boolean;
|
||||||
isPending: boolean;
|
isPending: boolean;
|
||||||
@@ -18,7 +18,7 @@ export function AuthProvider({ children }: { children: React.ReactNode }) {
|
|||||||
|
|
||||||
const contextSession: UserContext = useMemo(
|
const contextSession: UserContext = useMemo(
|
||||||
() => ({
|
() => ({
|
||||||
data: session as ClientSession,
|
session: session as ClientSession,
|
||||||
isPending,
|
isPending,
|
||||||
error,
|
error,
|
||||||
isAuth: !!session,
|
isAuth: !!session,
|
||||||
|
|||||||
@@ -9,7 +9,7 @@ interface AvatarUserProps {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const AvatarUser = ({ className, textSize = 'md' }: AvatarUserProps) => {
|
const AvatarUser = ({ className, textSize = 'md' }: AvatarUserProps) => {
|
||||||
const { data: session } = useAuth();
|
const { session } = useAuth();
|
||||||
const imagePath = session?.user?.image
|
const imagePath = session?.user?.image
|
||||||
? new URL(`../../../data/avatar/${session?.user?.image}`, import.meta.url)
|
? new URL(`../../../data/avatar/${session?.user?.image}`, import.meta.url)
|
||||||
.href
|
.href
|
||||||
|
|||||||
@@ -21,7 +21,7 @@ const defaultValues: ProfileInput = {
|
|||||||
|
|
||||||
const ProfileForm = () => {
|
const ProfileForm = () => {
|
||||||
const fileInputRef = useRef<HTMLInputElement>(null);
|
const fileInputRef = useRef<HTMLInputElement>(null);
|
||||||
const { data: session, isPending } = useAuth();
|
const { session, isPending } = useAuth();
|
||||||
const queryClient = useQueryClient();
|
const queryClient = useQueryClient();
|
||||||
|
|
||||||
const { mutate: updateProfileMutation, isPending: isRunning } = useMutation({
|
const { mutate: updateProfileMutation, isPending: isRunning } = useMutation({
|
||||||
|
|||||||
@@ -21,7 +21,7 @@ type FormProps = {
|
|||||||
};
|
};
|
||||||
|
|
||||||
const CreateNewHouseForm = ({ onSubmit, isPersonal = false }: FormProps) => {
|
const CreateNewHouseForm = ({ onSubmit, isPersonal = false }: FormProps) => {
|
||||||
const { data: session } = useAuth();
|
const { session } = useAuth();
|
||||||
const [userKeyword, setUserKeyword] = useState('');
|
const [userKeyword, setUserKeyword] = useState('');
|
||||||
const debouncedUserKeyword = useDebounced(userKeyword, 300);
|
const debouncedUserKeyword = useDebounced(userKeyword, 300);
|
||||||
const { data: users } = useQuery(
|
const { data: users } = useQuery(
|
||||||
|
|||||||
@@ -14,6 +14,7 @@ import {
|
|||||||
DialogTrigger,
|
DialogTrigger,
|
||||||
} from '@ui/dialog';
|
} from '@ui/dialog';
|
||||||
import { useState } from 'react';
|
import { useState } from 'react';
|
||||||
|
import { Skeleton } from '../ui/skeleton';
|
||||||
|
|
||||||
type CreateNewHouseProp = {
|
type CreateNewHouseProp = {
|
||||||
isPersonal?: boolean;
|
isPersonal?: boolean;
|
||||||
@@ -24,46 +25,42 @@ const CreateNewHouse = ({
|
|||||||
className,
|
className,
|
||||||
isPersonal = false,
|
isPersonal = false,
|
||||||
}: CreateNewHouseProp) => {
|
}: CreateNewHouseProp) => {
|
||||||
const { hasPermission, isLoading } = useHasPermission(
|
const { hasPermission, isLoading } = useHasPermission('house', 'create');
|
||||||
'house',
|
|
||||||
'create',
|
|
||||||
isPersonal,
|
|
||||||
);
|
|
||||||
const [_open, _setOpen] = useState(false);
|
const [_open, _setOpen] = useState(false);
|
||||||
const prevent = usePreventAutoFocus();
|
const prevent = usePreventAutoFocus();
|
||||||
|
|
||||||
if (isLoading) return null;
|
if (isLoading) {
|
||||||
|
return <Skeleton className={cn('h-7 w-23', className)} />;
|
||||||
if (hasPermission) {
|
|
||||||
return (
|
|
||||||
<Dialog open={_open} onOpenChange={_setOpen}>
|
|
||||||
<DialogTrigger asChild>
|
|
||||||
<Button type="button" variant="default" className={cn(className)}>
|
|
||||||
<PlusIcon />
|
|
||||||
{m.nav_add_new()}
|
|
||||||
</Button>
|
|
||||||
</DialogTrigger>
|
|
||||||
<DialogContent
|
|
||||||
className="max-w-80 xl:max-w-xl"
|
|
||||||
{...prevent}
|
|
||||||
onPointerDownOutside={(e) => e.preventDefault()}
|
|
||||||
>
|
|
||||||
<DialogHeader>
|
|
||||||
<DialogTitle className="flex items-center gap-3 text-lg font-bold text-primary">
|
|
||||||
<PlusIcon size={16} />
|
|
||||||
{m.nav_add_new()}
|
|
||||||
</DialogTitle>
|
|
||||||
<DialogDescription className="sr-only">
|
|
||||||
{m.nav_add_new()}
|
|
||||||
</DialogDescription>
|
|
||||||
</DialogHeader>
|
|
||||||
<CreateNewHouseForm onSubmit={_setOpen} isPersonal={isPersonal} />
|
|
||||||
</DialogContent>
|
|
||||||
</Dialog>
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return null;
|
if (!hasPermission) return null;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Dialog open={_open} onOpenChange={_setOpen}>
|
||||||
|
<DialogTrigger asChild>
|
||||||
|
<Button type="button" variant="default" className={cn(className)}>
|
||||||
|
<PlusIcon />
|
||||||
|
{m.nav_add_new()}
|
||||||
|
</Button>
|
||||||
|
</DialogTrigger>
|
||||||
|
<DialogContent
|
||||||
|
className="max-w-80 xl:max-w-xl"
|
||||||
|
{...prevent}
|
||||||
|
onPointerDownOutside={(e) => e.preventDefault()}
|
||||||
|
>
|
||||||
|
<DialogHeader>
|
||||||
|
<DialogTitle className="flex items-center gap-3 text-lg font-bold text-primary">
|
||||||
|
<PlusIcon size={16} />
|
||||||
|
{m.nav_add_new()}
|
||||||
|
</DialogTitle>
|
||||||
|
<DialogDescription className="sr-only">
|
||||||
|
{m.nav_add_new()}
|
||||||
|
</DialogDescription>
|
||||||
|
</DialogHeader>
|
||||||
|
<CreateNewHouseForm onSubmit={_setOpen} isPersonal={isPersonal} />
|
||||||
|
</DialogContent>
|
||||||
|
</Dialog>
|
||||||
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
export default CreateNewHouse;
|
export default CreateNewHouse;
|
||||||
|
|||||||
@@ -1,10 +1,11 @@
|
|||||||
import { authClient } from '@lib/auth-client';
|
import { authClient } from '@lib/auth-client';
|
||||||
import { m } from '@paraglide/messages';
|
import { m } from '@paraglide/messages';
|
||||||
import { GearIcon, PenIcon } from '@phosphor-icons/react';
|
import { GearIcon } from '@phosphor-icons/react';
|
||||||
import { Button } from '@ui/button';
|
|
||||||
import { Card, CardContent, CardHeader, CardTitle } from '@ui/card';
|
import { Card, CardContent, CardHeader, CardTitle } from '@ui/card';
|
||||||
|
import { Skeleton } from '../ui/skeleton';
|
||||||
import DeleteUserHouseAction from './delete-user-house-dialog';
|
import DeleteUserHouseAction from './delete-user-house-dialog';
|
||||||
import EditHouseAction from './edit-house-dialog';
|
import EditUserHouseAction from './edit-user-house-dialog';
|
||||||
|
import LeaveHouseAction from './leave-house-dialog';
|
||||||
|
|
||||||
type CurrentUserActionGroupProps = {
|
type CurrentUserActionGroupProps = {
|
||||||
oneHouse: boolean;
|
oneHouse: boolean;
|
||||||
@@ -15,6 +16,10 @@ const CurrentUserActionGroup = ({
|
|||||||
oneHouse,
|
oneHouse,
|
||||||
activeHouse,
|
activeHouse,
|
||||||
}: CurrentUserActionGroupProps) => {
|
}: CurrentUserActionGroupProps) => {
|
||||||
|
if (!activeHouse) {
|
||||||
|
return <Skeleton className="col-span-1" />;
|
||||||
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Card className="col-span-1">
|
<Card className="col-span-1">
|
||||||
<CardHeader>
|
<CardHeader>
|
||||||
@@ -24,17 +29,9 @@ const CurrentUserActionGroup = ({
|
|||||||
</CardTitle>
|
</CardTitle>
|
||||||
</CardHeader>
|
</CardHeader>
|
||||||
<CardContent className="flex flex-row gap-2">
|
<CardContent className="flex flex-row gap-2">
|
||||||
<EditHouseAction data={activeHouse as HouseWithMembers} isPersonal>
|
<EditUserHouseAction data={activeHouse} />
|
||||||
<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>
|
|
||||||
{!oneHouse && <DeleteUserHouseAction activeHouse={activeHouse} />}
|
{!oneHouse && <DeleteUserHouseAction activeHouse={activeHouse} />}
|
||||||
|
<LeaveHouseAction activeHouseId={activeHouse.id} />
|
||||||
</CardContent>
|
</CardContent>
|
||||||
</Card>
|
</Card>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -53,8 +53,10 @@ const CurrentUserInvitationList = ({ activeHouse }: InvitationListProps) => {
|
|||||||
cancelInvitationMutation({ data: { id } });
|
cancelInvitationMutation({ data: { id } });
|
||||||
};
|
};
|
||||||
|
|
||||||
if (!activeHouse) {
|
if (!activeHouse || isLoading) {
|
||||||
return <Skeleton className="col-span-2 h-80 w-full rounded-xl" />;
|
return (
|
||||||
|
<Skeleton className="col-span-1 lg:col-span-3 h-80 w-full rounded-xl" />
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
|
|||||||
@@ -10,16 +10,20 @@ import {
|
|||||||
TableHeader,
|
TableHeader,
|
||||||
TableRow,
|
TableRow,
|
||||||
} from '@ui/table';
|
} from '@ui/table';
|
||||||
|
import { useAuth } from '../auth/auth-provider';
|
||||||
import RoleBadge from '../avatar/role-badge';
|
import RoleBadge from '../avatar/role-badge';
|
||||||
import InviteUserAction from './invite-user-dialog';
|
import InviteUserAction from './invite-user-dialog';
|
||||||
|
import RemoveUserFormHouse from './remove-user-form-house';
|
||||||
|
|
||||||
type CurrentUserMemberListProps = {
|
type CurrentUserMemberListProps = {
|
||||||
activeHouse: ReturnType<typeof authClient.useActiveOrganization>['data'];
|
activeHouse: ReturnType<typeof authClient.useActiveOrganization>['data'];
|
||||||
};
|
};
|
||||||
|
|
||||||
const CurrentUserMemberList = ({ activeHouse }: CurrentUserMemberListProps) => {
|
const CurrentUserMemberList = ({ activeHouse }: CurrentUserMemberListProps) => {
|
||||||
|
const { session } = useAuth();
|
||||||
|
|
||||||
if (!activeHouse) {
|
if (!activeHouse) {
|
||||||
return <Skeleton className="col-span-2 h-80 w-full rounded-xl" />;
|
return <Skeleton className="col-span-1 lg:col-span-2 h-80 rounded-xl" />;
|
||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
@@ -57,7 +61,10 @@ const CurrentUserMemberList = ({ activeHouse }: CurrentUserMemberListProps) => {
|
|||||||
</TableCell>
|
</TableCell>
|
||||||
<TableCell className="px-4">
|
<TableCell className="px-4">
|
||||||
<div className="flex justify-end gap-2">
|
<div className="flex justify-end gap-2">
|
||||||
{activeHouse.color}
|
{member.role !== 'owner' &&
|
||||||
|
session.user.id !== member.user.id && (
|
||||||
|
<RemoveUserFormHouse member={member} />
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
</TableCell>
|
</TableCell>
|
||||||
</TableRow>
|
</TableRow>
|
||||||
|
|||||||
@@ -31,6 +31,7 @@ import parse from 'html-react-parser';
|
|||||||
import { useState } from 'react';
|
import { useState } from 'react';
|
||||||
import { toast } from 'sonner';
|
import { toast } from 'sonner';
|
||||||
import RoleBadge from '../avatar/role-badge';
|
import RoleBadge from '../avatar/role-badge';
|
||||||
|
import { Skeleton } from '../ui/skeleton';
|
||||||
|
|
||||||
type DeleteHouseProps = {
|
type DeleteHouseProps = {
|
||||||
data: HouseWithMembers;
|
data: HouseWithMembers;
|
||||||
@@ -69,93 +70,93 @@ const DeleteHouseAction = ({ data }: DeleteHouseProps) => {
|
|||||||
deleteHouseMutation({ data });
|
deleteHouseMutation({ data });
|
||||||
};
|
};
|
||||||
|
|
||||||
if (isLoading) return null;
|
if (isLoading) {
|
||||||
|
return <Skeleton className="h-8 w-8 rounded-full" />;
|
||||||
if (hasPermission) {
|
|
||||||
return (
|
|
||||||
<Dialog open={_open} onOpenChange={_setOpen}>
|
|
||||||
<Tooltip>
|
|
||||||
<TooltipTrigger asChild>
|
|
||||||
<DialogTrigger asChild>
|
|
||||||
<Button
|
|
||||||
type="button"
|
|
||||||
variant="ghost"
|
|
||||||
size="icon"
|
|
||||||
className="rounded-full cursor-pointer text-red-500 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: data.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>
|
|
||||||
{data.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;
|
if (!hasPermission) return null;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Dialog open={_open} onOpenChange={_setOpen}>
|
||||||
|
<Tooltip>
|
||||||
|
<TooltipTrigger asChild>
|
||||||
|
<DialogTrigger asChild>
|
||||||
|
<Button
|
||||||
|
type="button"
|
||||||
|
variant="ghost"
|
||||||
|
size="icon"
|
||||||
|
className="rounded-full cursor-pointer text-red-500 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: data.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>
|
||||||
|
{data.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>
|
||||||
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
export default DeleteHouseAction;
|
export default DeleteHouseAction;
|
||||||
|
|||||||
@@ -32,6 +32,7 @@ import parse from 'html-react-parser';
|
|||||||
import { useState } from 'react';
|
import { useState } from 'react';
|
||||||
import { toast } from 'sonner';
|
import { toast } from 'sonner';
|
||||||
import RoleBadge from '../avatar/role-badge';
|
import RoleBadge from '../avatar/role-badge';
|
||||||
|
import { Skeleton } from '../ui/skeleton';
|
||||||
|
|
||||||
type DeleteUserHouseProps = {
|
type DeleteUserHouseProps = {
|
||||||
activeHouse: ReturnType<typeof authClient.useActiveOrganization>['data'];
|
activeHouse: ReturnType<typeof authClient.useActiveOrganization>['data'];
|
||||||
@@ -40,7 +41,11 @@ type DeleteUserHouseProps = {
|
|||||||
const DeleteUserHouseAction = ({ activeHouse }: DeleteUserHouseProps) => {
|
const DeleteUserHouseAction = ({ activeHouse }: DeleteUserHouseProps) => {
|
||||||
const [_open, _setOpen] = useState(false);
|
const [_open, _setOpen] = useState(false);
|
||||||
const prevent = usePreventAutoFocus();
|
const prevent = usePreventAutoFocus();
|
||||||
const { hasPermission, isLoading } = useHasPermission('house', 'delete');
|
const { hasPermission, isLoading } = useHasPermission(
|
||||||
|
'house',
|
||||||
|
'delete',
|
||||||
|
true,
|
||||||
|
);
|
||||||
|
|
||||||
const queryClient = useQueryClient();
|
const queryClient = useQueryClient();
|
||||||
|
|
||||||
@@ -66,98 +71,98 @@ const DeleteUserHouseAction = ({ activeHouse }: DeleteUserHouseProps) => {
|
|||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
if (isLoading || !activeHouse) return null;
|
if (isLoading || !activeHouse) {
|
||||||
|
return <Skeleton className="h-8 w-8 rounded-full" />;
|
||||||
|
}
|
||||||
|
|
||||||
const onConfirm = async () => {
|
const onConfirm = async () => {
|
||||||
deleteHouseMutation({ data: { id: activeHouse.id } });
|
deleteHouseMutation({ data: { id: activeHouse.id } });
|
||||||
};
|
};
|
||||||
|
|
||||||
if (hasPermission) {
|
if (!hasPermission) return null;
|
||||||
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;
|
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>
|
||||||
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
export default DeleteUserHouseAction;
|
export default DeleteUserHouseAction;
|
||||||
|
|||||||
@@ -3,6 +3,7 @@ import useHasPermission from '@hooks/use-has-permission';
|
|||||||
import usePreventAutoFocus from '@hooks/use-prevent-auto-focus';
|
import usePreventAutoFocus from '@hooks/use-prevent-auto-focus';
|
||||||
import { m } from '@paraglide/messages';
|
import { m } from '@paraglide/messages';
|
||||||
import { PenIcon } from '@phosphor-icons/react';
|
import { PenIcon } from '@phosphor-icons/react';
|
||||||
|
import { Button } from '@ui/button';
|
||||||
import {
|
import {
|
||||||
Dialog,
|
Dialog,
|
||||||
DialogContent,
|
DialogContent,
|
||||||
@@ -12,62 +13,63 @@ import {
|
|||||||
DialogTrigger,
|
DialogTrigger,
|
||||||
} from '@ui/dialog';
|
} from '@ui/dialog';
|
||||||
import { Label } from '@ui/label';
|
import { Label } from '@ui/label';
|
||||||
|
import { Skeleton } from '@ui/skeleton';
|
||||||
import { Tooltip, TooltipContent, TooltipTrigger } from '@ui/tooltip';
|
import { Tooltip, TooltipContent, TooltipTrigger } from '@ui/tooltip';
|
||||||
import { useState } from 'react';
|
import { useState } from 'react';
|
||||||
|
|
||||||
type EditHouseProps = {
|
type EditHouseProps = {
|
||||||
data: HouseWithMembers;
|
data: HouseWithMembers;
|
||||||
children: React.ReactNode;
|
|
||||||
isPersonal?: boolean;
|
|
||||||
};
|
};
|
||||||
|
|
||||||
const EditHouseAction = ({
|
const EditHouseAction = ({ data }: EditHouseProps) => {
|
||||||
data,
|
|
||||||
children,
|
|
||||||
isPersonal = false,
|
|
||||||
}: EditHouseProps) => {
|
|
||||||
const [_open, _setOpen] = useState(false);
|
const [_open, _setOpen] = useState(false);
|
||||||
const prevent = usePreventAutoFocus();
|
const prevent = usePreventAutoFocus();
|
||||||
const { hasPermission, isLoading } = useHasPermission('house', 'update');
|
const { hasPermission, isLoading } = useHasPermission('house', 'update');
|
||||||
|
|
||||||
if (isLoading) return null;
|
if (isLoading) {
|
||||||
|
return <Skeleton className="h-8 w-8 rounded-full" />;
|
||||||
if (hasPermission) {
|
|
||||||
return (
|
|
||||||
<Dialog open={_open} onOpenChange={_setOpen}>
|
|
||||||
<Tooltip>
|
|
||||||
<TooltipTrigger asChild>
|
|
||||||
<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>
|
|
||||||
</TooltipContent>
|
|
||||||
</Tooltip>
|
|
||||||
<DialogContent
|
|
||||||
className="max-w-100 xl:max-w-2xl"
|
|
||||||
{...prevent}
|
|
||||||
onPointerDownOutside={(e) => e.preventDefault()}
|
|
||||||
>
|
|
||||||
<DialogHeader>
|
|
||||||
<DialogTitle className="flex items-center gap-3 text-lg font-bold text-blue-600">
|
|
||||||
<PenIcon size={16} />
|
|
||||||
{m.ui_edit_house_btn()}
|
|
||||||
</DialogTitle>
|
|
||||||
<DialogDescription className="sr-only">
|
|
||||||
{m.ui_edit_house_btn()}
|
|
||||||
</DialogDescription>
|
|
||||||
</DialogHeader>
|
|
||||||
<EditHouseForm
|
|
||||||
data={data}
|
|
||||||
onSubmit={_setOpen}
|
|
||||||
mutateKey={isPersonal ? 'currentUser' : 'list'}
|
|
||||||
/>
|
|
||||||
</DialogContent>
|
|
||||||
</Dialog>
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return null;
|
if (!hasPermission) return null;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<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>
|
||||||
|
</TooltipTrigger>
|
||||||
|
<TooltipContent className="bg-blue-500 [&_svg]:bg-blue-500 [&_svg]:fill-blue-500 text-white">
|
||||||
|
<Label>{m.ui_edit_house_btn()}</Label>
|
||||||
|
</TooltipContent>
|
||||||
|
</Tooltip>
|
||||||
|
<DialogContent
|
||||||
|
className="max-w-100 xl:max-w-2xl"
|
||||||
|
{...prevent}
|
||||||
|
onPointerDownOutside={(e) => e.preventDefault()}
|
||||||
|
>
|
||||||
|
<DialogHeader>
|
||||||
|
<DialogTitle className="flex items-center gap-3 text-lg font-bold text-blue-600">
|
||||||
|
<PenIcon size={16} />
|
||||||
|
{m.ui_edit_house_btn()}
|
||||||
|
</DialogTitle>
|
||||||
|
<DialogDescription className="sr-only">
|
||||||
|
{m.ui_edit_house_btn()}
|
||||||
|
</DialogDescription>
|
||||||
|
</DialogHeader>
|
||||||
|
<EditHouseForm data={data} onSubmit={_setOpen} mutateKey="list" />
|
||||||
|
</DialogContent>
|
||||||
|
</Dialog>
|
||||||
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
export default EditHouseAction;
|
export default EditHouseAction;
|
||||||
|
|||||||
83
src/components/house/edit-user-house-dialog.tsx
Normal file
83
src/components/house/edit-user-house-dialog.tsx
Normal file
@@ -0,0 +1,83 @@
|
|||||||
|
import EditHouseForm from '@form/house/admin-edit-house-form';
|
||||||
|
import useHasPermission from '@hooks/use-has-permission';
|
||||||
|
import usePreventAutoFocus from '@hooks/use-prevent-auto-focus';
|
||||||
|
import { authClient } from '@lib/auth-client';
|
||||||
|
import { m } from '@paraglide/messages';
|
||||||
|
import { PenIcon } from '@phosphor-icons/react';
|
||||||
|
import { Button } from '@ui/button';
|
||||||
|
import {
|
||||||
|
Dialog,
|
||||||
|
DialogContent,
|
||||||
|
DialogDescription,
|
||||||
|
DialogHeader,
|
||||||
|
DialogTitle,
|
||||||
|
DialogTrigger,
|
||||||
|
} from '@ui/dialog';
|
||||||
|
import { Label } from '@ui/label';
|
||||||
|
import { Skeleton } from '@ui/skeleton';
|
||||||
|
import { Tooltip, TooltipContent, TooltipTrigger } from '@ui/tooltip';
|
||||||
|
import { useState } from 'react';
|
||||||
|
|
||||||
|
type EditUserHouseProps = {
|
||||||
|
data: ReturnType<typeof authClient.useActiveOrganization>['data'];
|
||||||
|
};
|
||||||
|
|
||||||
|
const EditUserHouseAction = ({ data }: EditUserHouseProps) => {
|
||||||
|
const [_open, _setOpen] = useState(false);
|
||||||
|
const prevent = usePreventAutoFocus();
|
||||||
|
const { hasPermission, isLoading } = useHasPermission(
|
||||||
|
'house',
|
||||||
|
'update',
|
||||||
|
true,
|
||||||
|
);
|
||||||
|
|
||||||
|
if (isLoading) {
|
||||||
|
return <Skeleton className="h-8 w-8 rounded-full" />;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!hasPermission) return null;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Dialog open={_open} onOpenChange={_setOpen}>
|
||||||
|
<Tooltip>
|
||||||
|
<TooltipTrigger asChild>
|
||||||
|
<DialogTrigger asChild>
|
||||||
|
<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>
|
||||||
|
</DialogTrigger>
|
||||||
|
</TooltipTrigger>
|
||||||
|
<TooltipContent className="bg-blue-500 [&_svg]:bg-blue-500 [&_svg]:fill-blue-500 text-white">
|
||||||
|
<Label>{m.ui_edit_house_btn()}</Label>
|
||||||
|
</TooltipContent>
|
||||||
|
</Tooltip>
|
||||||
|
<DialogContent
|
||||||
|
className="max-w-100 xl:max-w-2xl"
|
||||||
|
{...prevent}
|
||||||
|
onPointerDownOutside={(e) => e.preventDefault()}
|
||||||
|
>
|
||||||
|
<DialogHeader>
|
||||||
|
<DialogTitle className="flex items-center gap-3 text-lg font-bold text-blue-600">
|
||||||
|
<PenIcon size={16} />
|
||||||
|
{m.ui_edit_house_btn()}
|
||||||
|
</DialogTitle>
|
||||||
|
<DialogDescription className="sr-only">
|
||||||
|
{m.ui_edit_house_btn()}
|
||||||
|
</DialogDescription>
|
||||||
|
</DialogHeader>
|
||||||
|
<EditHouseForm
|
||||||
|
data={data as HouseWithMembers}
|
||||||
|
onSubmit={_setOpen}
|
||||||
|
mutateKey="currentUser"
|
||||||
|
/>
|
||||||
|
</DialogContent>
|
||||||
|
</Dialog>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default EditUserHouseAction;
|
||||||
@@ -1,7 +1,5 @@
|
|||||||
import { m } from '@paraglide/messages';
|
import { m } from '@paraglide/messages';
|
||||||
import { PenIcon } from '@phosphor-icons/react';
|
|
||||||
import { ColumnDef } from '@tanstack/react-table';
|
import { ColumnDef } from '@tanstack/react-table';
|
||||||
import { Button } from '@ui/button';
|
|
||||||
import { formatters } from '@utils/formatters';
|
import { formatters } from '@utils/formatters';
|
||||||
import DeleteHouseAction from './delete-house-dialog';
|
import DeleteHouseAction from './delete-house-dialog';
|
||||||
import EditHouseAction from './edit-house-dialog';
|
import EditHouseAction from './edit-house-dialog';
|
||||||
@@ -44,17 +42,7 @@ export const houseColumns: ColumnDef<HouseWithMembers>[] = [
|
|||||||
return (
|
return (
|
||||||
<div className="flex justify-end gap-2">
|
<div className="flex justify-end gap-2">
|
||||||
<ViewDetailHouse data={row.original} />
|
<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} />
|
<DeleteHouseAction data={row.original} />
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -13,6 +13,7 @@ import {
|
|||||||
DialogTrigger,
|
DialogTrigger,
|
||||||
} from '@ui/dialog';
|
} from '@ui/dialog';
|
||||||
import { useState } from 'react';
|
import { useState } from 'react';
|
||||||
|
import { Skeleton } from '../ui/skeleton';
|
||||||
|
|
||||||
type ActionProps = {};
|
type ActionProps = {};
|
||||||
|
|
||||||
@@ -25,41 +26,43 @@ const InviteUserAction = ({}: ActionProps) => {
|
|||||||
const [_open, _setOpen] = useState(false);
|
const [_open, _setOpen] = useState(false);
|
||||||
const prevent = usePreventAutoFocus();
|
const prevent = usePreventAutoFocus();
|
||||||
|
|
||||||
if (isLoading) return null;
|
if (isLoading) {
|
||||||
|
return <Skeleton className="h-7 w-29.6 rounded-full" />;
|
||||||
if (hasPermission) {
|
|
||||||
return (
|
|
||||||
<Dialog open={_open} onOpenChange={_setOpen}>
|
|
||||||
<DialogTrigger asChild>
|
|
||||||
<Button
|
|
||||||
type="button"
|
|
||||||
variant="ghost"
|
|
||||||
size="sm"
|
|
||||||
className="rounded-full cursor-pointer bg-white text-black hover:bg-green-500 hover:text-white"
|
|
||||||
>
|
|
||||||
<PaperPlaneTiltIcon weight="fill" />
|
|
||||||
{m.houses_user_page_action_invite_user()}
|
|
||||||
</Button>
|
|
||||||
</DialogTrigger>
|
|
||||||
<DialogContent
|
|
||||||
className="max-w-80 xl:max-w-lg"
|
|
||||||
{...prevent}
|
|
||||||
onPointerDownOutside={(e) => e.preventDefault()}
|
|
||||||
>
|
|
||||||
<DialogHeader>
|
|
||||||
<DialogTitle className="flex items-center gap-3 text-lg font-bold text-green-600">
|
|
||||||
<PaperPlaneTiltIcon size={16} weight="fill" />
|
|
||||||
{m.houses_user_page_action_invite_user()}
|
|
||||||
</DialogTitle>
|
|
||||||
<DialogDescription className="sr-only">
|
|
||||||
{m.houses_user_page_action_invite_user()}
|
|
||||||
</DialogDescription>
|
|
||||||
</DialogHeader>
|
|
||||||
<UserInviteMemberForm onSubmit={_setOpen} />
|
|
||||||
</DialogContent>
|
|
||||||
</Dialog>
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (!hasPermission) return null;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Dialog open={_open} onOpenChange={_setOpen}>
|
||||||
|
<DialogTrigger asChild>
|
||||||
|
<Button
|
||||||
|
type="button"
|
||||||
|
variant="ghost"
|
||||||
|
size="sm"
|
||||||
|
className="rounded-full cursor-pointer bg-white text-black hover:bg-green-500 hover:text-white"
|
||||||
|
>
|
||||||
|
<PaperPlaneTiltIcon weight="fill" />
|
||||||
|
{m.houses_user_page_action_invite_user()}
|
||||||
|
</Button>
|
||||||
|
</DialogTrigger>
|
||||||
|
<DialogContent
|
||||||
|
className="max-w-80 xl:max-w-lg"
|
||||||
|
{...prevent}
|
||||||
|
onPointerDownOutside={(e) => e.preventDefault()}
|
||||||
|
>
|
||||||
|
<DialogHeader>
|
||||||
|
<DialogTitle className="flex items-center gap-3 text-lg font-bold text-green-600">
|
||||||
|
<PaperPlaneTiltIcon size={16} weight="fill" />
|
||||||
|
{m.houses_user_page_action_invite_user()}
|
||||||
|
</DialogTitle>
|
||||||
|
<DialogDescription className="sr-only">
|
||||||
|
{m.houses_user_page_action_invite_user()}
|
||||||
|
</DialogDescription>
|
||||||
|
</DialogHeader>
|
||||||
|
<UserInviteMemberForm onSubmit={_setOpen} />
|
||||||
|
</DialogContent>
|
||||||
|
</Dialog>
|
||||||
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
export default InviteUserAction;
|
export default InviteUserAction;
|
||||||
|
|||||||
120
src/components/house/leave-house-dialog.tsx
Normal file
120
src/components/house/leave-house-dialog.tsx
Normal file
@@ -0,0 +1,120 @@
|
|||||||
|
import useHasPermission from '@/hooks/use-has-permission';
|
||||||
|
import usePreventAutoFocus from '@/hooks/use-prevent-auto-focus';
|
||||||
|
import { m } from '@/paraglide/messages';
|
||||||
|
import { leaveHouse } from '@/service/house.api';
|
||||||
|
import { FootprintsIcon } from '@phosphor-icons/react';
|
||||||
|
import { useMutation } 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 { Skeleton } from '@ui/skeleton';
|
||||||
|
import { Spinner } from '@ui/spinner';
|
||||||
|
import { Tooltip, TooltipContent, TooltipTrigger } from '@ui/tooltip';
|
||||||
|
import { useState } from 'react';
|
||||||
|
import { toast } from 'sonner';
|
||||||
|
|
||||||
|
type LeaveHouseProps = {
|
||||||
|
activeHouseId: string;
|
||||||
|
};
|
||||||
|
|
||||||
|
const LeaveHouseAction = ({ activeHouseId }: LeaveHouseProps) => {
|
||||||
|
const [_open, _setOpen] = useState(false);
|
||||||
|
const prevent = usePreventAutoFocus();
|
||||||
|
const { hasPermission, isLoading } = useHasPermission('house', 'leave', true);
|
||||||
|
|
||||||
|
const { mutate: leaveHouseMutation, isPending } = useMutation({
|
||||||
|
mutationFn: leaveHouse,
|
||||||
|
onSuccess: () => {
|
||||||
|
toast.success(m.houses_page_message_leave_house_success(), {
|
||||||
|
richColors: true,
|
||||||
|
});
|
||||||
|
},
|
||||||
|
onError: (error: ReturnError) => {
|
||||||
|
const code = error.code as Parameters<
|
||||||
|
typeof m.backend_message
|
||||||
|
>[0]['code'];
|
||||||
|
toast.error(m.backend_message({ code }), {
|
||||||
|
richColors: true,
|
||||||
|
});
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
if (isLoading || !activeHouseId) {
|
||||||
|
return <Skeleton className="h-8 w-8 rounded-full" />;
|
||||||
|
}
|
||||||
|
|
||||||
|
const onConfirm = async () => {
|
||||||
|
leaveHouseMutation({ data: { id: activeHouseId } });
|
||||||
|
};
|
||||||
|
|
||||||
|
if (!hasPermission) return null;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Dialog open={_open} onOpenChange={_setOpen}>
|
||||||
|
<Tooltip>
|
||||||
|
<TooltipTrigger asChild>
|
||||||
|
<DialogTrigger asChild>
|
||||||
|
<Button
|
||||||
|
type="button"
|
||||||
|
variant="ghost"
|
||||||
|
size="icon-lg"
|
||||||
|
className="rounded-full cursor-pointer bg-red-500 text-white hover:bg-red-100 hover:text-red-600"
|
||||||
|
>
|
||||||
|
<FootprintsIcon size={16} />
|
||||||
|
<span className="sr-only">{m.ui_leave_btn()}</span>
|
||||||
|
</Button>
|
||||||
|
</DialogTrigger>
|
||||||
|
</TooltipTrigger>
|
||||||
|
<TooltipContent className="bg-red-500 [&_svg]:bg-red-500 [&_svg]:fill-red-500 text-white">
|
||||||
|
<Label>{m.ui_leave_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">
|
||||||
|
<FootprintsIcon size={30} />
|
||||||
|
</div>
|
||||||
|
{m.houses_page_ui_dialog_alert_leave_title()}
|
||||||
|
</DialogTitle>
|
||||||
|
<DialogDescription className="text-red-500 text-sm">
|
||||||
|
{m.houses_page_ui_dialog_alert_leave_description()}
|
||||||
|
</DialogDescription>
|
||||||
|
</DialogHeader>
|
||||||
|
<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>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default LeaveHouseAction;
|
||||||
136
src/components/house/remove-user-form-house.tsx
Normal file
136
src/components/house/remove-user-form-house.tsx
Normal file
@@ -0,0 +1,136 @@
|
|||||||
|
import usePreventAutoFocus from '@/hooks/use-prevent-auto-focus';
|
||||||
|
import { authClient } from '@/lib/auth-client';
|
||||||
|
import { m } from '@/paraglide/messages';
|
||||||
|
import { removeMember } from '@/service/house.api';
|
||||||
|
import { housesQueries } from '@/service/queries';
|
||||||
|
import { UserCircleMinusIcon } 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 { Spinner } from '@ui/spinner';
|
||||||
|
import { Tooltip, TooltipContent, TooltipTrigger } from '@ui/tooltip';
|
||||||
|
import { useState } from 'react';
|
||||||
|
import { toast } from 'sonner';
|
||||||
|
|
||||||
|
type RemoveUserFormProps = {
|
||||||
|
member: {
|
||||||
|
id: string;
|
||||||
|
organizationId: string;
|
||||||
|
userId: string;
|
||||||
|
user: {
|
||||||
|
email: string;
|
||||||
|
name: string;
|
||||||
|
};
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
const RemoveUserFormHouse = ({ member }: RemoveUserFormProps) => {
|
||||||
|
const [_open, _setOpen] = useState(false);
|
||||||
|
const prevent = usePreventAutoFocus();
|
||||||
|
const { refetch } = authClient.useActiveOrganization();
|
||||||
|
|
||||||
|
const queryClient = useQueryClient();
|
||||||
|
|
||||||
|
const { mutate: removeMemberMutation, isPending } = useMutation({
|
||||||
|
mutationFn: removeMember,
|
||||||
|
onSuccess: () => {
|
||||||
|
refetch();
|
||||||
|
queryClient.invalidateQueries({
|
||||||
|
queryKey: [...housesQueries.all, 'currentUser'],
|
||||||
|
});
|
||||||
|
_setOpen(false);
|
||||||
|
toast.success(m.houses_page_message_remove_member_success(), {
|
||||||
|
richColors: true,
|
||||||
|
});
|
||||||
|
},
|
||||||
|
onError: (error: ReturnError) => {
|
||||||
|
const code = error.code as Parameters<
|
||||||
|
typeof m.backend_message
|
||||||
|
>[0]['code'];
|
||||||
|
toast.error(m.backend_message({ code }), {
|
||||||
|
richColors: true,
|
||||||
|
});
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
const onConfirm = () => {
|
||||||
|
removeMemberMutation({
|
||||||
|
data: {
|
||||||
|
houseId: member.organizationId,
|
||||||
|
memberId: member.id,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Dialog open={_open} onOpenChange={_setOpen}>
|
||||||
|
<Tooltip>
|
||||||
|
<TooltipTrigger asChild>
|
||||||
|
<DialogTrigger asChild>
|
||||||
|
<Button
|
||||||
|
type="button"
|
||||||
|
variant="ghost"
|
||||||
|
size="icon-lg"
|
||||||
|
className="rounded-full cursor-pointer bg-red-500 text-white hover:bg-red-100 hover:text-red-600"
|
||||||
|
>
|
||||||
|
<UserCircleMinusIcon size={16} />
|
||||||
|
<span className="sr-only">{m.ui_remove_btn()}</span>
|
||||||
|
</Button>
|
||||||
|
</DialogTrigger>
|
||||||
|
</TooltipTrigger>
|
||||||
|
<TooltipContent className="bg-red-500 [&_svg]:bg-red-500 [&_svg]:fill-red-500 text-white">
|
||||||
|
<Label>{m.ui_remove_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">
|
||||||
|
<UserCircleMinusIcon size={30} />
|
||||||
|
</div>
|
||||||
|
{m.houses_page_ui_dialog_alert_remove_title({
|
||||||
|
name: member.user.name,
|
||||||
|
})}
|
||||||
|
</DialogTitle>
|
||||||
|
<DialogDescription className="text-red-500 text-sm">
|
||||||
|
{m.houses_page_ui_dialog_alert_remove_description()}
|
||||||
|
</DialogDescription>
|
||||||
|
</DialogHeader>
|
||||||
|
<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>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default RemoveUserFormHouse;
|
||||||
@@ -1,15 +1,11 @@
|
|||||||
import { Button } from '@/components/ui/button';
|
import { Button } from '@/components/ui/button';
|
||||||
import {
|
|
||||||
ItemActions,
|
|
||||||
ItemContent,
|
|
||||||
ItemDescription,
|
|
||||||
ItemTitle,
|
|
||||||
} from '@/components/ui/item';
|
|
||||||
import { m } from '@/paraglide/messages';
|
|
||||||
import { acceptInvitation, rejectInvitation } from '@/service/house.api';
|
|
||||||
import { notificationQueries } from '@/service/queries';
|
|
||||||
import { formatTimeAgo } from '@/utils/helper';
|
import { formatTimeAgo } from '@/utils/helper';
|
||||||
|
import { m } from '@paraglide/messages';
|
||||||
|
import { acceptInvitation, rejectInvitation } from '@service/house.api';
|
||||||
|
import { notificationQueries } from '@service/queries';
|
||||||
import { useMutation, useQueryClient } from '@tanstack/react-query';
|
import { useMutation, useQueryClient } from '@tanstack/react-query';
|
||||||
|
import { ItemActions, ItemContent, ItemDescription, ItemTitle } from '@ui/item';
|
||||||
|
import { Spinner } from '@ui/spinner';
|
||||||
import { toast } from 'sonner';
|
import { toast } from 'sonner';
|
||||||
|
|
||||||
type NotifyProps = {
|
type NotifyProps = {
|
||||||
@@ -21,47 +17,49 @@ const NotificationInvitation = ({ notify }: NotifyProps) => {
|
|||||||
|
|
||||||
const queryClient = useQueryClient();
|
const queryClient = useQueryClient();
|
||||||
|
|
||||||
const { mutate: acceptInvitationMutation } = useMutation({
|
const { mutate: acceptInvitationMutation, isPending: isAcceptPending } =
|
||||||
mutationFn: acceptInvitation,
|
useMutation({
|
||||||
onSuccess: () => {
|
mutationFn: acceptInvitation,
|
||||||
queryClient.invalidateQueries({
|
onSuccess: () => {
|
||||||
queryKey: [...notificationQueries.all, 'list'],
|
queryClient.invalidateQueries({
|
||||||
});
|
queryKey: [...notificationQueries.all, 'list'],
|
||||||
toast.success(m.notification_page_message_invitation_success(), {
|
});
|
||||||
richColors: true,
|
toast.success(m.notification_page_message_invitation_success(), {
|
||||||
});
|
richColors: true,
|
||||||
},
|
});
|
||||||
onError: (error: ReturnError) => {
|
},
|
||||||
console.error(error);
|
onError: (error: ReturnError) => {
|
||||||
const code = error.code as Parameters<
|
console.error(error);
|
||||||
typeof m.backend_message
|
const code = error.code as Parameters<
|
||||||
>[0]['code'];
|
typeof m.backend_message
|
||||||
toast.error(m.backend_message({ code }), {
|
>[0]['code'];
|
||||||
richColors: true,
|
toast.error(m.backend_message({ code }), {
|
||||||
});
|
richColors: true,
|
||||||
},
|
});
|
||||||
});
|
},
|
||||||
|
});
|
||||||
|
|
||||||
const { mutate: rejectInvitationMutation } = useMutation({
|
const { mutate: rejectInvitationMutation, isPending: isRejectPending } =
|
||||||
mutationFn: rejectInvitation,
|
useMutation({
|
||||||
onSuccess: () => {
|
mutationFn: rejectInvitation,
|
||||||
queryClient.invalidateQueries({
|
onSuccess: () => {
|
||||||
queryKey: [...notificationQueries.all, 'list'],
|
queryClient.invalidateQueries({
|
||||||
});
|
queryKey: [...notificationQueries.all, 'list'],
|
||||||
toast.success(m.notification_page_message_invitation_rejected(), {
|
});
|
||||||
richColors: true,
|
toast.success(m.notification_page_message_invitation_rejected(), {
|
||||||
});
|
richColors: true,
|
||||||
},
|
});
|
||||||
onError: (error: ReturnError) => {
|
},
|
||||||
console.error(error);
|
onError: (error: ReturnError) => {
|
||||||
const code = error.code as Parameters<
|
console.error(error);
|
||||||
typeof m.backend_message
|
const code = error.code as Parameters<
|
||||||
>[0]['code'];
|
typeof m.backend_message
|
||||||
toast.error(m.backend_message({ code }), {
|
>[0]['code'];
|
||||||
richColors: true,
|
toast.error(m.backend_message({ code }), {
|
||||||
});
|
richColors: true,
|
||||||
},
|
});
|
||||||
});
|
},
|
||||||
|
});
|
||||||
|
|
||||||
const handleAgreeAction = async () => {
|
const handleAgreeAction = async () => {
|
||||||
if (notify.link) {
|
if (notify.link) {
|
||||||
@@ -95,10 +93,19 @@ const NotificationInvitation = ({ notify }: NotifyProps) => {
|
|||||||
</ItemContent>
|
</ItemContent>
|
||||||
{notify.link && (
|
{notify.link && (
|
||||||
<ItemActions>
|
<ItemActions>
|
||||||
<Button onClick={() => handleAgreeAction()}>
|
<Button
|
||||||
|
onClick={() => handleAgreeAction()}
|
||||||
|
disabled={isAcceptPending}
|
||||||
|
>
|
||||||
|
{isAcceptPending && <Spinner data-icon="inline-start" />}
|
||||||
{m.ui_agree_btn()}
|
{m.ui_agree_btn()}
|
||||||
</Button>
|
</Button>
|
||||||
<Button variant="outline" onClick={() => handleRejectAction()}>
|
<Button
|
||||||
|
variant="outline"
|
||||||
|
onClick={() => handleRejectAction()}
|
||||||
|
disabled={isRejectPending}
|
||||||
|
>
|
||||||
|
{isRejectPending && <Spinner data-icon="inline-start" />}
|
||||||
{m.ui_reject_btn()}
|
{m.ui_reject_btn()}
|
||||||
</Button>
|
</Button>
|
||||||
</ItemActions>
|
</ItemActions>
|
||||||
|
|||||||
@@ -114,6 +114,9 @@ const NavMain = () => {
|
|||||||
to={item.path}
|
to={item.path}
|
||||||
className="cursor-pointer"
|
className="cursor-pointer"
|
||||||
tooltip={`${nav.title} - ${item.title}`}
|
tooltip={`${nav.title} - ${item.title}`}
|
||||||
|
activeProps={{
|
||||||
|
className: 'bg-primary text-white',
|
||||||
|
}}
|
||||||
>
|
>
|
||||||
<Icon size={24} />
|
<Icon size={24} />
|
||||||
{item.title}
|
{item.title}
|
||||||
|
|||||||
@@ -35,7 +35,7 @@ const NavUser = () => {
|
|||||||
const navigate = useNavigate();
|
const navigate = useNavigate();
|
||||||
const { isMobile } = useSidebar();
|
const { isMobile } = useSidebar();
|
||||||
const queryClient = useQueryClient();
|
const queryClient = useQueryClient();
|
||||||
const { data: session } = useAuth();
|
const { session } = useAuth();
|
||||||
|
|
||||||
const signout = async () => {
|
const signout = async () => {
|
||||||
await authClient.signOut({
|
await authClient.signOut({
|
||||||
|
|||||||
@@ -5,7 +5,7 @@ import * as React from 'react';
|
|||||||
import { cn } from '@lib/utils';
|
import { cn } from '@lib/utils';
|
||||||
|
|
||||||
const buttonVariants = cva(
|
const buttonVariants = cva(
|
||||||
"focus-visible:border-ring focus-visible:ring-ring/30 aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 aria-invalid:border-destructive dark:aria-invalid:border-destructive/50 rounded-md border border-transparent bg-clip-padding text-xs/relaxed font-medium focus-visible:ring-[2px] aria-invalid:ring-[2px] [&_svg:not([class*='size-'])]:size-4 inline-flex items-center justify-center whitespace-nowrap transition-all disabled:pointer-events-none disabled:opacity-50 disabled:data-[active=true]:opacity-100 disabled:data-[active=true]:bg-primary disabled:data-[active=true]:border-primary disabled:data-[active=true]:text-white disabled:data-[dot=true]:opacity-100 [&_svg]:pointer-events-none shrink-0 [&_svg]:shrink-0 outline-none group/button select-none",
|
"focus-visible:border-ring focus-visible:ring-ring/30 aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 aria-invalid:border-destructive dark:aria-invalid:border-destructive/50 rounded-md border border-transparent bg-clip-padding text-xs/relaxed font-medium focus-visible:ring-[2px] aria-invalid:ring-[2px] [&_svg:not([class*='size-'])]:size-4 inline-flex items-center justify-center whitespace-nowrap transition-all disabled:pointer-events-none disabled:opacity-50 disabled:data-[active=true]:opacity-100 disabled:data-[active=true]:bg-primary disabled:data-[active=true]:border-primary disabled:data-[active=true]:text-white disabled:data-[dot=true]:opacity-100 [&_svg]:pointer-events-none shrink-0 [&_svg]:shrink-0 outline-none group/button select-none cursor-pointer",
|
||||||
{
|
{
|
||||||
variants: {
|
variants: {
|
||||||
variant: {
|
variant: {
|
||||||
|
|||||||
@@ -6,11 +6,19 @@ function useHasPermission(
|
|||||||
action: string,
|
action: string,
|
||||||
houseCheck: boolean = false,
|
houseCheck: boolean = false,
|
||||||
) {
|
) {
|
||||||
|
const { data: activeOrganization } = authClient.useActiveOrganization();
|
||||||
const [hasPermission, setHasPermission] = useState(false);
|
const [hasPermission, setHasPermission] = useState(false);
|
||||||
const [isLoading, setIsLoading] = useState(true);
|
const [isLoading, setIsLoading] = useState(true);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
|
if (houseCheck && !activeOrganization?.id) {
|
||||||
|
setIsLoading(false);
|
||||||
|
setHasPermission(false);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
const checkPermission = async () => {
|
const checkPermission = async () => {
|
||||||
|
setIsLoading(true);
|
||||||
try {
|
try {
|
||||||
const option = {
|
const option = {
|
||||||
permissions: {
|
permissions: {
|
||||||
@@ -29,7 +37,7 @@ function useHasPermission(
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
checkPermission();
|
checkPermission();
|
||||||
}, [resource, action]);
|
}, [resource, action, houseCheck, activeOrganization?.id]);
|
||||||
|
|
||||||
return { hasPermission, isLoading };
|
return { hasPermission, isLoading };
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,37 +1,37 @@
|
|||||||
import { createAccessControl } from 'better-auth/plugins/access'
|
import { createAccessControl } from 'better-auth/plugins/access';
|
||||||
import {
|
import {
|
||||||
defaultStatements,
|
|
||||||
adminAc,
|
adminAc,
|
||||||
|
defaultStatements,
|
||||||
ownerAc,
|
ownerAc,
|
||||||
} from 'better-auth/plugins/organization/access'
|
} from 'better-auth/plugins/organization/access';
|
||||||
|
|
||||||
const statement = {
|
const statement = {
|
||||||
...defaultStatements,
|
...defaultStatements,
|
||||||
house: ['list', 'create', 'update', 'delete'],
|
house: ['list', 'create', 'update', 'delete', 'leave'],
|
||||||
box: ['list', 'create', 'update', 'delete'],
|
box: ['list', 'create', 'update', 'delete'],
|
||||||
item: ['list', 'create', 'update', 'delete'],
|
item: ['list', 'create', 'update', 'delete'],
|
||||||
} as const
|
} as const;
|
||||||
|
|
||||||
const acOrg = createAccessControl(statement)
|
const acOrg = createAccessControl(statement);
|
||||||
|
|
||||||
const owner = acOrg.newRole({
|
const owner = acOrg.newRole({
|
||||||
...ownerAc.statements,
|
...ownerAc.statements,
|
||||||
house: ['list', 'create', 'update', 'delete'],
|
house: ['list', 'create', 'update', 'delete'],
|
||||||
box: ['list', 'create', 'update', 'delete'],
|
box: ['list', 'create', 'update', 'delete'],
|
||||||
item: ['list', 'create', 'update', 'delete'],
|
item: ['list', 'create', 'update', 'delete'],
|
||||||
})
|
});
|
||||||
|
|
||||||
const adminOrg = acOrg.newRole({
|
const adminOrg = acOrg.newRole({
|
||||||
...adminAc.statements,
|
...adminAc.statements,
|
||||||
house: ['list', 'create', 'update', 'delete'],
|
house: ['list', 'create', 'update', 'leave'],
|
||||||
box: ['list', 'create', 'update', 'delete'],
|
box: ['list', 'create', 'update', 'delete'],
|
||||||
item: ['list', 'create', 'update', 'delete'],
|
item: ['list', 'create', 'update', 'delete'],
|
||||||
})
|
});
|
||||||
|
|
||||||
const member = acOrg.newRole({
|
const member = acOrg.newRole({
|
||||||
house: ['list', 'create', 'update', 'delete'],
|
house: ['list', 'leave'],
|
||||||
box: ['list', 'create', 'update', 'delete'],
|
box: ['list'],
|
||||||
item: ['list', 'create', 'update', 'delete'],
|
item: ['list', 'create', 'update', 'delete'],
|
||||||
})
|
});
|
||||||
|
|
||||||
export { acOrg, owner, adminOrg, member }
|
export { acOrg, adminOrg, member, owner };
|
||||||
|
|||||||
@@ -13,6 +13,7 @@ import {
|
|||||||
houseEditBESchema,
|
houseEditBESchema,
|
||||||
houseListSchema,
|
houseListSchema,
|
||||||
invitationCreateBESchema,
|
invitationCreateBESchema,
|
||||||
|
removeMemberSchema,
|
||||||
} from './house.schema';
|
} from './house.schema';
|
||||||
import { createAuditLog, createNotification } from './repository';
|
import { createAuditLog, createNotification } from './repository';
|
||||||
|
|
||||||
@@ -347,7 +348,7 @@ export const cancelInvitation = createServerFn({ method: 'POST' })
|
|||||||
export const acceptInvitation = createServerFn({ method: 'POST' })
|
export const acceptInvitation = createServerFn({ method: 'POST' })
|
||||||
.middleware([authMiddleware])
|
.middleware([authMiddleware])
|
||||||
.inputValidator(actionInvitationSchema)
|
.inputValidator(actionInvitationSchema)
|
||||||
.handler(async ({ data }) => {
|
.handler(async ({ data, context: { user } }) => {
|
||||||
try {
|
try {
|
||||||
const result = await prisma.invitation.update({
|
const result = await prisma.invitation.update({
|
||||||
where: { id: data.id },
|
where: { id: data.id },
|
||||||
@@ -360,13 +361,33 @@ export const acceptInvitation = createServerFn({ method: 'POST' })
|
|||||||
data: { link: null },
|
data: { link: null },
|
||||||
});
|
});
|
||||||
|
|
||||||
await auth.api.addMember({
|
const member = await auth.api.addMember({
|
||||||
body: {
|
body: {
|
||||||
userId: notify.userId,
|
userId: notify.userId,
|
||||||
organizationId: result.organizationId,
|
organizationId: result.organizationId,
|
||||||
role: (result.role as 'admin' | 'owner' | 'member') || 'member',
|
role: (result.role as 'admin' | 'owner' | 'member') || 'member',
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
|
await createAuditLog({
|
||||||
|
action: LOG_ACTION.UPDATE,
|
||||||
|
tableName: DB_TABLE.INVITATION,
|
||||||
|
recordId: result.id,
|
||||||
|
oldValue: JSON.stringify(result),
|
||||||
|
newValue: 'Accept Invitation / Đồng ý mời',
|
||||||
|
userId: user.id,
|
||||||
|
});
|
||||||
|
|
||||||
|
if (member) {
|
||||||
|
await createAuditLog({
|
||||||
|
action: LOG_ACTION.CREATE,
|
||||||
|
tableName: DB_TABLE.MEMBER,
|
||||||
|
recordId: member.id,
|
||||||
|
oldValue: '',
|
||||||
|
newValue: JSON.stringify(member),
|
||||||
|
userId: user.id,
|
||||||
|
});
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return result;
|
return result;
|
||||||
@@ -404,3 +425,72 @@ export const rejectInvitation = createServerFn({ method: 'POST' })
|
|||||||
throw { message, code };
|
throw { message, code };
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
export const leaveHouse = createServerFn({ method: 'POST' })
|
||||||
|
.middleware([authMiddleware])
|
||||||
|
.inputValidator(baseHouse)
|
||||||
|
.handler(async ({ data }) => {
|
||||||
|
try {
|
||||||
|
const headers = getRequestHeaders();
|
||||||
|
const result = await auth.api.leaveOrganization({
|
||||||
|
body: {
|
||||||
|
organizationId: data.id,
|
||||||
|
},
|
||||||
|
headers,
|
||||||
|
});
|
||||||
|
|
||||||
|
if (result) {
|
||||||
|
await createAuditLog({
|
||||||
|
action: LOG_ACTION.UPDATE,
|
||||||
|
tableName: DB_TABLE.ORGANIZATION,
|
||||||
|
recordId: result.id,
|
||||||
|
oldValue: JSON.stringify(result),
|
||||||
|
newValue: `${result.user.name} đã rời khỏi nhà (left the house)`,
|
||||||
|
userId: result.user.id,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
return result;
|
||||||
|
} catch (error) {
|
||||||
|
console.error(error);
|
||||||
|
const { message, code } = parseError(error);
|
||||||
|
throw { message, code };
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
export const removeMember = createServerFn({ method: 'POST' })
|
||||||
|
.middleware([authMiddleware])
|
||||||
|
.inputValidator(removeMemberSchema)
|
||||||
|
.handler(async ({ data, context: { user } }) => {
|
||||||
|
try {
|
||||||
|
const headers = getRequestHeaders();
|
||||||
|
const result = await auth.api.removeMember({
|
||||||
|
body: {
|
||||||
|
memberIdOrEmail: data.memberId,
|
||||||
|
organizationId: data.houseId,
|
||||||
|
},
|
||||||
|
headers,
|
||||||
|
});
|
||||||
|
|
||||||
|
if (result) {
|
||||||
|
const oldMember = await prisma.user.findUnique({
|
||||||
|
where: { id: result.member.userId },
|
||||||
|
});
|
||||||
|
|
||||||
|
await createAuditLog({
|
||||||
|
action: LOG_ACTION.UPDATE,
|
||||||
|
tableName: DB_TABLE.MEMBER,
|
||||||
|
recordId: result.member.id,
|
||||||
|
oldValue: JSON.stringify(result),
|
||||||
|
newValue: `${oldMember?.name} đã bị "mời" rời khỏi nhà (was "asked" to leave the house.)`,
|
||||||
|
userId: user.id,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
return result;
|
||||||
|
} catch (error) {
|
||||||
|
console.error(error);
|
||||||
|
const { message, code } = parseError(error);
|
||||||
|
throw { message, code };
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|||||||
@@ -71,3 +71,8 @@ export const invitationCreateBESchema = invitationCreateSchema.extend({
|
|||||||
export const actionInvitationSchema = baseInvitation.extend({
|
export const actionInvitationSchema = baseInvitation.extend({
|
||||||
notificationId: z.string().nonempty(m.notification_page_notify_not_found()),
|
notificationId: z.string().nonempty(m.notification_page_notify_not_found()),
|
||||||
});
|
});
|
||||||
|
|
||||||
|
export const removeMemberSchema = z.object({
|
||||||
|
memberId: z.string().nonempty(m.users_page_message_user_not_found()),
|
||||||
|
houseId: z.string().nonempty(m.houses_page_message_house_not_found()),
|
||||||
|
});
|
||||||
|
|||||||
Reference in New Issue
Block a user