added leave house, remove member
This commit is contained in:
@@ -55,6 +55,8 @@
|
||||
"ui_save_btn": "Save changes",
|
||||
"ui_update_btn": "Update",
|
||||
"ui_delete_btn": "Delete",
|
||||
"ui_leave_btn": "Leave",
|
||||
"ui_remove_btn": "Remove",
|
||||
"ui_ban_btn": "Lock",
|
||||
"ui_unban_btn": "Unlock",
|
||||
"ui_invite_btn": "Invite",
|
||||
@@ -174,6 +176,12 @@
|
||||
"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_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_house_not_found": "House not found!",
|
||||
"houses_page_message_update_house_success": "Updated house successfully!",
|
||||
|
||||
@@ -28,7 +28,7 @@
|
||||
"role=admin": "Quản lý",
|
||||
"role=user": "Người dùng",
|
||||
"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_update_btn": "Cập nhật",
|
||||
"ui_delete_btn": "Xóa",
|
||||
"ui_leave_btn": "Rời đi",
|
||||
"ui_remove_btn": "Xóa",
|
||||
"ui_ban_btn": "Khóa",
|
||||
"ui_unban_btn": "Mở khóa",
|
||||
"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_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_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_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!",
|
||||
|
||||
@@ -22,7 +22,7 @@ export const userData = [
|
||||
email: 'raysam024@gmail.com',
|
||||
},
|
||||
{
|
||||
name: 'Raysam',
|
||||
name: 'Sam Miu',
|
||||
email: 'juines.liu@gmail.com',
|
||||
},
|
||||
];
|
||||
|
||||
@@ -5,7 +5,7 @@ import Notification from './Notification';
|
||||
import RouterBreadcrumb from './sidebar/router-breadcrumb';
|
||||
|
||||
export default function Header() {
|
||||
const { data: session } = useAuth();
|
||||
const { session } = useAuth();
|
||||
|
||||
return (
|
||||
<>
|
||||
|
||||
@@ -4,7 +4,7 @@ import { useQuery } from '@tanstack/react-query';
|
||||
import { createContext, useContext, useMemo } from 'react';
|
||||
|
||||
export type UserContext = {
|
||||
data: ClientSession;
|
||||
session: ClientSession;
|
||||
isAuth: boolean;
|
||||
isAdmin: boolean;
|
||||
isPending: boolean;
|
||||
@@ -18,7 +18,7 @@ export function AuthProvider({ children }: { children: React.ReactNode }) {
|
||||
|
||||
const contextSession: UserContext = useMemo(
|
||||
() => ({
|
||||
data: session as ClientSession,
|
||||
session: session as ClientSession,
|
||||
isPending,
|
||||
error,
|
||||
isAuth: !!session,
|
||||
|
||||
@@ -9,7 +9,7 @@ interface AvatarUserProps {
|
||||
}
|
||||
|
||||
const AvatarUser = ({ className, textSize = 'md' }: AvatarUserProps) => {
|
||||
const { data: session } = useAuth();
|
||||
const { session } = useAuth();
|
||||
const imagePath = session?.user?.image
|
||||
? new URL(`../../../data/avatar/${session?.user?.image}`, import.meta.url)
|
||||
.href
|
||||
|
||||
@@ -21,7 +21,7 @@ const defaultValues: ProfileInput = {
|
||||
|
||||
const ProfileForm = () => {
|
||||
const fileInputRef = useRef<HTMLInputElement>(null);
|
||||
const { data: session, isPending } = useAuth();
|
||||
const { session, isPending } = useAuth();
|
||||
const queryClient = useQueryClient();
|
||||
|
||||
const { mutate: updateProfileMutation, isPending: isRunning } = useMutation({
|
||||
|
||||
@@ -21,7 +21,7 @@ type FormProps = {
|
||||
};
|
||||
|
||||
const CreateNewHouseForm = ({ onSubmit, isPersonal = false }: FormProps) => {
|
||||
const { data: session } = useAuth();
|
||||
const { session } = useAuth();
|
||||
const [userKeyword, setUserKeyword] = useState('');
|
||||
const debouncedUserKeyword = useDebounced(userKeyword, 300);
|
||||
const { data: users } = useQuery(
|
||||
|
||||
@@ -14,6 +14,7 @@ import {
|
||||
DialogTrigger,
|
||||
} from '@ui/dialog';
|
||||
import { useState } from 'react';
|
||||
import { Skeleton } from '../ui/skeleton';
|
||||
|
||||
type CreateNewHouseProp = {
|
||||
isPersonal?: boolean;
|
||||
@@ -24,46 +25,42 @@ const CreateNewHouse = ({
|
||||
className,
|
||||
isPersonal = false,
|
||||
}: CreateNewHouseProp) => {
|
||||
const { hasPermission, isLoading } = useHasPermission(
|
||||
'house',
|
||||
'create',
|
||||
isPersonal,
|
||||
);
|
||||
const { hasPermission, isLoading } = useHasPermission('house', 'create');
|
||||
const [_open, _setOpen] = useState(false);
|
||||
const prevent = usePreventAutoFocus();
|
||||
|
||||
if (isLoading) return null;
|
||||
|
||||
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>
|
||||
);
|
||||
if (isLoading) {
|
||||
return <Skeleton className={cn('h-7 w-23', className)} />;
|
||||
}
|
||||
|
||||
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;
|
||||
|
||||
@@ -1,10 +1,11 @@
|
||||
import { authClient } from '@lib/auth-client';
|
||||
import { m } from '@paraglide/messages';
|
||||
import { GearIcon, PenIcon } from '@phosphor-icons/react';
|
||||
import { Button } from '@ui/button';
|
||||
import { GearIcon } from '@phosphor-icons/react';
|
||||
import { Card, CardContent, CardHeader, CardTitle } from '@ui/card';
|
||||
import { Skeleton } from '../ui/skeleton';
|
||||
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 = {
|
||||
oneHouse: boolean;
|
||||
@@ -15,6 +16,10 @@ const CurrentUserActionGroup = ({
|
||||
oneHouse,
|
||||
activeHouse,
|
||||
}: CurrentUserActionGroupProps) => {
|
||||
if (!activeHouse) {
|
||||
return <Skeleton className="col-span-1" />;
|
||||
}
|
||||
|
||||
return (
|
||||
<Card className="col-span-1">
|
||||
<CardHeader>
|
||||
@@ -24,17 +29,9 @@ const CurrentUserActionGroup = ({
|
||||
</CardTitle>
|
||||
</CardHeader>
|
||||
<CardContent className="flex flex-row gap-2">
|
||||
<EditHouseAction data={activeHouse as HouseWithMembers} 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>
|
||||
<EditUserHouseAction data={activeHouse} />
|
||||
{!oneHouse && <DeleteUserHouseAction activeHouse={activeHouse} />}
|
||||
<LeaveHouseAction activeHouseId={activeHouse.id} />
|
||||
</CardContent>
|
||||
</Card>
|
||||
);
|
||||
|
||||
@@ -53,8 +53,10 @@ const CurrentUserInvitationList = ({ activeHouse }: InvitationListProps) => {
|
||||
cancelInvitationMutation({ data: { id } });
|
||||
};
|
||||
|
||||
if (!activeHouse) {
|
||||
return <Skeleton className="col-span-2 h-80 w-full rounded-xl" />;
|
||||
if (!activeHouse || isLoading) {
|
||||
return (
|
||||
<Skeleton className="col-span-1 lg:col-span-3 h-80 w-full rounded-xl" />
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
|
||||
@@ -10,16 +10,20 @@ import {
|
||||
TableHeader,
|
||||
TableRow,
|
||||
} from '@ui/table';
|
||||
import { useAuth } from '../auth/auth-provider';
|
||||
import RoleBadge from '../avatar/role-badge';
|
||||
import InviteUserAction from './invite-user-dialog';
|
||||
import RemoveUserFormHouse from './remove-user-form-house';
|
||||
|
||||
type CurrentUserMemberListProps = {
|
||||
activeHouse: ReturnType<typeof authClient.useActiveOrganization>['data'];
|
||||
};
|
||||
|
||||
const CurrentUserMemberList = ({ activeHouse }: CurrentUserMemberListProps) => {
|
||||
const { session } = useAuth();
|
||||
|
||||
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 (
|
||||
@@ -57,7 +61,10 @@ const CurrentUserMemberList = ({ activeHouse }: CurrentUserMemberListProps) => {
|
||||
</TableCell>
|
||||
<TableCell className="px-4">
|
||||
<div className="flex justify-end gap-2">
|
||||
{activeHouse.color}
|
||||
{member.role !== 'owner' &&
|
||||
session.user.id !== member.user.id && (
|
||||
<RemoveUserFormHouse member={member} />
|
||||
)}
|
||||
</div>
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
|
||||
@@ -31,6 +31,7 @@ import parse from 'html-react-parser';
|
||||
import { useState } from 'react';
|
||||
import { toast } from 'sonner';
|
||||
import RoleBadge from '../avatar/role-badge';
|
||||
import { Skeleton } from '../ui/skeleton';
|
||||
|
||||
type DeleteHouseProps = {
|
||||
data: HouseWithMembers;
|
||||
@@ -69,93 +70,93 @@ const DeleteHouseAction = ({ data }: DeleteHouseProps) => {
|
||||
deleteHouseMutation({ data });
|
||||
};
|
||||
|
||||
if (isLoading) return null;
|
||||
|
||||
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>
|
||||
);
|
||||
if (isLoading) {
|
||||
return <Skeleton className="h-8 w-8 rounded-full" />;
|
||||
}
|
||||
|
||||
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;
|
||||
|
||||
@@ -32,6 +32,7 @@ import parse from 'html-react-parser';
|
||||
import { useState } from 'react';
|
||||
import { toast } from 'sonner';
|
||||
import RoleBadge from '../avatar/role-badge';
|
||||
import { Skeleton } from '../ui/skeleton';
|
||||
|
||||
type DeleteUserHouseProps = {
|
||||
activeHouse: ReturnType<typeof authClient.useActiveOrganization>['data'];
|
||||
@@ -40,7 +41,11 @@ type DeleteUserHouseProps = {
|
||||
const DeleteUserHouseAction = ({ activeHouse }: DeleteUserHouseProps) => {
|
||||
const [_open, _setOpen] = useState(false);
|
||||
const prevent = usePreventAutoFocus();
|
||||
const { hasPermission, isLoading } = useHasPermission('house', 'delete');
|
||||
const { hasPermission, isLoading } = useHasPermission(
|
||||
'house',
|
||||
'delete',
|
||||
true,
|
||||
);
|
||||
|
||||
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 () => {
|
||||
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>
|
||||
);
|
||||
}
|
||||
if (!hasPermission) return null;
|
||||
|
||||
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;
|
||||
|
||||
@@ -3,6 +3,7 @@ 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,
|
||||
@@ -12,62 +13,63 @@ import {
|
||||
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 EditHouseProps = {
|
||||
data: HouseWithMembers;
|
||||
children: React.ReactNode;
|
||||
isPersonal?: boolean;
|
||||
};
|
||||
|
||||
const EditHouseAction = ({
|
||||
data,
|
||||
children,
|
||||
isPersonal = false,
|
||||
}: EditHouseProps) => {
|
||||
const EditHouseAction = ({ data }: EditHouseProps) => {
|
||||
const [_open, _setOpen] = useState(false);
|
||||
const prevent = usePreventAutoFocus();
|
||||
const { hasPermission, isLoading } = useHasPermission('house', 'update');
|
||||
|
||||
if (isLoading) return null;
|
||||
|
||||
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>
|
||||
);
|
||||
if (isLoading) {
|
||||
return <Skeleton className="h-8 w-8 rounded-full" />;
|
||||
}
|
||||
|
||||
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;
|
||||
|
||||
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 { PenIcon } from '@phosphor-icons/react';
|
||||
import { ColumnDef } from '@tanstack/react-table';
|
||||
import { Button } from '@ui/button';
|
||||
import { formatters } from '@utils/formatters';
|
||||
import DeleteHouseAction from './delete-house-dialog';
|
||||
import EditHouseAction from './edit-house-dialog';
|
||||
@@ -44,17 +42,7 @@ export const houseColumns: ColumnDef<HouseWithMembers>[] = [
|
||||
return (
|
||||
<div className="flex justify-end gap-2">
|
||||
<ViewDetailHouse 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>
|
||||
<EditHouseAction data={row.original} />
|
||||
<DeleteHouseAction data={row.original} />
|
||||
</div>
|
||||
);
|
||||
|
||||
@@ -13,6 +13,7 @@ import {
|
||||
DialogTrigger,
|
||||
} from '@ui/dialog';
|
||||
import { useState } from 'react';
|
||||
import { Skeleton } from '../ui/skeleton';
|
||||
|
||||
type ActionProps = {};
|
||||
|
||||
@@ -25,41 +26,43 @@ const InviteUserAction = ({}: ActionProps) => {
|
||||
const [_open, _setOpen] = useState(false);
|
||||
const prevent = usePreventAutoFocus();
|
||||
|
||||
if (isLoading) return null;
|
||||
|
||||
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 (isLoading) {
|
||||
return <Skeleton className="h-7 w-29.6 rounded-full" />;
|
||||
}
|
||||
|
||||
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;
|
||||
|
||||
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 {
|
||||
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 { m } from '@paraglide/messages';
|
||||
import { acceptInvitation, rejectInvitation } from '@service/house.api';
|
||||
import { notificationQueries } from '@service/queries';
|
||||
import { useMutation, useQueryClient } from '@tanstack/react-query';
|
||||
import { ItemActions, ItemContent, ItemDescription, ItemTitle } from '@ui/item';
|
||||
import { Spinner } from '@ui/spinner';
|
||||
import { toast } from 'sonner';
|
||||
|
||||
type NotifyProps = {
|
||||
@@ -21,47 +17,49 @@ const NotificationInvitation = ({ notify }: NotifyProps) => {
|
||||
|
||||
const queryClient = useQueryClient();
|
||||
|
||||
const { mutate: acceptInvitationMutation } = useMutation({
|
||||
mutationFn: acceptInvitation,
|
||||
onSuccess: () => {
|
||||
queryClient.invalidateQueries({
|
||||
queryKey: [...notificationQueries.all, 'list'],
|
||||
});
|
||||
toast.success(m.notification_page_message_invitation_success(), {
|
||||
richColors: true,
|
||||
});
|
||||
},
|
||||
onError: (error: ReturnError) => {
|
||||
console.error(error);
|
||||
const code = error.code as Parameters<
|
||||
typeof m.backend_message
|
||||
>[0]['code'];
|
||||
toast.error(m.backend_message({ code }), {
|
||||
richColors: true,
|
||||
});
|
||||
},
|
||||
});
|
||||
const { mutate: acceptInvitationMutation, isPending: isAcceptPending } =
|
||||
useMutation({
|
||||
mutationFn: acceptInvitation,
|
||||
onSuccess: () => {
|
||||
queryClient.invalidateQueries({
|
||||
queryKey: [...notificationQueries.all, 'list'],
|
||||
});
|
||||
toast.success(m.notification_page_message_invitation_success(), {
|
||||
richColors: true,
|
||||
});
|
||||
},
|
||||
onError: (error: ReturnError) => {
|
||||
console.error(error);
|
||||
const code = error.code as Parameters<
|
||||
typeof m.backend_message
|
||||
>[0]['code'];
|
||||
toast.error(m.backend_message({ code }), {
|
||||
richColors: true,
|
||||
});
|
||||
},
|
||||
});
|
||||
|
||||
const { mutate: rejectInvitationMutation } = useMutation({
|
||||
mutationFn: rejectInvitation,
|
||||
onSuccess: () => {
|
||||
queryClient.invalidateQueries({
|
||||
queryKey: [...notificationQueries.all, 'list'],
|
||||
});
|
||||
toast.success(m.notification_page_message_invitation_rejected(), {
|
||||
richColors: true,
|
||||
});
|
||||
},
|
||||
onError: (error: ReturnError) => {
|
||||
console.error(error);
|
||||
const code = error.code as Parameters<
|
||||
typeof m.backend_message
|
||||
>[0]['code'];
|
||||
toast.error(m.backend_message({ code }), {
|
||||
richColors: true,
|
||||
});
|
||||
},
|
||||
});
|
||||
const { mutate: rejectInvitationMutation, isPending: isRejectPending } =
|
||||
useMutation({
|
||||
mutationFn: rejectInvitation,
|
||||
onSuccess: () => {
|
||||
queryClient.invalidateQueries({
|
||||
queryKey: [...notificationQueries.all, 'list'],
|
||||
});
|
||||
toast.success(m.notification_page_message_invitation_rejected(), {
|
||||
richColors: true,
|
||||
});
|
||||
},
|
||||
onError: (error: ReturnError) => {
|
||||
console.error(error);
|
||||
const code = error.code as Parameters<
|
||||
typeof m.backend_message
|
||||
>[0]['code'];
|
||||
toast.error(m.backend_message({ code }), {
|
||||
richColors: true,
|
||||
});
|
||||
},
|
||||
});
|
||||
|
||||
const handleAgreeAction = async () => {
|
||||
if (notify.link) {
|
||||
@@ -95,10 +93,19 @@ const NotificationInvitation = ({ notify }: NotifyProps) => {
|
||||
</ItemContent>
|
||||
{notify.link && (
|
||||
<ItemActions>
|
||||
<Button onClick={() => handleAgreeAction()}>
|
||||
<Button
|
||||
onClick={() => handleAgreeAction()}
|
||||
disabled={isAcceptPending}
|
||||
>
|
||||
{isAcceptPending && <Spinner data-icon="inline-start" />}
|
||||
{m.ui_agree_btn()}
|
||||
</Button>
|
||||
<Button variant="outline" onClick={() => handleRejectAction()}>
|
||||
<Button
|
||||
variant="outline"
|
||||
onClick={() => handleRejectAction()}
|
||||
disabled={isRejectPending}
|
||||
>
|
||||
{isRejectPending && <Spinner data-icon="inline-start" />}
|
||||
{m.ui_reject_btn()}
|
||||
</Button>
|
||||
</ItemActions>
|
||||
|
||||
@@ -114,6 +114,9 @@ const NavMain = () => {
|
||||
to={item.path}
|
||||
className="cursor-pointer"
|
||||
tooltip={`${nav.title} - ${item.title}`}
|
||||
activeProps={{
|
||||
className: 'bg-primary text-white',
|
||||
}}
|
||||
>
|
||||
<Icon size={24} />
|
||||
{item.title}
|
||||
|
||||
@@ -35,7 +35,7 @@ const NavUser = () => {
|
||||
const navigate = useNavigate();
|
||||
const { isMobile } = useSidebar();
|
||||
const queryClient = useQueryClient();
|
||||
const { data: session } = useAuth();
|
||||
const { session } = useAuth();
|
||||
|
||||
const signout = async () => {
|
||||
await authClient.signOut({
|
||||
|
||||
@@ -5,7 +5,7 @@ import * as React from 'react';
|
||||
import { cn } from '@lib/utils';
|
||||
|
||||
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: {
|
||||
variant: {
|
||||
|
||||
@@ -6,11 +6,19 @@ function useHasPermission(
|
||||
action: string,
|
||||
houseCheck: boolean = false,
|
||||
) {
|
||||
const { data: activeOrganization } = authClient.useActiveOrganization();
|
||||
const [hasPermission, setHasPermission] = useState(false);
|
||||
const [isLoading, setIsLoading] = useState(true);
|
||||
|
||||
useEffect(() => {
|
||||
if (houseCheck && !activeOrganization?.id) {
|
||||
setIsLoading(false);
|
||||
setHasPermission(false);
|
||||
return;
|
||||
}
|
||||
|
||||
const checkPermission = async () => {
|
||||
setIsLoading(true);
|
||||
try {
|
||||
const option = {
|
||||
permissions: {
|
||||
@@ -29,7 +37,7 @@ function useHasPermission(
|
||||
}
|
||||
};
|
||||
checkPermission();
|
||||
}, [resource, action]);
|
||||
}, [resource, action, houseCheck, activeOrganization?.id]);
|
||||
|
||||
return { hasPermission, isLoading };
|
||||
}
|
||||
|
||||
@@ -1,37 +1,37 @@
|
||||
import { createAccessControl } from 'better-auth/plugins/access'
|
||||
import { createAccessControl } from 'better-auth/plugins/access';
|
||||
import {
|
||||
defaultStatements,
|
||||
adminAc,
|
||||
defaultStatements,
|
||||
ownerAc,
|
||||
} from 'better-auth/plugins/organization/access'
|
||||
} from 'better-auth/plugins/organization/access';
|
||||
|
||||
const statement = {
|
||||
...defaultStatements,
|
||||
house: ['list', 'create', 'update', 'delete'],
|
||||
house: ['list', 'create', 'update', 'delete', 'leave'],
|
||||
box: ['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({
|
||||
...ownerAc.statements,
|
||||
house: ['list', 'create', 'update', 'delete'],
|
||||
box: ['list', 'create', 'update', 'delete'],
|
||||
item: ['list', 'create', 'update', 'delete'],
|
||||
})
|
||||
});
|
||||
|
||||
const adminOrg = acOrg.newRole({
|
||||
...adminAc.statements,
|
||||
house: ['list', 'create', 'update', 'delete'],
|
||||
house: ['list', 'create', 'update', 'leave'],
|
||||
box: ['list', 'create', 'update', 'delete'],
|
||||
item: ['list', 'create', 'update', 'delete'],
|
||||
})
|
||||
});
|
||||
|
||||
const member = acOrg.newRole({
|
||||
house: ['list', 'create', 'update', 'delete'],
|
||||
box: ['list', 'create', 'update', 'delete'],
|
||||
house: ['list', 'leave'],
|
||||
box: ['list'],
|
||||
item: ['list', 'create', 'update', 'delete'],
|
||||
})
|
||||
});
|
||||
|
||||
export { acOrg, owner, adminOrg, member }
|
||||
export { acOrg, adminOrg, member, owner };
|
||||
|
||||
@@ -13,6 +13,7 @@ import {
|
||||
houseEditBESchema,
|
||||
houseListSchema,
|
||||
invitationCreateBESchema,
|
||||
removeMemberSchema,
|
||||
} from './house.schema';
|
||||
import { createAuditLog, createNotification } from './repository';
|
||||
|
||||
@@ -347,7 +348,7 @@ export const cancelInvitation = createServerFn({ method: 'POST' })
|
||||
export const acceptInvitation = createServerFn({ method: 'POST' })
|
||||
.middleware([authMiddleware])
|
||||
.inputValidator(actionInvitationSchema)
|
||||
.handler(async ({ data }) => {
|
||||
.handler(async ({ data, context: { user } }) => {
|
||||
try {
|
||||
const result = await prisma.invitation.update({
|
||||
where: { id: data.id },
|
||||
@@ -360,13 +361,33 @@ export const acceptInvitation = createServerFn({ method: 'POST' })
|
||||
data: { link: null },
|
||||
});
|
||||
|
||||
await auth.api.addMember({
|
||||
const member = await auth.api.addMember({
|
||||
body: {
|
||||
userId: notify.userId,
|
||||
organizationId: result.organizationId,
|
||||
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;
|
||||
@@ -404,3 +425,72 @@ export const rejectInvitation = createServerFn({ method: 'POST' })
|
||||
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({
|
||||
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