From 42435faa7fb10d9d3431d9f59666bbdf13b29542 Mon Sep 17 00:00:00 2001 From: Sam Date: Fri, 6 Feb 2026 11:34:23 +0700 Subject: [PATCH] Add delete house --- messages/en.json | 8 +- messages/vi.json | 6 +- package.json | 1 + pnpm-lock.yaml | 58 ++++++- src/components/audit/audit-columns.tsx | 2 +- ...-dialog.tsx => view-log-detail-dialog.tsx} | 0 src/components/house/delete-house-dialog.tsx | 155 ++++++++++++++++++ src/components/house/house-column.tsx | 4 +- ...ialog.tsx => view-house-detail-dialog.tsx} | 0 .../user/ban-user-confirm-dialog.tsx | 66 ++++++-- src/service/house.api.ts | 40 +++++ 11 files changed, 316 insertions(+), 24 deletions(-) rename src/components/audit/{view-detail-dialog.tsx => view-log-detail-dialog.tsx} (100%) create mode 100644 src/components/house/delete-house-dialog.tsx rename src/components/house/{view-detail-dialog.tsx => view-house-detail-dialog.tsx} (100%) diff --git a/messages/en.json b/messages/en.json index b97d4ec..e1ad911 100644 --- a/messages/en.json +++ b/messages/en.json @@ -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: Box, Item. 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": { diff --git a/messages/vi.json b/messages/vi.json index 2db8ba4..c4cd18b 100644 --- a/messages/vi.json +++ b/messages/vi.json @@ -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ư: Hộp chứa, Vật Phẩm. 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": { diff --git a/package.json b/package.json index be69881..2498727 100644 --- a/package.json +++ b/package.json @@ -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", diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 4f55f56..9b66164 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -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 diff --git a/src/components/audit/audit-columns.tsx b/src/components/audit/audit-columns.tsx index c0f94f7..acba04a 100644 --- a/src/components/audit/audit-columns.tsx +++ b/src/components/audit/audit-columns.tsx @@ -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[] = [ { diff --git a/src/components/audit/view-detail-dialog.tsx b/src/components/audit/view-log-detail-dialog.tsx similarity index 100% rename from src/components/audit/view-detail-dialog.tsx rename to src/components/audit/view-log-detail-dialog.tsx diff --git a/src/components/house/delete-house-dialog.tsx b/src/components/house/delete-house-dialog.tsx new file mode 100644 index 0000000..668f391 --- /dev/null +++ b/src/components/house/delete-house-dialog.tsx @@ -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 ( + + + + + + + + + + + + e.preventDefault()} + onEscapeKeyDown={(e) => e.preventDefault()} + > + + +
+ +
+ {m.houses_page_ui_dialog_alert_delete_title({ name: data.name })} +
+ + {parse(m.houses_page_ui_dialog_alert_delete_description())} + +
+
+ + + + + {m.houses_page_ui_view_table_header_email()} + + + {m.houses_page_ui_view_table_header_role()} + + + + + {data.members.map((member) => ( + + {member.user.email} + + + + + ))} + +
+
+ + + + + + +
+
+ ); + } + + return null; +}; + +export default DeleteHouseAction; diff --git a/src/components/house/house-column.tsx b/src/components/house/house-column.tsx index b08f1f1..8dfaf26 100644 --- a/src/components/house/house-column.tsx +++ b/src/components/house/house-column.tsx @@ -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[] = [ { @@ -42,6 +43,7 @@ export const houseColumns: ColumnDef[] = [
+
); }, diff --git a/src/components/house/view-detail-dialog.tsx b/src/components/house/view-house-detail-dialog.tsx similarity index 100% rename from src/components/house/view-detail-dialog.tsx rename to src/components/house/view-house-detail-dialog.tsx diff --git a/src/components/user/ban-user-confirm-dialog.tsx b/src/components/user/ban-user-confirm-dialog.tsx index ee3e6de..c802f2b 100644 --- a/src/components/user/ban-user-confirm-dialog.tsx +++ b/src/components/user/ban-user-confirm-dialog.tsx @@ -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()} - - {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'], - }), - })} - +
+ + + + + {m.users_page_ui_dialog_alert_description_title()} + + + + + + + {m.users_page_ui_table_header_name()}: + + {data.name} + + + + {m.users_page_ui_table_header_email()}: + + {data.email} + + + + {m.users_page_ui_form_ban_reason()}: + + {submitData.banReason} + + + + {m.users_page_ui_form_ban_exp()}: + + + {m.exp_time({ + time: submitData.banExp.toString() as Parameters< + typeof m.exp_time + >[0]['time'], + })} + + + +
+