Add delete house

This commit is contained in:
2026-02-06 11:34:23 +07:00
parent 7b14b30320
commit 42435faa7f
11 changed files with 316 additions and 24 deletions

View File

@@ -147,9 +147,8 @@
"users_page_ui_select_placeholder_role": "Select role",
"users_page_ui_select_placeholder_ban_exp": "Select time",
"users_page_ui_dialog_alert_title": "Unban this user?",
"users_page_ui_dialog_alert_ban_title": "",
"users_page_ui_dialog_alert_description": "Detail: \nName: {name}. \nEmail: {email}",
"users_page_ui_dialog_alert_description_2": "Reason: {reason}. \nExpiration: {exp}",
"users_page_ui_dialog_alert_ban_title": "Lock this user?",
"users_page_ui_dialog_alert_description_title": "Detail",
"houses_page_ui_title": "Houses",
"houses_page_ui_table_header_name": "Name",
"houses_page_ui_table_header_members": "Members",
@@ -161,9 +160,12 @@
"houses_page_form_user": "User",
"houses_page_form_create_for": "Create for",
"houses_page_form_color": "Color",
"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_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!",
"houses_page_message_delete_house_success": "Delete house successfully!",
"backend_message": [
{
"match": {

View File

@@ -149,8 +149,7 @@
"users_page_ui_select_placeholder_ban_exp": "Hãy chọn thời gian cấm",
"users_page_ui_dialog_alert_title": "Bạn muốn mở khóa người dùng này?",
"users_page_ui_dialog_alert_ban_title": "Bạn muốn khóa người dùng này?",
"users_page_ui_dialog_alert_description": "Chi tiết: \nTên: {name}. \nEmail: {email}",
"users_page_ui_dialog_alert_description_2": "\nLý do: {reason}. \nHiệu lực: {exp}",
"users_page_ui_dialog_alert_description_title": "Chi tiết",
"houses_page_ui_title": "Nhà",
"houses_page_ui_table_header_name": "Tên",
"houses_page_ui_table_header_members": "Thành viên",
@@ -162,9 +161,12 @@
"houses_page_form_user": "Người dùng",
"houses_page_form_create_for": "Tạo cho",
"houses_page_form_color": "Màu sắc",
"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_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!",
"houses_page_message_delete_house_success": "Xóa nhà thành công!",
"backend_message": [
{
"match": {

View File

@@ -37,6 +37,7 @@
"better-auth": "^1.4.10",
"class-variance-authority": "^0.7.1",
"clsx": "^2.1.1",
"html-react-parser": "^5.2.16",
"next-themes": "^0.4.6",
"prisma": "^7.1.0",
"radix-ui": "^1.4.3",

58
pnpm-lock.yaml generated
View File

@@ -9,7 +9,7 @@ importers:
.:
dependencies:
'@base-ui/react':
specifier: ^1.1.0
specifier: ^1.0.0
version: 1.1.0(@types/react@19.2.10)(react-dom@19.2.4(react@19.2.4))(react@19.2.4)
'@fontsource-variable/inter':
specifier: ^5.2.8
@@ -65,6 +65,9 @@ importers:
clsx:
specifier: ^2.1.1
version: 2.1.1
html-react-parser:
specifier: ^5.2.16
version: 5.2.16(@types/react@19.2.10)(react@19.2.4)
next-themes:
specifier: ^0.4.6
version: 0.4.6(react-dom@19.2.4(react@19.2.4))(react@19.2.4)
@@ -81,7 +84,7 @@ importers:
specifier: ^19.2.4
version: 19.2.4(react@19.2.4)
shadcn:
specifier: 3.8.2
specifier: ^3.6.1
version: 3.8.2(@types/node@25.2.0)(hono@4.11.4)(typescript@5.9.3)
sonner:
specifier: ^2.0.7
@@ -3299,10 +3302,22 @@ packages:
resolution: {integrity: sha512-U7tt8JsyrxSRKspfhtLET79pU8K+tInj5QZXs1jSugO1Vq5dFj3kmZsRldo29mTBfcjDRVRXrEZ6LS63Cog9ZA==}
engines: {node: '>=16.9.0'}
html-dom-parser@5.1.7:
resolution: {integrity: sha512-Sn+6S3Z8P3P12qqUm4+9wnchC3Bjc4DHp60fgnUdgeiy6e3EbECFWdrmyTBuphxJA5Is7V400+v7ct/Ix2pJFw==}
html-encoding-sniffer@6.0.0:
resolution: {integrity: sha512-CV9TW3Y3f8/wT0BRFc1/KAVQ3TUHiXmaAb6VW9vtiMFf7SLoMd1PdAc4W3KFOFETBJUb90KatHqlsZMWV+R9Gg==}
engines: {node: ^20.19.0 || ^22.12.0 || >=24.0.0}
html-react-parser@5.2.16:
resolution: {integrity: sha512-1S6KLse1hKWOXYL/PSnZhsARJBE6eIO93CjPlDKMneO0wz8YTnzTfc9Yw4mWsCk2kcB9IrU+R0W6Rdi4N7YfJw==}
peerDependencies:
'@types/react': 0.14 || 15 || 16 || 17 || 18 || 19
react: 0.14 || 15 || 16 || 17 || 18 || 19
peerDependenciesMeta:
'@types/react':
optional: true
htmlparser2@10.1.0:
resolution: {integrity: sha512-VTZkM9GWRAtEpveh7MSF6SjjrpNVNNVJfFup7xTY3UpFtm67foy9HDVXneLtFVt4pMz5kZtgNcvCniNFb1hlEQ==}
@@ -3360,6 +3375,9 @@ packages:
inherits@2.0.4:
resolution: {integrity: sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==}
inline-style-parser@0.2.7:
resolution: {integrity: sha512-Nb2ctOyNR8DqQoR0OwRG95uNWIC0C1lCgf5Naz5H6Ji72KZ8OcFZLz2P5sNgwlyoJ8Yif11oMuYs5pBQa86csA==}
ipaddr.js@1.9.1:
resolution: {integrity: sha512-0KI/607xoxSToH7GjN1FfSbLoU0+btTicjsQSWQlh/hZykN8KpmMf7uYwPW3R+akZ6R/w18ZlXSHBYXiYUPO3g==}
engines: {node: '>= 0.10'}
@@ -4087,6 +4105,9 @@ packages:
react-is@17.0.2:
resolution: {integrity: sha512-w2GsyukL62IJnlaff/nRegPQR94C/XXamvMWmSHRJ4y7Ts/4ocGRmTHvOs8PSE6pB3dWOrD/nueuU5sduBsQ4w==}
react-property@2.0.2:
resolution: {integrity: sha512-+PbtI3VuDV0l6CleQMsx2gtK0JZbZKbpdu5ynr+lbsuvtmgbNcS3VM0tuY2QjFNOcWxvXeHjDpy42RO+4U2rug==}
react-refresh@0.18.0:
resolution: {integrity: sha512-QgT5//D3jfjJb6Gsjxv0Slpj23ip+HtOpnNgnb2S5zU3CB26G/IDPGoy4RJB42wzFE46DRsstbW6tKHoKbhAxw==}
engines: {node: '>=0.10.0'}
@@ -4382,6 +4403,12 @@ packages:
resolution: {integrity: sha512-6fPc+R4ihwqP6N/aIv2f1gMH8lOVtWQHoqC4yK6oSDVVocumAsfCqjkXnqiYMhmMwS/mEHLp7Vehlt3ql6lEig==}
engines: {node: '>=8'}
style-to-js@1.1.21:
resolution: {integrity: sha512-RjQetxJrrUJLQPHbLku6U/ocGtzyjbJMP9lCNK7Ag0CNh690nSH8woqWH9u16nMjYBAok+i7JO1NP2pOy8IsPQ==}
style-to-object@1.0.14:
resolution: {integrity: sha512-LIN7rULI0jBscWQYaSswptyderlarFkjQ+t79nzty8tcIAceVomEVlLzH5VP4Cmsv6MtKhs7qaAiwlcp+Mgaxw==}
supports-color@7.2.0:
resolution: {integrity: sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==}
engines: {node: '>=8'}
@@ -8159,12 +8186,27 @@ snapshots:
hono@4.11.4: {}
html-dom-parser@5.1.7:
dependencies:
domhandler: 5.0.3
htmlparser2: 10.1.0
html-encoding-sniffer@6.0.0(@noble/hashes@2.0.1):
dependencies:
'@exodus/bytes': 1.11.0(@noble/hashes@2.0.1)
transitivePeerDependencies:
- '@noble/hashes'
html-react-parser@5.2.16(@types/react@19.2.10)(react@19.2.4):
dependencies:
domhandler: 5.0.3
html-dom-parser: 5.1.7
react: 19.2.4
react-property: 2.0.2
style-to-js: 1.1.21
optionalDependencies:
'@types/react': 19.2.10
htmlparser2@10.1.0:
dependencies:
domelementtype: 2.3.0
@@ -8223,6 +8265,8 @@ snapshots:
inherits@2.0.4: {}
inline-style-parser@0.2.7: {}
ipaddr.js@1.9.1: {}
is-arrayish@0.2.1: {}
@@ -8913,6 +8957,8 @@ snapshots:
react-is@17.0.2: {}
react-property@2.0.2: {}
react-refresh@0.18.0: {}
react-remove-scroll-bar@2.3.8(@types/react@19.2.10)(react@19.2.4):
@@ -9243,6 +9289,14 @@ snapshots:
strip-json-comments@3.1.1: {}
style-to-js@1.1.21:
dependencies:
style-to-object: 1.0.14
style-to-object@1.0.14:
dependencies:
inline-style-parser: 0.2.7
supports-color@7.2.0:
dependencies:
has-flag: 4.0.0

View File

@@ -5,7 +5,7 @@ import { formatters } from '@utils/formatters';
import { LOG_ACTION } from '@/types/enum';
import ActionBadge from './action-badge';
import ViewDetailAudit from './view-detail-dialog';
import ViewDetailAudit from './view-log-detail-dialog';
export const logColumns: ColumnDef<AuditWithUser>[] = [
{

View File

@@ -0,0 +1,155 @@
import { m } from '@/paraglide/messages';
import { deleteHouse } from '@/service/house.api';
import { housesQueries } from '@/service/queries';
import { ReturnError } from '@/types/common';
import useHasPermission from '@hooks/use-has-permission';
import usePreventAutoFocus from '@hooks/use-prevent-auto-focus';
import { ShieldWarningIcon, TrashIcon } from '@phosphor-icons/react';
import { useMutation, useQueryClient } from '@tanstack/react-query';
import { Button } from '@ui/button';
import {
Dialog,
DialogClose,
DialogContent,
DialogDescription,
DialogFooter,
DialogHeader,
DialogTitle,
DialogTrigger,
} from '@ui/dialog';
import { Label } from '@ui/label';
import {
Table,
TableBody,
TableCell,
TableHead,
TableHeader,
TableRow,
} from '@ui/table';
import { Tooltip, TooltipContent, TooltipTrigger } from '@ui/tooltip';
import parse from 'html-react-parser';
import { useState } from 'react';
import { toast } from 'sonner';
import RoleBadge from '../avatar/role-badge';
type DeleteHouseProps = {
data: OrganizationWithMembers;
};
const DeleteHouseAction = ({ data }: DeleteHouseProps) => {
const [_open, _setOpen] = useState(false);
const prevent = usePreventAutoFocus();
const { hasPermission, isLoading } = useHasPermission('house', 'delete');
const queryClient = useQueryClient();
const { mutate: deleteHouseMutation } = useMutation({
mutationFn: deleteHouse,
onSuccess: () => {
queryClient.invalidateQueries({
queryKey: [...housesQueries.all, 'list'],
});
_setOpen(false);
toast.success(m.houses_page_message_delete_house_success(), {
richColors: true,
});
},
onError: (error: ReturnError) => {
console.error(error);
const code = error.code as Parameters<
typeof m.backend_message
>[0]['code'];
toast.error(m.backend_message({ code }), {
richColors: true,
});
},
});
const onConfirm = () => {
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}>
{m.ui_confirm_btn()}
</Button>
</DialogFooter>
</DialogContent>
</Dialog>
);
}
return null;
};
export default DeleteHouseAction;

View File

@@ -1,8 +1,9 @@
import { m } from '@paraglide/messages';
import { ColumnDef } from '@tanstack/react-table';
import { formatters } from '@utils/formatters';
import DeleteHouseAction from './delete-house-dialog';
import EditHouseAction from './edit-house-dialog';
import ViewDetailHouse from './view-detail-dialog';
import ViewDetailHouse from './view-house-detail-dialog';
export const houseColumns: ColumnDef<OrganizationWithMembers>[] = [
{
@@ -42,6 +43,7 @@ export const houseColumns: ColumnDef<OrganizationWithMembers>[] = [
<div className="flex justify-end gap-2">
<ViewDetailHouse data={row.original} />
<EditHouseAction data={row.original} />
<DeleteHouseAction data={row.original} />
</div>
);
},

View File

@@ -17,7 +17,14 @@ import {
} from '@ui/dialog';
import { UserWithRole } from 'better-auth/plugins';
import { toast } from 'sonner';
import DisplayBreakLineMessage from '../DisplayBreakLineMessage';
import {
Table,
TableBody,
TableCell,
TableHead,
TableHeader,
TableRow,
} from '../ui/table';
import { useBanContext } from './ban-user-dialog';
type BanConfirmProps = {
@@ -74,20 +81,49 @@ const BanUserConfirm = ({ data }: BanConfirmProps) => {
{m.users_page_ui_dialog_alert_ban_title()}
</DialogDescription>
</DialogHeader>
<DisplayBreakLineMessage>
{m.users_page_ui_dialog_alert_description({
name: data.name,
email: data.email,
})}
{m.users_page_ui_dialog_alert_description_2({
reason: submitData.banReason,
exp: m.exp_time({
time: submitData.banExp.toString() as Parameters<
typeof m.exp_time
>[0]['time'],
}),
})}
</DisplayBreakLineMessage>
<div className="overflow-hidden rounded-md border">
<Table className="bg-white">
<TableHeader>
<TableRow className="bg-primary">
<TableHead className="px-2 h-7 text-white text-xs" colSpan={2}>
{m.users_page_ui_dialog_alert_description_title()}
</TableHead>
</TableRow>
</TableHeader>
<TableBody>
<TableRow>
<TableCell className="font-bold">
{m.users_page_ui_table_header_name()}:
</TableCell>
<TableCell>{data.name}</TableCell>
</TableRow>
<TableRow>
<TableCell className="font-bold">
{m.users_page_ui_table_header_email()}:
</TableCell>
<TableCell>{data.email}</TableCell>
</TableRow>
<TableRow>
<TableCell className="font-bold">
{m.users_page_ui_form_ban_reason()}:
</TableCell>
<TableCell>{submitData.banReason}</TableCell>
</TableRow>
<TableRow>
<TableCell className="font-bold">
{m.users_page_ui_form_ban_exp()}:
</TableCell>
<TableCell>
{m.exp_time({
time: submitData.banExp.toString() as Parameters<
typeof m.exp_time
>[0]['time'],
})}
</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">

View File

@@ -6,6 +6,7 @@ import { authMiddleware } from '@lib/middleware';
import { createServerFn } from '@tanstack/react-start';
import { parseError } from '@utils/helper';
import {
baseHouse,
houseCreateBESchema,
houseEditBESchema,
houseListSchema,
@@ -137,3 +138,42 @@ export const updateHouse = createServerFn({ method: 'POST' })
throw { message, code };
}
});
export const deleteHouse = createServerFn({ method: 'POST' })
.middleware([authMiddleware])
.inputValidator(baseHouse)
.handler(async ({ data, context: { user } }) => {
try {
const currentHouse = await prisma.organization.findUnique({
where: { id: data.id },
});
if (!currentHouse) throw Error('House not found');
const result = await Promise.all([
prisma.organization.delete({
where: { id: data.id },
}),
prisma.member.deleteMany({
where: { organizationId: data.id },
}),
prisma.invitation.deleteMany({
where: { organizationId: data.id },
}),
]);
await createAuditLog({
action: LOG_ACTION.DELETE,
tableName: DB_TABLE.ORGANIZATION,
recordId: result[0]?.id,
oldValue: JSON.stringify(currentHouse),
newValue: '',
userId: user.id,
});
return result;
} catch (error) {
console.error(error);
const { message, code } = parseError(error);
throw { message, code };
}
});