feature/notification #12

Merged
sam merged 2 commits from feature/notification into develop 2026-02-21 15:35:42 +00:00
43 changed files with 2902 additions and 116 deletions

View File

@@ -13,7 +13,15 @@
"common_per_page": "Show", "common_per_page": "Show",
"common_select_page_size": "Select page size", "common_select_page_size": "Select page size",
"common_no_list": "Currently there is no data!", "common_no_list": "Currently there is no data!",
"common_no_notify": "Không có thống báo",
"common_is_required": "{field} is required.", "common_is_required": "{field} is required.",
"common_time_ago_second": "{value} giây trước",
"common_time_ago_minute": "{value} phút trước",
"common_time_ago_hour": "{value} giờ trước",
"common_time_ago_day": "{value} ngày trước",
"common_time_ago_week": "{value} tuần trước",
"common_time_ago_month": "{value} tháng trước",
"common_time_ago_year": "{value} năm trước",
"role_tags": [ "role_tags": [
{ {
"match": { "match": {
@@ -170,23 +178,42 @@
"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!",
"houses_page_message_delete_house_success": "Delete house successfully!", "houses_page_message_delete_house_success": "Delete house successfully!",
"houses_page_message_invite_member_success": "Invite member successfully!",
"houses_page_message_cancel_invitation_success": "Cancel invitation successfully!",
"houses_page_house_active_btn": "Active", "houses_page_house_active_btn": "Active",
"houses_user_page_message_active_house_success": "Active \"<b>{house}</b>\" successfully!", "houses_user_page_message_active_house_success": "Active \"<b>{house}</b>\" successfully!",
"houses_user_page_block_action_title": "Action", "houses_user_page_block_action_title": "Action",
"houses_user_page_action_invite_user": "Invite member", "houses_user_page_action_invite_user": "Invite member",
"houses_user_page_invite_label_to": "To", "houses_user_page_invite_label_to": "To",
"houses_user_page_invite_label_status": "Status", "houses_user_page_invite_label_status": "Status",
"invitation_not_found": "Invitation not found!",
"notification_page_notify_not_found": "Notification not found!",
"invite_status": [ "invite_status": [
{ {
"match": { "match": {
"status=pending": "Pending", "status=pending": "Pending",
"status=accept": "Accept", "status=accepted": "Accept",
"status=reject": "Reject", "status=rejected": "Reject",
"status=expired": "Expired", "status=expired": "Expired",
"status=canceled": "Cancel" "status=canceled": "Cancel"
} }
} }
], ],
"templates_title_notification": [
{
"match": {
"title=INVITATION_HOUSE": "Invite to join house"
}
}
],
"templates_message_notification": [
{
"match": {
"message=INVITATION_HOUSE": "You have been invited to join house: {name}, do you accept?"
}
}
],
"notification_page_message_invitation_success": "You have been invited to join house!",
"backend_message": [ "backend_message": [
{ {
"match": { "match": {
@@ -197,7 +224,8 @@
"code=BANNED_USER": "Your account get banned, please contact administrator for more information!", "code=BANNED_USER": "Your account get banned, please contact administrator for more information!",
"code=VALIDATION_ERROR": "Some field value invalid!", "code=VALIDATION_ERROR": "Some field value invalid!",
"code=USER_IS_NOT_A_MEMBER_OF_THE_ORGANIZATION": "User is not a member of the house", "code=USER_IS_NOT_A_MEMBER_OF_THE_ORGANIZATION": "User is not a member of the house",
"code=USER_IS_ALREADY_INVITED_TO_THIS_ORGANIZATION": "This member has already been invited, waiting for the member to join!" "code=USER_IS_ALREADY_INVITED_TO_THIS_ORGANIZATION": "This member has already been invited, waiting for the member to join!",
"code=INVITATION_NOT_FOUND": "Invitation not found!"
} }
} }
] ]

View File

@@ -13,7 +13,15 @@
"common_per_page": "Hiển thị", "common_per_page": "Hiển thị",
"common_select_page_size": "Chọn số lượng", "common_select_page_size": "Chọn số lượng",
"common_no_list": "Hiện tại chưa có dữ liệu nào!", "common_no_list": "Hiện tại chưa có dữ liệu nào!",
"common_no_notify": "Không có thống báo",
"common_is_required": "{field} là bắt buộc.", "common_is_required": "{field} là bắt buộc.",
"common_time_ago_second": "{value} giây trước",
"common_time_ago_minute": "{value} phút trước",
"common_time_ago_hour": "{value} giờ trước",
"common_time_ago_day": "{value} ngày trước",
"common_time_ago_week": "{value} tuần trước",
"common_time_ago_month": "{value} tháng trước",
"common_time_ago_year": "{value} năm trước",
"role_tags": [ "role_tags": [
{ {
"match": { "match": {
@@ -50,6 +58,8 @@
"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",
"ui_agree_btn": "Đồng ý",
"ui_reject_btn": "Từ chối",
"ui_update_password_btn": "Đặt lại mật khẩu", "ui_update_password_btn": "Đặt lại mật khẩu",
"ui_change_role_btn": "Đặt lại quyền hạn", "ui_change_role_btn": "Đặt lại quyền hạn",
"ui_edit_user_btn": "Chỉnh sửa người dùng", "ui_edit_user_btn": "Chỉnh sửa người dùng",
@@ -172,23 +182,43 @@
"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!",
"houses_page_message_delete_house_success": "Xóa nhà thành công!", "houses_page_message_delete_house_success": "Xóa nhà thành công!",
"houses_page_message_invite_member_success": "Mời thành viên thành công!",
"houses_page_message_cancel_invitation_success": "Hủy lời mời thành công!",
"houses_page_house_active_btn": "Kích hoạt", "houses_page_house_active_btn": "Kích hoạt",
"houses_user_page_message_active_house_success": "Kích hoạt \"<b>{house}</b>\" thành công!", "houses_user_page_message_active_house_success": "Kích hoạt \"<b>{house}</b>\" thành công!",
"houses_user_page_block_action_title": "Hành động", "houses_user_page_block_action_title": "Hành động",
"houses_user_page_action_invite_user": "Mời thành viên", "houses_user_page_action_invite_user": "Mời thành viên",
"houses_user_page_invite_label_to": "Đến", "houses_user_page_invite_label_to": "Đến",
"houses_user_page_invite_label_status": "Trạng thái", "houses_user_page_invite_label_status": "Trạng thái",
"invitation_not_found": "Không tìm thấy lời mời!",
"notification_page_notify_not_found": "Không tìm thấy thống báo!",
"invite_status": [ "invite_status": [
{ {
"match": { "match": {
"status=pending": "Đang chờ", "status=pending": "Đang chờ",
"status=accept": "Đồng ý", "status=accepted": "Đồng ý",
"status=reject": "Không đồng ý", "status=rejected": "Không đồng ý",
"status=expired": "Hết hạn", "status=expired": "Hết hạn",
"status=canceled": "Đã hủy" "status=canceled": "Đã hủy"
} }
} }
], ],
"templates_title_notification": [
{
"match": {
"title=INVITATION_HOUSE": "Mời tham gia nhà"
}
}
],
"templates_message_notification": [
{
"match": {
"message=INVITATION_HOUSE": "mời bạn tham gia nhà: {name}, bạn có đồng ý không?"
}
}
],
"notification_page_message_invitation_success": "Bạn đã đồng ý tham gia nhà!",
"notification_page_message_invitation_rejected": "Bạn đã từ chối tham gia nhà!",
"backend_message": [ "backend_message": [
{ {
"match": { "match": {
@@ -199,7 +229,8 @@
"code=BANNED_USER": "Bạn đã bị quản trị viên khóa tài khoản, hãy liên hệ quản trị viên để tìm hiểu thêm!", "code=BANNED_USER": "Bạn đã bị quản trị viên khóa tài khoản, hãy liên hệ quản trị viên để tìm hiểu thêm!",
"code=VALIDATION_ERROR": "Có giá trị không hợp lệ!", "code=VALIDATION_ERROR": "Có giá trị không hợp lệ!",
"code=USER_IS_NOT_A_MEMBER_OF_THE_ORGANIZATION": "Người dùng này không phải thành viên nhà này", "code=USER_IS_NOT_A_MEMBER_OF_THE_ORGANIZATION": "Người dùng này không phải thành viên nhà này",
"code=USER_IS_ALREADY_INVITED_TO_THIS_ORGANIZATION": "Thành viên này đã được mời rồi, còn đang đợi thành viên đồng ý!" "code=USER_IS_ALREADY_INVITED_TO_THIS_ORGANIZATION": "Thành viên này đã được mời rồi, còn đang đợi thành viên đồng ý!",
"code=INVITATION_NOT_FOUND": "Không tìm thấy lời mời!"
} }
} }
] ]

View File

@@ -11,6 +11,7 @@
"format": "prettier", "format": "prettier",
"check": "prettier --write . && eslint --fix", "check": "prettier --write . && eslint --fix",
"post-cta-init": "npx create-db@latest", "post-cta-init": "npx create-db@latest",
"db:reset": "dotenv -e .env.local -- prisma migrate reset",
"db:generate": "dotenv -e .env.local -- prisma generate", "db:generate": "dotenv -e .env.local -- prisma generate",
"db:push": "dotenv -e .env.local -- prisma db push", "db:push": "dotenv -e .env.local -- prisma db push",
"db:migrate": "dotenv -e .env.local -- prisma migrate dev", "db:migrate": "dotenv -e .env.local -- prisma migrate dev",
@@ -49,7 +50,8 @@
"tailwindcss": "^4.0.6", "tailwindcss": "^4.0.6",
"tw-animate-css": "^1.3.6", "tw-animate-css": "^1.3.6",
"vite-tsconfig-paths": "^6.0.5", "vite-tsconfig-paths": "^6.0.5",
"zod": "^4.3.6" "zod": "^4.3.6",
"zustand": "^5.0.11"
}, },
"devDependencies": { "devDependencies": {
"@inlang/paraglide-js": "2.10.0", "@inlang/paraglide-js": "2.10.0",

27
pnpm-lock.yaml generated
View File

@@ -104,6 +104,9 @@ importers:
zod: zod:
specifier: ^4.3.6 specifier: ^4.3.6
version: 4.3.6 version: 4.3.6
zustand:
specifier: ^5.0.11
version: 5.0.11(@types/react@19.2.10)(react@19.2.4)(use-sync-external-store@1.6.0(react@19.2.4))
devDependencies: devDependencies:
'@inlang/paraglide-js': '@inlang/paraglide-js':
specifier: 2.10.0 specifier: 2.10.0
@@ -4866,6 +4869,24 @@ packages:
zod@4.3.6: zod@4.3.6:
resolution: {integrity: sha512-rftlrkhHZOcjDwkGlnUtZZkvaPHCsDATp4pGpuOOMDaTdDDXF91wuVDJoWoPsKX/3YPQ5fHuF3STjcYyKr+Qhg==} resolution: {integrity: sha512-rftlrkhHZOcjDwkGlnUtZZkvaPHCsDATp4pGpuOOMDaTdDDXF91wuVDJoWoPsKX/3YPQ5fHuF3STjcYyKr+Qhg==}
zustand@5.0.11:
resolution: {integrity: sha512-fdZY+dk7zn/vbWNCYmzZULHRrss0jx5pPFiOuMZ/5HJN6Yv3u+1Wswy/4MpZEkEGhtNH+pwxZB8OKgUBPzYAGg==}
engines: {node: '>=12.20.0'}
peerDependencies:
'@types/react': '>=18.0.0'
immer: '>=9.0.6'
react: '>=18.0.0'
use-sync-external-store: '>=1.2.0'
peerDependenciesMeta:
'@types/react':
optional: true
immer:
optional: true
react:
optional: true
use-sync-external-store:
optional: true
snapshots: snapshots:
'@acemir/cssom@0.9.31': {} '@acemir/cssom@0.9.31': {}
@@ -9690,3 +9711,9 @@ snapshots:
zod@3.25.76: {} zod@3.25.76: {}
zod@4.3.6: {} zod@4.3.6: {}
zustand@5.0.11(@types/react@19.2.10)(react@19.2.4)(use-sync-external-store@1.6.0(react@19.2.4)):
optionalDependencies:
'@types/react': 19.2.10
react: 19.2.4
use-sync-external-store: 1.6.0(react@19.2.4)

View File

@@ -15,3 +15,14 @@ export const settingsData = [
description: 'The keywords of the site', description: 'The keywords of the site',
}, },
]; ];
export const userData = [
{
name: 'Raysam',
email: 'raysam024@gmail.com',
},
{
name: 'Raysam',
email: 'juines.liu@gmail.com',
},
];

View File

@@ -0,0 +1,57 @@
-- AlterTable
ALTER TABLE "account" ALTER COLUMN "createdAt" SET DATA TYPE TIMESTAMPTZ,
ALTER COLUMN "updatedAt" SET DATA TYPE TIMESTAMPTZ;
-- AlterTable
ALTER TABLE "audit" ALTER COLUMN "createdAt" SET DATA TYPE TIMESTAMPTZ;
-- AlterTable
ALTER TABLE "invitation" ALTER COLUMN "expiresAt" SET DATA TYPE TIMESTAMPTZ,
ALTER COLUMN "createdAt" SET DATA TYPE TIMESTAMPTZ;
-- AlterTable
ALTER TABLE "member" ALTER COLUMN "createdAt" SET DATA TYPE TIMESTAMPTZ;
-- AlterTable
ALTER TABLE "organization" ALTER COLUMN "createdAt" SET DATA TYPE TIMESTAMPTZ;
-- AlterTable
ALTER TABLE "session" ALTER COLUMN "expiresAt" SET DATA TYPE TIMESTAMPTZ,
ALTER COLUMN "createdAt" SET DATA TYPE TIMESTAMPTZ,
ALTER COLUMN "updatedAt" SET DATA TYPE TIMESTAMPTZ;
-- AlterTable
ALTER TABLE "setting" ALTER COLUMN "createdAt" SET DATA TYPE TIMESTAMPTZ,
ALTER COLUMN "updatedAt" SET DATA TYPE TIMESTAMPTZ;
-- AlterTable
ALTER TABLE "user" ALTER COLUMN "createdAt" SET DATA TYPE TIMESTAMPTZ,
ALTER COLUMN "updatedAt" SET DATA TYPE TIMESTAMPTZ;
-- AlterTable
ALTER TABLE "verification" ALTER COLUMN "createdAt" SET DATA TYPE TIMESTAMPTZ,
ALTER COLUMN "updatedAt" SET DATA TYPE TIMESTAMPTZ;
-- CreateTable
CREATE TABLE "notification" (
"id" TEXT NOT NULL,
"userId" TEXT NOT NULL,
"title" TEXT NOT NULL,
"message" TEXT NOT NULL,
"type" TEXT NOT NULL DEFAULT 'system',
"link" TEXT,
"metadata" TEXT,
"createdAt" TIMESTAMPTZ NOT NULL DEFAULT CURRENT_TIMESTAMP,
"readAt" TIMESTAMPTZ,
CONSTRAINT "notification_pkey" PRIMARY KEY ("id")
);
-- CreateIndex
CREATE INDEX "notification_userId_readAt_idx" ON "notification"("userId", "readAt");
-- CreateIndex
CREATE INDEX "notification_readAt_idx" ON "notification"("readAt");
-- AddForeignKey
ALTER TABLE "notification" ADD CONSTRAINT "notification_userId_fkey" FOREIGN KEY ("userId") REFERENCES "user"("id") ON DELETE CASCADE ON UPDATE CASCADE;

View File

@@ -14,11 +14,12 @@ model User {
email String email String
emailVerified Boolean @default(false) emailVerified Boolean @default(false)
image String? image String?
createdAt DateTime @default(now()) createdAt DateTime @default(now()) @db.Timestamptz
updatedAt DateTime @updatedAt updatedAt DateTime @updatedAt @db.Timestamptz
sessions Session[] sessions Session[]
accounts Account[] accounts Account[]
audit Audit[] audit Audit[]
notification Notification[]
role String? role String?
banned Boolean? @default(false) banned Boolean? @default(false)
@@ -34,10 +35,10 @@ model User {
model Session { model Session {
id String @id @default(uuid()) id String @id @default(uuid())
expiresAt DateTime expiresAt DateTime @db.Timestamptz
token String token String
createdAt DateTime @default(now()) createdAt DateTime @default(now()) @db.Timestamptz
updatedAt DateTime @updatedAt updatedAt DateTime @updatedAt @db.Timestamptz
ipAddress String? ipAddress String?
userAgent String? userAgent String?
userId String userId String
@@ -65,8 +66,8 @@ model Account {
refreshTokenExpiresAt DateTime? refreshTokenExpiresAt DateTime?
scope String? scope String?
password String? password String?
createdAt DateTime @default(now()) createdAt DateTime @default(now()) @db.Timestamptz
updatedAt DateTime @updatedAt updatedAt DateTime @updatedAt @db.Timestamptz
@@index([userId]) @@index([userId])
@@map("account") @@map("account")
@@ -77,8 +78,8 @@ model Verification {
identifier String identifier String
value String value String
expiresAt DateTime expiresAt DateTime
createdAt DateTime @default(now()) createdAt DateTime @default(now()) @db.Timestamptz
updatedAt DateTime @updatedAt updatedAt DateTime @updatedAt @db.Timestamptz
@@index([identifier]) @@index([identifier])
@@map("verification") @@map("verification")
@@ -89,7 +90,7 @@ model Organization {
name String name String
slug String slug String
logo String? logo String?
createdAt DateTime createdAt DateTime @db.Timestamptz
metadata String? metadata String?
members Member[] members Member[]
invitations Invitation[] invitations Invitation[]
@@ -107,7 +108,7 @@ model Member {
userId String userId String
user User @relation(fields: [userId], references: [id], onDelete: Cascade) user User @relation(fields: [userId], references: [id], onDelete: Cascade)
role String @default("member") role String @default("member")
createdAt DateTime createdAt DateTime @db.Timestamptz
@@index([organizationId]) @@index([organizationId])
@@index([userId]) @@index([userId])
@@ -121,8 +122,8 @@ model Invitation {
email String email String
role String? role String?
status String @default("pending") status String @default("pending")
expiresAt DateTime expiresAt DateTime @db.Timestamptz
createdAt DateTime @default(now()) createdAt DateTime @default(now()) @db.Timestamptz
inviterId String inviterId String
user User @relation(fields: [inviterId], references: [id], onDelete: Cascade) user User @relation(fields: [inviterId], references: [id], onDelete: Cascade)
@@ -138,8 +139,8 @@ model Setting {
description String description String
relation String @default("admin") relation String @default("admin")
createdAt DateTime @default(now()) createdAt DateTime @default(now()) @db.Timestamptz
updatedAt DateTime @updatedAt updatedAt DateTime @updatedAt @db.Timestamptz
@@map("setting") @@map("setting")
} }
@@ -152,9 +153,30 @@ model Audit {
recordId String recordId String
oldValue String? oldValue String?
newValue String? newValue String?
createdAt DateTime @default(now()) createdAt DateTime @default(now()) @db.Timestamptz
user User @relation(fields: [userId], references: [id], onDelete: Cascade) user User @relation(fields: [userId], references: [id], onDelete: Cascade)
@@map("audit") @@map("audit")
} }
model Notification {
id String @id @default(uuid())
userId String
title String
message String
type String @default("system")
link String?
metadata String?
createdAt DateTime @default(now()) @db.Timestamptz
readAt DateTime? @db.Timestamptz
user User @relation(fields: [userId], references: [id], onDelete: Cascade)
@@index([userId, readAt])
@@index([readAt])
@@map("notification")
}

View File

@@ -1,7 +1,7 @@
import { auth } from '@lib/auth'; import { auth } from '@lib/auth';
import { PrismaPg } from '@prisma/adapter-pg'; import { PrismaPg } from '@prisma/adapter-pg';
import { PrismaClient } from '../src/generated/prisma/client.js'; import { PrismaClient } from '../src/generated/prisma/client.js';
import { settingsData } from './data.js'; import { settingsData, userData } from './data.js';
const adapter = new PrismaPg({ const adapter = new PrismaPg({
connectionString: process.env.DATABASE_URL!, connectionString: process.env.DATABASE_URL!,
@@ -32,6 +32,19 @@ async function main() {
} }
console.log('---------------Created admin user-----------------'); console.log('---------------Created admin user-----------------');
userData.map(async (user) => {
await auth.api.createUser({
body: {
email: user.email,
password: 'Th@m!S@m!040390',
name: user.name,
role: 'user',
},
});
});
console.log('---------------Created member user-----------------');
await prisma.setting.deleteMany(); await prisma.setting.deleteMany();
const listSettings = [ const listSettings = [

View File

@@ -3,8 +3,8 @@
"baseLocale": "en", "baseLocale": "en",
"locales": ["en", "vi"], "locales": ["en", "vi"],
"modules": [ "modules": [
"https://cdn.jsdelivr.net/npm/@inlang/plugin-message-format@4/dist/index.js", "https://cdn.jsdelivr.net/npm/@inlang/plugin-message-format@latest/dist/index.js",
"https://cdn.jsdelivr.net/npm/@inlang/plugin-m-function-matcher@2/dist/index.js" "https://cdn.jsdelivr.net/npm/@inlang/plugin-m-function-matcher@latest/dist/index.js"
], ],
"plugin.inlang.messageFormat": { "plugin.inlang.messageFormat": {
"pathPattern": "./messages/{locale}.json" "pathPattern": "./messages/{locale}.json"

View File

@@ -1,19 +1,7 @@
import { Separator } from '@base-ui/react/separator'; import { Separator } from '@base-ui/react/separator';
import { m } from '@paraglide/messages';
import { BellIcon } from '@phosphor-icons/react';
import { Badge } from '@ui/badge';
import { Button } from '@ui/button';
import {
DropdownMenu,
DropdownMenuContent,
DropdownMenuGroup,
DropdownMenuItem,
DropdownMenuLabel,
DropdownMenuSeparator,
DropdownMenuTrigger,
} from '@ui/dropdown-menu';
import { SidebarTrigger } from '@ui/sidebar'; import { SidebarTrigger } from '@ui/sidebar';
import { useAuth } from './auth/auth-provider'; import { useAuth } from './auth/auth-provider';
import Notification from './Notification';
import RouterBreadcrumb from './sidebar/router-breadcrumb'; import RouterBreadcrumb from './sidebar/router-breadcrumb';
export default function Header() { export default function Header() {
@@ -30,48 +18,7 @@ export default function Header() {
/> />
<RouterBreadcrumb /> <RouterBreadcrumb />
</div> </div>
<div className="flex mr-2"> <div className="flex mr-2">{session?.user && <Notification />}</div>
{session?.user && (
<DropdownMenu>
<DropdownMenuTrigger asChild>
<Button size="lg" variant="ghost" className="relative">
<BellIcon size={32} />
{false && (
<Badge
variant="destructive"
className="absolute -top-1 -right-1 h-5 w-5 flex items-center justify-center p-0 text-xs"
>
0
</Badge>
)}
<span className="sr-only">Notifications</span>
</Button>
</DropdownMenuTrigger>
<DropdownMenuContent className="w-sm min-w-56 rounded-lg">
<DropdownMenuLabel className="font-bold text-black">
{m.ui_label_notifications()}
</DropdownMenuLabel>
<DropdownMenuSeparator />
<DropdownMenuGroup>
<DropdownMenuItem>
<div className="flex flex-col gap-1">
<p className="text-sm font-medium">System</p>
<p className="text-xs text-muted-foreground">
1 hour ago
</p>
</div>
</DropdownMenuItem>
</DropdownMenuGroup>
<DropdownMenuGroup>
<DropdownMenuSeparator />
<DropdownMenuItem>
{m.ui_view_all_notifications()}
</DropdownMenuItem>
</DropdownMenuGroup>
</DropdownMenuContent>
</DropdownMenu>
)}
</div>
</header> </header>
</> </>
); );

View File

@@ -0,0 +1,118 @@
import { updateReadedNotification } from '@/service/notify.api';
import { notificationQueries } from '@/service/queries';
import useNotificationStore from '@/store/useNotificationStore';
import { formatTimeAgo } from '@/utils/helper';
import { cn } from '@lib/utils';
import { m } from '@paraglide/messages';
import { BellIcon } from '@phosphor-icons/react';
import { useMutation, useQuery } from '@tanstack/react-query';
import { Link } from '@tanstack/react-router';
import { Button } from '@ui/button';
import {
DropdownMenu,
DropdownMenuContent,
DropdownMenuGroup,
DropdownMenuItem,
DropdownMenuLabel,
DropdownMenuSeparator,
DropdownMenuTrigger,
} from '@ui/dropdown-menu';
import { useEffect, useState } from 'react';
import { toast } from 'sonner';
import { Item, ItemContent, ItemDescription, ItemTitle } from './ui/item';
const Notification = () => {
const [open, _setOpen] = useState(false);
const { hasNew, setHasNew } = useNotificationStore((state) => state);
const { data } = useQuery(notificationQueries.topFive());
const { mutate: updateReaded } = useMutation({
mutationFn: () => updateReadedNotification(),
onError: (error: ReturnError) => {
const code = error.code as Parameters<
typeof m.backend_message
>[0]['code'];
toast.error(m.backend_message({ code }), {
richColors: true,
});
},
});
const onOpenNotification = (isOpen: boolean) => {
_setOpen(isOpen);
updateReaded();
};
useEffect(() => {
if (data) {
setHasNew(data.hasNewNotify);
}
}, [data]);
if (!data) return null;
return (
<DropdownMenu open={open} onOpenChange={onOpenNotification}>
<DropdownMenuTrigger asChild>
<Button
size="icon-lg"
variant="ghost"
className="relative rounded-full"
>
<BellIcon
size={32}
className={cn('origin-top', { 'animate-bell-ring': hasNew })}
/>
{hasNew && (
<span className="absolute top-1 right-1 rounded-full w-2 h-2 bg-red-600"></span>
)}
<span className="sr-only">{m.ui_label_notifications()}</span>
</Button>
</DropdownMenuTrigger>
<DropdownMenuContent className="w-sm min-w-56 rounded-lg">
<DropdownMenuLabel className="font-bold text-black">
{m.ui_label_notifications()}
</DropdownMenuLabel>
<DropdownMenuSeparator />
<DropdownMenuGroup>
{data.list && data.list.length > 0 ? (
data.list.map((notify) => {
return (
<DropdownMenuItem key={notify.id}>
<Item className="p-0">
<ItemContent>
<ItemTitle className="text-sm">
{m.templates_title_notification({
title: notify.title as Parameters<
typeof m.templates_title_notification
>[0]['title'],
})}
</ItemTitle>
<ItemDescription>
{formatTimeAgo(new Date(notify.createdAt))}
</ItemDescription>
</ItemContent>
</Item>
</DropdownMenuItem>
);
})
) : (
<DropdownMenuItem className="py-10 justify-center">
{m.common_no_notify()}
</DropdownMenuItem>
)}
</DropdownMenuGroup>
<DropdownMenuGroup>
<DropdownMenuSeparator />
<DropdownMenuItem asChild>
<Link to="/management/notifications" className="cursor-pointer">
{m.ui_view_all_notifications()}
</Link>
</DropdownMenuItem>
</DropdownMenuGroup>
</DropdownMenuContent>
</DropdownMenu>
);
};
export default Notification;

View File

@@ -38,7 +38,7 @@ const UserInviteMemberForm = ({ onSubmit }: FormProps) => {
}); });
onSubmit(false); onSubmit(false);
refetch(); refetch();
toast.success(m.houses_page_message_create_house_success(), { toast.success(m.houses_page_message_invite_member_success(), {
richColors: true, richColors: true,
}); });
}, },

View File

@@ -1,3 +1,4 @@
import useHasPermission from '@/hooks/use-has-permission';
import { cancelInvitation } from '@/service/house.api'; import { cancelInvitation } from '@/service/house.api';
import { INVITE_STATUS } from '@/types/enum'; import { INVITE_STATUS } from '@/types/enum';
import { authClient } from '@lib/auth-client'; import { authClient } from '@lib/auth-client';
@@ -23,13 +24,17 @@ type InvitationListProps = {
const CurrentUserInvitationList = ({ activeHouse }: InvitationListProps) => { const CurrentUserInvitationList = ({ activeHouse }: InvitationListProps) => {
const { refetch } = authClient.useActiveOrganization(); const { refetch } = authClient.useActiveOrganization();
const { hasPermission, isLoading } = useHasPermission(
'invitation',
'cancel',
true,
);
const { mutate: cancelInvitationMutation } = useMutation({ const { mutate: cancelInvitationMutation } = useMutation({
mutationFn: cancelInvitation, mutationFn: cancelInvitation,
onSuccess: () => { onSuccess: () => {
refetch(); refetch();
// _setOpen(false); toast.success(m.houses_page_message_cancel_invitation_success(), {
toast.success(m.houses_page_message_delete_house_success(), {
richColors: true, richColors: true,
}); });
}, },
@@ -66,7 +71,7 @@ const CurrentUserInvitationList = ({ activeHouse }: InvitationListProps) => {
<TableBody> <TableBody>
{activeHouse.invitations.length > 0 ? ( {activeHouse.invitations.length > 0 ? (
activeHouse.invitations.map((item) => ( activeHouse.invitations.map((item) => (
<TableRow> <TableRow key={item.id}>
<TableCell> <TableCell>
<Item> <Item>
<ItemContent> <ItemContent>
@@ -87,7 +92,7 @@ const CurrentUserInvitationList = ({ activeHouse }: InvitationListProps) => {
</TableCell> </TableCell>
<TableCell className="p-6"> <TableCell className="p-6">
<div className="flex justify-end gap-2"> <div className="flex justify-end gap-2">
{item.status !== INVITE_STATUS.CANCELED && ( {item.status === INVITE_STATUS.PENDING && hasPermission && (
<Button <Button
variant="outline" variant="outline"
className="cursor-pointer w-20 py-4" className="cursor-pointer w-20 py-4"

View File

@@ -18,7 +18,7 @@ type ActionProps = {};
const InviteUserAction = ({}: ActionProps) => { const InviteUserAction = ({}: ActionProps) => {
const { hasPermission, isLoading } = useHasPermission( const { hasPermission, isLoading } = useHasPermission(
'house', 'invitation',
'create', 'create',
true, true,
); );

View File

@@ -0,0 +1,59 @@
import { NOTIFICATION_TYPE } from '@/types/enum';
import { cn } from '@lib/utils';
import { m } from '@paraglide/messages';
import { Item, ItemHeader } from '@ui/item';
import { cva, VariantProps } from 'class-variance-authority';
import NotificationInvitation from './notification-type/invitation';
type NotifyProps = {
notify: NotificationWithUser;
};
const notifyVariants = cva('bg-linear-to-br shadow-xs', {
variants: {
variant: {
invitation: 'to-gray-50 from-teal-50',
system: '',
error: '',
house: '',
expired: '',
},
},
defaultVariants: {
variant: 'invitation',
},
});
const NotificationItem = ({
className,
notify,
...props
}: NotifyProps & React.ComponentProps<'div'>) => {
return (
<Item
variant="outline"
className={cn(
notifyVariants({
variant: notify.type as VariantProps<
typeof notifyVariants
>['variant'],
className,
}),
)}
{...props}
>
<ItemHeader>
{m.templates_title_notification({
title: notify.title as Parameters<
typeof m.templates_title_notification
>[0]['title'],
})}
</ItemHeader>
{notify.type === NOTIFICATION_TYPE.INVITATION && (
<NotificationInvitation notify={notify} />
)}
</Item>
);
};
export default NotificationItem;

View File

@@ -0,0 +1,110 @@
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 { useMutation, useQueryClient } from '@tanstack/react-query';
import { toast } from 'sonner';
type NotifyProps = {
notify: NotificationWithUser;
};
const NotificationInvitation = ({ notify }: NotifyProps) => {
const { house } = JSON.parse(notify.metadata || '');
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: 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 handleAgreeAction = async () => {
if (notify.link) {
acceptInvitationMutation({
data: { id: notify.link, notificationId: notify.id },
});
}
};
const handleRejectAction = () => {
if (notify.link) {
rejectInvitationMutation({
data: { id: notify.link, notificationId: notify.id },
});
}
};
return (
<>
<ItemContent>
<ItemTitle>
{`${notify.user.name} (${notify.user.email})`}{' '}
{m.templates_message_notification({
message: notify.message as Parameters<
typeof m.templates_message_notification
>[0]['message'],
name: house.name,
})}
</ItemTitle>
<ItemDescription>{formatTimeAgo(notify.createdAt)}</ItemDescription>
</ItemContent>
{notify.link && (
<ItemActions>
<Button onClick={() => handleAgreeAction()}>
{m.ui_agree_btn()}
</Button>
<Button variant="outline" onClick={() => handleRejectAction()}>
{m.ui_reject_btn()}
</Button>
</ItemActions>
)}
</>
);
};
export default NotificationInvitation;

View File

@@ -25,7 +25,7 @@ const AddNewUserButton = () => {
return ( return (
<Dialog open={_open} onOpenChange={_setOpen}> <Dialog open={_open} onOpenChange={_setOpen}>
<DialogTrigger asChild> <DialogTrigger asChild>
<Button type="button" variant="default"> <Button type="button" variant="default" className="cursor-pointer">
<PlusIcon /> <PlusIcon />
{m.nav_add_new()} {m.nav_add_new()}
</Button> </Button>

View File

@@ -62,3 +62,8 @@ export type Setting = Prisma.SettingModel
* *
*/ */
export type Audit = Prisma.AuditModel export type Audit = Prisma.AuditModel
/**
* Model Notification
*
*/
export type Notification = Prisma.NotificationModel

View File

@@ -84,3 +84,8 @@ export type Setting = Prisma.SettingModel
* *
*/ */
export type Audit = Prisma.AuditModel export type Audit = Prisma.AuditModel
/**
* Model Notification
*
*/
export type Notification = Prisma.NotificationModel

File diff suppressed because one or more lines are too long

View File

@@ -392,7 +392,8 @@ export const ModelName = {
Member: 'Member', Member: 'Member',
Invitation: 'Invitation', Invitation: 'Invitation',
Setting: 'Setting', Setting: 'Setting',
Audit: 'Audit' Audit: 'Audit',
Notification: 'Notification'
} as const } as const
export type ModelName = (typeof ModelName)[keyof typeof ModelName] export type ModelName = (typeof ModelName)[keyof typeof ModelName]
@@ -408,7 +409,7 @@ export type TypeMap<ExtArgs extends runtime.Types.Extensions.InternalArgs = runt
omit: GlobalOmitOptions omit: GlobalOmitOptions
} }
meta: { meta: {
modelProps: "user" | "session" | "account" | "verification" | "organization" | "member" | "invitation" | "setting" | "audit" modelProps: "user" | "session" | "account" | "verification" | "organization" | "member" | "invitation" | "setting" | "audit" | "notification"
txIsolationLevel: TransactionIsolationLevel txIsolationLevel: TransactionIsolationLevel
} }
model: { model: {
@@ -1078,6 +1079,80 @@ export type TypeMap<ExtArgs extends runtime.Types.Extensions.InternalArgs = runt
} }
} }
} }
Notification: {
payload: Prisma.$NotificationPayload<ExtArgs>
fields: Prisma.NotificationFieldRefs
operations: {
findUnique: {
args: Prisma.NotificationFindUniqueArgs<ExtArgs>
result: runtime.Types.Utils.PayloadToResult<Prisma.$NotificationPayload> | null
}
findUniqueOrThrow: {
args: Prisma.NotificationFindUniqueOrThrowArgs<ExtArgs>
result: runtime.Types.Utils.PayloadToResult<Prisma.$NotificationPayload>
}
findFirst: {
args: Prisma.NotificationFindFirstArgs<ExtArgs>
result: runtime.Types.Utils.PayloadToResult<Prisma.$NotificationPayload> | null
}
findFirstOrThrow: {
args: Prisma.NotificationFindFirstOrThrowArgs<ExtArgs>
result: runtime.Types.Utils.PayloadToResult<Prisma.$NotificationPayload>
}
findMany: {
args: Prisma.NotificationFindManyArgs<ExtArgs>
result: runtime.Types.Utils.PayloadToResult<Prisma.$NotificationPayload>[]
}
create: {
args: Prisma.NotificationCreateArgs<ExtArgs>
result: runtime.Types.Utils.PayloadToResult<Prisma.$NotificationPayload>
}
createMany: {
args: Prisma.NotificationCreateManyArgs<ExtArgs>
result: BatchPayload
}
createManyAndReturn: {
args: Prisma.NotificationCreateManyAndReturnArgs<ExtArgs>
result: runtime.Types.Utils.PayloadToResult<Prisma.$NotificationPayload>[]
}
delete: {
args: Prisma.NotificationDeleteArgs<ExtArgs>
result: runtime.Types.Utils.PayloadToResult<Prisma.$NotificationPayload>
}
update: {
args: Prisma.NotificationUpdateArgs<ExtArgs>
result: runtime.Types.Utils.PayloadToResult<Prisma.$NotificationPayload>
}
deleteMany: {
args: Prisma.NotificationDeleteManyArgs<ExtArgs>
result: BatchPayload
}
updateMany: {
args: Prisma.NotificationUpdateManyArgs<ExtArgs>
result: BatchPayload
}
updateManyAndReturn: {
args: Prisma.NotificationUpdateManyAndReturnArgs<ExtArgs>
result: runtime.Types.Utils.PayloadToResult<Prisma.$NotificationPayload>[]
}
upsert: {
args: Prisma.NotificationUpsertArgs<ExtArgs>
result: runtime.Types.Utils.PayloadToResult<Prisma.$NotificationPayload>
}
aggregate: {
args: Prisma.NotificationAggregateArgs<ExtArgs>
result: runtime.Types.Utils.Optional<Prisma.AggregateNotification>
}
groupBy: {
args: Prisma.NotificationGroupByArgs<ExtArgs>
result: runtime.Types.Utils.Optional<Prisma.NotificationGroupByOutputType>[]
}
count: {
args: Prisma.NotificationCountArgs<ExtArgs>
result: runtime.Types.Utils.Optional<Prisma.NotificationCountAggregateOutputType> | number
}
}
}
} }
} & { } & {
other: { other: {
@@ -1246,6 +1321,21 @@ export const AuditScalarFieldEnum = {
export type AuditScalarFieldEnum = (typeof AuditScalarFieldEnum)[keyof typeof AuditScalarFieldEnum] export type AuditScalarFieldEnum = (typeof AuditScalarFieldEnum)[keyof typeof AuditScalarFieldEnum]
export const NotificationScalarFieldEnum = {
id: 'id',
userId: 'userId',
title: 'title',
message: 'message',
type: 'type',
link: 'link',
metadata: 'metadata',
createdAt: 'createdAt',
readAt: 'readAt'
} as const
export type NotificationScalarFieldEnum = (typeof NotificationScalarFieldEnum)[keyof typeof NotificationScalarFieldEnum]
export const SortOrder = { export const SortOrder = {
asc: 'asc', asc: 'asc',
desc: 'desc' desc: 'desc'
@@ -1428,6 +1518,7 @@ export type GlobalOmitConfig = {
invitation?: Prisma.InvitationOmit invitation?: Prisma.InvitationOmit
setting?: Prisma.SettingOmit setting?: Prisma.SettingOmit
audit?: Prisma.AuditOmit audit?: Prisma.AuditOmit
notification?: Prisma.NotificationOmit
} }
/* Types for Logging */ /* Types for Logging */

View File

@@ -59,7 +59,8 @@ export const ModelName = {
Member: 'Member', Member: 'Member',
Invitation: 'Invitation', Invitation: 'Invitation',
Setting: 'Setting', Setting: 'Setting',
Audit: 'Audit' Audit: 'Audit',
Notification: 'Notification'
} as const } as const
export type ModelName = (typeof ModelName)[keyof typeof ModelName] export type ModelName = (typeof ModelName)[keyof typeof ModelName]
@@ -207,6 +208,21 @@ export const AuditScalarFieldEnum = {
export type AuditScalarFieldEnum = (typeof AuditScalarFieldEnum)[keyof typeof AuditScalarFieldEnum] export type AuditScalarFieldEnum = (typeof AuditScalarFieldEnum)[keyof typeof AuditScalarFieldEnum]
export const NotificationScalarFieldEnum = {
id: 'id',
userId: 'userId',
title: 'title',
message: 'message',
type: 'type',
link: 'link',
metadata: 'metadata',
createdAt: 'createdAt',
readAt: 'readAt'
} as const
export type NotificationScalarFieldEnum = (typeof NotificationScalarFieldEnum)[keyof typeof NotificationScalarFieldEnum]
export const SortOrder = { export const SortOrder = {
asc: 'asc', asc: 'asc',
desc: 'desc' desc: 'desc'

View File

@@ -17,4 +17,5 @@ export type * from './models/Member.ts'
export type * from './models/Invitation.ts' export type * from './models/Invitation.ts'
export type * from './models/Setting.ts' export type * from './models/Setting.ts'
export type * from './models/Audit.ts' export type * from './models/Audit.ts'
export type * from './models/Notification.ts'
export type * from './commonInputTypes.ts' export type * from './commonInputTypes.ts'

File diff suppressed because it is too large Load Diff

View File

@@ -233,6 +233,7 @@ export type UserWhereInput = {
sessions?: Prisma.SessionListRelationFilter sessions?: Prisma.SessionListRelationFilter
accounts?: Prisma.AccountListRelationFilter accounts?: Prisma.AccountListRelationFilter
audit?: Prisma.AuditListRelationFilter audit?: Prisma.AuditListRelationFilter
notification?: Prisma.NotificationListRelationFilter
members?: Prisma.MemberListRelationFilter members?: Prisma.MemberListRelationFilter
invitations?: Prisma.InvitationListRelationFilter invitations?: Prisma.InvitationListRelationFilter
} }
@@ -252,6 +253,7 @@ export type UserOrderByWithRelationInput = {
sessions?: Prisma.SessionOrderByRelationAggregateInput sessions?: Prisma.SessionOrderByRelationAggregateInput
accounts?: Prisma.AccountOrderByRelationAggregateInput accounts?: Prisma.AccountOrderByRelationAggregateInput
audit?: Prisma.AuditOrderByRelationAggregateInput audit?: Prisma.AuditOrderByRelationAggregateInput
notification?: Prisma.NotificationOrderByRelationAggregateInput
members?: Prisma.MemberOrderByRelationAggregateInput members?: Prisma.MemberOrderByRelationAggregateInput
invitations?: Prisma.InvitationOrderByRelationAggregateInput invitations?: Prisma.InvitationOrderByRelationAggregateInput
} }
@@ -274,6 +276,7 @@ export type UserWhereUniqueInput = Prisma.AtLeast<{
sessions?: Prisma.SessionListRelationFilter sessions?: Prisma.SessionListRelationFilter
accounts?: Prisma.AccountListRelationFilter accounts?: Prisma.AccountListRelationFilter
audit?: Prisma.AuditListRelationFilter audit?: Prisma.AuditListRelationFilter
notification?: Prisma.NotificationListRelationFilter
members?: Prisma.MemberListRelationFilter members?: Prisma.MemberListRelationFilter
invitations?: Prisma.InvitationListRelationFilter invitations?: Prisma.InvitationListRelationFilter
}, "id" | "email"> }, "id" | "email">
@@ -327,6 +330,7 @@ export type UserCreateInput = {
sessions?: Prisma.SessionCreateNestedManyWithoutUserInput sessions?: Prisma.SessionCreateNestedManyWithoutUserInput
accounts?: Prisma.AccountCreateNestedManyWithoutUserInput accounts?: Prisma.AccountCreateNestedManyWithoutUserInput
audit?: Prisma.AuditCreateNestedManyWithoutUserInput audit?: Prisma.AuditCreateNestedManyWithoutUserInput
notification?: Prisma.NotificationCreateNestedManyWithoutUserInput
members?: Prisma.MemberCreateNestedManyWithoutUserInput members?: Prisma.MemberCreateNestedManyWithoutUserInput
invitations?: Prisma.InvitationCreateNestedManyWithoutUserInput invitations?: Prisma.InvitationCreateNestedManyWithoutUserInput
} }
@@ -346,6 +350,7 @@ export type UserUncheckedCreateInput = {
sessions?: Prisma.SessionUncheckedCreateNestedManyWithoutUserInput sessions?: Prisma.SessionUncheckedCreateNestedManyWithoutUserInput
accounts?: Prisma.AccountUncheckedCreateNestedManyWithoutUserInput accounts?: Prisma.AccountUncheckedCreateNestedManyWithoutUserInput
audit?: Prisma.AuditUncheckedCreateNestedManyWithoutUserInput audit?: Prisma.AuditUncheckedCreateNestedManyWithoutUserInput
notification?: Prisma.NotificationUncheckedCreateNestedManyWithoutUserInput
members?: Prisma.MemberUncheckedCreateNestedManyWithoutUserInput members?: Prisma.MemberUncheckedCreateNestedManyWithoutUserInput
invitations?: Prisma.InvitationUncheckedCreateNestedManyWithoutUserInput invitations?: Prisma.InvitationUncheckedCreateNestedManyWithoutUserInput
} }
@@ -365,6 +370,7 @@ export type UserUpdateInput = {
sessions?: Prisma.SessionUpdateManyWithoutUserNestedInput sessions?: Prisma.SessionUpdateManyWithoutUserNestedInput
accounts?: Prisma.AccountUpdateManyWithoutUserNestedInput accounts?: Prisma.AccountUpdateManyWithoutUserNestedInput
audit?: Prisma.AuditUpdateManyWithoutUserNestedInput audit?: Prisma.AuditUpdateManyWithoutUserNestedInput
notification?: Prisma.NotificationUpdateManyWithoutUserNestedInput
members?: Prisma.MemberUpdateManyWithoutUserNestedInput members?: Prisma.MemberUpdateManyWithoutUserNestedInput
invitations?: Prisma.InvitationUpdateManyWithoutUserNestedInput invitations?: Prisma.InvitationUpdateManyWithoutUserNestedInput
} }
@@ -384,6 +390,7 @@ export type UserUncheckedUpdateInput = {
sessions?: Prisma.SessionUncheckedUpdateManyWithoutUserNestedInput sessions?: Prisma.SessionUncheckedUpdateManyWithoutUserNestedInput
accounts?: Prisma.AccountUncheckedUpdateManyWithoutUserNestedInput accounts?: Prisma.AccountUncheckedUpdateManyWithoutUserNestedInput
audit?: Prisma.AuditUncheckedUpdateManyWithoutUserNestedInput audit?: Prisma.AuditUncheckedUpdateManyWithoutUserNestedInput
notification?: Prisma.NotificationUncheckedUpdateManyWithoutUserNestedInput
members?: Prisma.MemberUncheckedUpdateManyWithoutUserNestedInput members?: Prisma.MemberUncheckedUpdateManyWithoutUserNestedInput
invitations?: Prisma.InvitationUncheckedUpdateManyWithoutUserNestedInput invitations?: Prisma.InvitationUncheckedUpdateManyWithoutUserNestedInput
} }
@@ -571,6 +578,20 @@ export type UserUpdateOneRequiredWithoutAuditNestedInput = {
update?: Prisma.XOR<Prisma.XOR<Prisma.UserUpdateToOneWithWhereWithoutAuditInput, Prisma.UserUpdateWithoutAuditInput>, Prisma.UserUncheckedUpdateWithoutAuditInput> update?: Prisma.XOR<Prisma.XOR<Prisma.UserUpdateToOneWithWhereWithoutAuditInput, Prisma.UserUpdateWithoutAuditInput>, Prisma.UserUncheckedUpdateWithoutAuditInput>
} }
export type UserCreateNestedOneWithoutNotificationInput = {
create?: Prisma.XOR<Prisma.UserCreateWithoutNotificationInput, Prisma.UserUncheckedCreateWithoutNotificationInput>
connectOrCreate?: Prisma.UserCreateOrConnectWithoutNotificationInput
connect?: Prisma.UserWhereUniqueInput
}
export type UserUpdateOneRequiredWithoutNotificationNestedInput = {
create?: Prisma.XOR<Prisma.UserCreateWithoutNotificationInput, Prisma.UserUncheckedCreateWithoutNotificationInput>
connectOrCreate?: Prisma.UserCreateOrConnectWithoutNotificationInput
upsert?: Prisma.UserUpsertWithoutNotificationInput
connect?: Prisma.UserWhereUniqueInput
update?: Prisma.XOR<Prisma.XOR<Prisma.UserUpdateToOneWithWhereWithoutNotificationInput, Prisma.UserUpdateWithoutNotificationInput>, Prisma.UserUncheckedUpdateWithoutNotificationInput>
}
export type UserCreateWithoutSessionsInput = { export type UserCreateWithoutSessionsInput = {
id?: string id?: string
name: string name: string
@@ -585,6 +606,7 @@ export type UserCreateWithoutSessionsInput = {
banExpires?: Date | string | null banExpires?: Date | string | null
accounts?: Prisma.AccountCreateNestedManyWithoutUserInput accounts?: Prisma.AccountCreateNestedManyWithoutUserInput
audit?: Prisma.AuditCreateNestedManyWithoutUserInput audit?: Prisma.AuditCreateNestedManyWithoutUserInput
notification?: Prisma.NotificationCreateNestedManyWithoutUserInput
members?: Prisma.MemberCreateNestedManyWithoutUserInput members?: Prisma.MemberCreateNestedManyWithoutUserInput
invitations?: Prisma.InvitationCreateNestedManyWithoutUserInput invitations?: Prisma.InvitationCreateNestedManyWithoutUserInput
} }
@@ -603,6 +625,7 @@ export type UserUncheckedCreateWithoutSessionsInput = {
banExpires?: Date | string | null banExpires?: Date | string | null
accounts?: Prisma.AccountUncheckedCreateNestedManyWithoutUserInput accounts?: Prisma.AccountUncheckedCreateNestedManyWithoutUserInput
audit?: Prisma.AuditUncheckedCreateNestedManyWithoutUserInput audit?: Prisma.AuditUncheckedCreateNestedManyWithoutUserInput
notification?: Prisma.NotificationUncheckedCreateNestedManyWithoutUserInput
members?: Prisma.MemberUncheckedCreateNestedManyWithoutUserInput members?: Prisma.MemberUncheckedCreateNestedManyWithoutUserInput
invitations?: Prisma.InvitationUncheckedCreateNestedManyWithoutUserInput invitations?: Prisma.InvitationUncheckedCreateNestedManyWithoutUserInput
} }
@@ -637,6 +660,7 @@ export type UserUpdateWithoutSessionsInput = {
banExpires?: Prisma.NullableDateTimeFieldUpdateOperationsInput | Date | string | null banExpires?: Prisma.NullableDateTimeFieldUpdateOperationsInput | Date | string | null
accounts?: Prisma.AccountUpdateManyWithoutUserNestedInput accounts?: Prisma.AccountUpdateManyWithoutUserNestedInput
audit?: Prisma.AuditUpdateManyWithoutUserNestedInput audit?: Prisma.AuditUpdateManyWithoutUserNestedInput
notification?: Prisma.NotificationUpdateManyWithoutUserNestedInput
members?: Prisma.MemberUpdateManyWithoutUserNestedInput members?: Prisma.MemberUpdateManyWithoutUserNestedInput
invitations?: Prisma.InvitationUpdateManyWithoutUserNestedInput invitations?: Prisma.InvitationUpdateManyWithoutUserNestedInput
} }
@@ -655,6 +679,7 @@ export type UserUncheckedUpdateWithoutSessionsInput = {
banExpires?: Prisma.NullableDateTimeFieldUpdateOperationsInput | Date | string | null banExpires?: Prisma.NullableDateTimeFieldUpdateOperationsInput | Date | string | null
accounts?: Prisma.AccountUncheckedUpdateManyWithoutUserNestedInput accounts?: Prisma.AccountUncheckedUpdateManyWithoutUserNestedInput
audit?: Prisma.AuditUncheckedUpdateManyWithoutUserNestedInput audit?: Prisma.AuditUncheckedUpdateManyWithoutUserNestedInput
notification?: Prisma.NotificationUncheckedUpdateManyWithoutUserNestedInput
members?: Prisma.MemberUncheckedUpdateManyWithoutUserNestedInput members?: Prisma.MemberUncheckedUpdateManyWithoutUserNestedInput
invitations?: Prisma.InvitationUncheckedUpdateManyWithoutUserNestedInput invitations?: Prisma.InvitationUncheckedUpdateManyWithoutUserNestedInput
} }
@@ -673,6 +698,7 @@ export type UserCreateWithoutAccountsInput = {
banExpires?: Date | string | null banExpires?: Date | string | null
sessions?: Prisma.SessionCreateNestedManyWithoutUserInput sessions?: Prisma.SessionCreateNestedManyWithoutUserInput
audit?: Prisma.AuditCreateNestedManyWithoutUserInput audit?: Prisma.AuditCreateNestedManyWithoutUserInput
notification?: Prisma.NotificationCreateNestedManyWithoutUserInput
members?: Prisma.MemberCreateNestedManyWithoutUserInput members?: Prisma.MemberCreateNestedManyWithoutUserInput
invitations?: Prisma.InvitationCreateNestedManyWithoutUserInput invitations?: Prisma.InvitationCreateNestedManyWithoutUserInput
} }
@@ -691,6 +717,7 @@ export type UserUncheckedCreateWithoutAccountsInput = {
banExpires?: Date | string | null banExpires?: Date | string | null
sessions?: Prisma.SessionUncheckedCreateNestedManyWithoutUserInput sessions?: Prisma.SessionUncheckedCreateNestedManyWithoutUserInput
audit?: Prisma.AuditUncheckedCreateNestedManyWithoutUserInput audit?: Prisma.AuditUncheckedCreateNestedManyWithoutUserInput
notification?: Prisma.NotificationUncheckedCreateNestedManyWithoutUserInput
members?: Prisma.MemberUncheckedCreateNestedManyWithoutUserInput members?: Prisma.MemberUncheckedCreateNestedManyWithoutUserInput
invitations?: Prisma.InvitationUncheckedCreateNestedManyWithoutUserInput invitations?: Prisma.InvitationUncheckedCreateNestedManyWithoutUserInput
} }
@@ -725,6 +752,7 @@ export type UserUpdateWithoutAccountsInput = {
banExpires?: Prisma.NullableDateTimeFieldUpdateOperationsInput | Date | string | null banExpires?: Prisma.NullableDateTimeFieldUpdateOperationsInput | Date | string | null
sessions?: Prisma.SessionUpdateManyWithoutUserNestedInput sessions?: Prisma.SessionUpdateManyWithoutUserNestedInput
audit?: Prisma.AuditUpdateManyWithoutUserNestedInput audit?: Prisma.AuditUpdateManyWithoutUserNestedInput
notification?: Prisma.NotificationUpdateManyWithoutUserNestedInput
members?: Prisma.MemberUpdateManyWithoutUserNestedInput members?: Prisma.MemberUpdateManyWithoutUserNestedInput
invitations?: Prisma.InvitationUpdateManyWithoutUserNestedInput invitations?: Prisma.InvitationUpdateManyWithoutUserNestedInput
} }
@@ -743,6 +771,7 @@ export type UserUncheckedUpdateWithoutAccountsInput = {
banExpires?: Prisma.NullableDateTimeFieldUpdateOperationsInput | Date | string | null banExpires?: Prisma.NullableDateTimeFieldUpdateOperationsInput | Date | string | null
sessions?: Prisma.SessionUncheckedUpdateManyWithoutUserNestedInput sessions?: Prisma.SessionUncheckedUpdateManyWithoutUserNestedInput
audit?: Prisma.AuditUncheckedUpdateManyWithoutUserNestedInput audit?: Prisma.AuditUncheckedUpdateManyWithoutUserNestedInput
notification?: Prisma.NotificationUncheckedUpdateManyWithoutUserNestedInput
members?: Prisma.MemberUncheckedUpdateManyWithoutUserNestedInput members?: Prisma.MemberUncheckedUpdateManyWithoutUserNestedInput
invitations?: Prisma.InvitationUncheckedUpdateManyWithoutUserNestedInput invitations?: Prisma.InvitationUncheckedUpdateManyWithoutUserNestedInput
} }
@@ -762,6 +791,7 @@ export type UserCreateWithoutMembersInput = {
sessions?: Prisma.SessionCreateNestedManyWithoutUserInput sessions?: Prisma.SessionCreateNestedManyWithoutUserInput
accounts?: Prisma.AccountCreateNestedManyWithoutUserInput accounts?: Prisma.AccountCreateNestedManyWithoutUserInput
audit?: Prisma.AuditCreateNestedManyWithoutUserInput audit?: Prisma.AuditCreateNestedManyWithoutUserInput
notification?: Prisma.NotificationCreateNestedManyWithoutUserInput
invitations?: Prisma.InvitationCreateNestedManyWithoutUserInput invitations?: Prisma.InvitationCreateNestedManyWithoutUserInput
} }
@@ -780,6 +810,7 @@ export type UserUncheckedCreateWithoutMembersInput = {
sessions?: Prisma.SessionUncheckedCreateNestedManyWithoutUserInput sessions?: Prisma.SessionUncheckedCreateNestedManyWithoutUserInput
accounts?: Prisma.AccountUncheckedCreateNestedManyWithoutUserInput accounts?: Prisma.AccountUncheckedCreateNestedManyWithoutUserInput
audit?: Prisma.AuditUncheckedCreateNestedManyWithoutUserInput audit?: Prisma.AuditUncheckedCreateNestedManyWithoutUserInput
notification?: Prisma.NotificationUncheckedCreateNestedManyWithoutUserInput
invitations?: Prisma.InvitationUncheckedCreateNestedManyWithoutUserInput invitations?: Prisma.InvitationUncheckedCreateNestedManyWithoutUserInput
} }
@@ -814,6 +845,7 @@ export type UserUpdateWithoutMembersInput = {
sessions?: Prisma.SessionUpdateManyWithoutUserNestedInput sessions?: Prisma.SessionUpdateManyWithoutUserNestedInput
accounts?: Prisma.AccountUpdateManyWithoutUserNestedInput accounts?: Prisma.AccountUpdateManyWithoutUserNestedInput
audit?: Prisma.AuditUpdateManyWithoutUserNestedInput audit?: Prisma.AuditUpdateManyWithoutUserNestedInput
notification?: Prisma.NotificationUpdateManyWithoutUserNestedInput
invitations?: Prisma.InvitationUpdateManyWithoutUserNestedInput invitations?: Prisma.InvitationUpdateManyWithoutUserNestedInput
} }
@@ -832,6 +864,7 @@ export type UserUncheckedUpdateWithoutMembersInput = {
sessions?: Prisma.SessionUncheckedUpdateManyWithoutUserNestedInput sessions?: Prisma.SessionUncheckedUpdateManyWithoutUserNestedInput
accounts?: Prisma.AccountUncheckedUpdateManyWithoutUserNestedInput accounts?: Prisma.AccountUncheckedUpdateManyWithoutUserNestedInput
audit?: Prisma.AuditUncheckedUpdateManyWithoutUserNestedInput audit?: Prisma.AuditUncheckedUpdateManyWithoutUserNestedInput
notification?: Prisma.NotificationUncheckedUpdateManyWithoutUserNestedInput
invitations?: Prisma.InvitationUncheckedUpdateManyWithoutUserNestedInput invitations?: Prisma.InvitationUncheckedUpdateManyWithoutUserNestedInput
} }
@@ -850,6 +883,7 @@ export type UserCreateWithoutInvitationsInput = {
sessions?: Prisma.SessionCreateNestedManyWithoutUserInput sessions?: Prisma.SessionCreateNestedManyWithoutUserInput
accounts?: Prisma.AccountCreateNestedManyWithoutUserInput accounts?: Prisma.AccountCreateNestedManyWithoutUserInput
audit?: Prisma.AuditCreateNestedManyWithoutUserInput audit?: Prisma.AuditCreateNestedManyWithoutUserInput
notification?: Prisma.NotificationCreateNestedManyWithoutUserInput
members?: Prisma.MemberCreateNestedManyWithoutUserInput members?: Prisma.MemberCreateNestedManyWithoutUserInput
} }
@@ -868,6 +902,7 @@ export type UserUncheckedCreateWithoutInvitationsInput = {
sessions?: Prisma.SessionUncheckedCreateNestedManyWithoutUserInput sessions?: Prisma.SessionUncheckedCreateNestedManyWithoutUserInput
accounts?: Prisma.AccountUncheckedCreateNestedManyWithoutUserInput accounts?: Prisma.AccountUncheckedCreateNestedManyWithoutUserInput
audit?: Prisma.AuditUncheckedCreateNestedManyWithoutUserInput audit?: Prisma.AuditUncheckedCreateNestedManyWithoutUserInput
notification?: Prisma.NotificationUncheckedCreateNestedManyWithoutUserInput
members?: Prisma.MemberUncheckedCreateNestedManyWithoutUserInput members?: Prisma.MemberUncheckedCreateNestedManyWithoutUserInput
} }
@@ -902,6 +937,7 @@ export type UserUpdateWithoutInvitationsInput = {
sessions?: Prisma.SessionUpdateManyWithoutUserNestedInput sessions?: Prisma.SessionUpdateManyWithoutUserNestedInput
accounts?: Prisma.AccountUpdateManyWithoutUserNestedInput accounts?: Prisma.AccountUpdateManyWithoutUserNestedInput
audit?: Prisma.AuditUpdateManyWithoutUserNestedInput audit?: Prisma.AuditUpdateManyWithoutUserNestedInput
notification?: Prisma.NotificationUpdateManyWithoutUserNestedInput
members?: Prisma.MemberUpdateManyWithoutUserNestedInput members?: Prisma.MemberUpdateManyWithoutUserNestedInput
} }
@@ -920,6 +956,7 @@ export type UserUncheckedUpdateWithoutInvitationsInput = {
sessions?: Prisma.SessionUncheckedUpdateManyWithoutUserNestedInput sessions?: Prisma.SessionUncheckedUpdateManyWithoutUserNestedInput
accounts?: Prisma.AccountUncheckedUpdateManyWithoutUserNestedInput accounts?: Prisma.AccountUncheckedUpdateManyWithoutUserNestedInput
audit?: Prisma.AuditUncheckedUpdateManyWithoutUserNestedInput audit?: Prisma.AuditUncheckedUpdateManyWithoutUserNestedInput
notification?: Prisma.NotificationUncheckedUpdateManyWithoutUserNestedInput
members?: Prisma.MemberUncheckedUpdateManyWithoutUserNestedInput members?: Prisma.MemberUncheckedUpdateManyWithoutUserNestedInput
} }
@@ -937,6 +974,7 @@ export type UserCreateWithoutAuditInput = {
banExpires?: Date | string | null banExpires?: Date | string | null
sessions?: Prisma.SessionCreateNestedManyWithoutUserInput sessions?: Prisma.SessionCreateNestedManyWithoutUserInput
accounts?: Prisma.AccountCreateNestedManyWithoutUserInput accounts?: Prisma.AccountCreateNestedManyWithoutUserInput
notification?: Prisma.NotificationCreateNestedManyWithoutUserInput
members?: Prisma.MemberCreateNestedManyWithoutUserInput members?: Prisma.MemberCreateNestedManyWithoutUserInput
invitations?: Prisma.InvitationCreateNestedManyWithoutUserInput invitations?: Prisma.InvitationCreateNestedManyWithoutUserInput
} }
@@ -955,6 +993,7 @@ export type UserUncheckedCreateWithoutAuditInput = {
banExpires?: Date | string | null banExpires?: Date | string | null
sessions?: Prisma.SessionUncheckedCreateNestedManyWithoutUserInput sessions?: Prisma.SessionUncheckedCreateNestedManyWithoutUserInput
accounts?: Prisma.AccountUncheckedCreateNestedManyWithoutUserInput accounts?: Prisma.AccountUncheckedCreateNestedManyWithoutUserInput
notification?: Prisma.NotificationUncheckedCreateNestedManyWithoutUserInput
members?: Prisma.MemberUncheckedCreateNestedManyWithoutUserInput members?: Prisma.MemberUncheckedCreateNestedManyWithoutUserInput
invitations?: Prisma.InvitationUncheckedCreateNestedManyWithoutUserInput invitations?: Prisma.InvitationUncheckedCreateNestedManyWithoutUserInput
} }
@@ -989,6 +1028,7 @@ export type UserUpdateWithoutAuditInput = {
banExpires?: Prisma.NullableDateTimeFieldUpdateOperationsInput | Date | string | null banExpires?: Prisma.NullableDateTimeFieldUpdateOperationsInput | Date | string | null
sessions?: Prisma.SessionUpdateManyWithoutUserNestedInput sessions?: Prisma.SessionUpdateManyWithoutUserNestedInput
accounts?: Prisma.AccountUpdateManyWithoutUserNestedInput accounts?: Prisma.AccountUpdateManyWithoutUserNestedInput
notification?: Prisma.NotificationUpdateManyWithoutUserNestedInput
members?: Prisma.MemberUpdateManyWithoutUserNestedInput members?: Prisma.MemberUpdateManyWithoutUserNestedInput
invitations?: Prisma.InvitationUpdateManyWithoutUserNestedInput invitations?: Prisma.InvitationUpdateManyWithoutUserNestedInput
} }
@@ -1007,6 +1047,99 @@ export type UserUncheckedUpdateWithoutAuditInput = {
banExpires?: Prisma.NullableDateTimeFieldUpdateOperationsInput | Date | string | null banExpires?: Prisma.NullableDateTimeFieldUpdateOperationsInput | Date | string | null
sessions?: Prisma.SessionUncheckedUpdateManyWithoutUserNestedInput sessions?: Prisma.SessionUncheckedUpdateManyWithoutUserNestedInput
accounts?: Prisma.AccountUncheckedUpdateManyWithoutUserNestedInput accounts?: Prisma.AccountUncheckedUpdateManyWithoutUserNestedInput
notification?: Prisma.NotificationUncheckedUpdateManyWithoutUserNestedInput
members?: Prisma.MemberUncheckedUpdateManyWithoutUserNestedInput
invitations?: Prisma.InvitationUncheckedUpdateManyWithoutUserNestedInput
}
export type UserCreateWithoutNotificationInput = {
id?: string
name: string
email: string
emailVerified?: boolean
image?: string | null
createdAt?: Date | string
updatedAt?: Date | string
role?: string | null
banned?: boolean | null
banReason?: string | null
banExpires?: Date | string | null
sessions?: Prisma.SessionCreateNestedManyWithoutUserInput
accounts?: Prisma.AccountCreateNestedManyWithoutUserInput
audit?: Prisma.AuditCreateNestedManyWithoutUserInput
members?: Prisma.MemberCreateNestedManyWithoutUserInput
invitations?: Prisma.InvitationCreateNestedManyWithoutUserInput
}
export type UserUncheckedCreateWithoutNotificationInput = {
id?: string
name: string
email: string
emailVerified?: boolean
image?: string | null
createdAt?: Date | string
updatedAt?: Date | string
role?: string | null
banned?: boolean | null
banReason?: string | null
banExpires?: Date | string | null
sessions?: Prisma.SessionUncheckedCreateNestedManyWithoutUserInput
accounts?: Prisma.AccountUncheckedCreateNestedManyWithoutUserInput
audit?: Prisma.AuditUncheckedCreateNestedManyWithoutUserInput
members?: Prisma.MemberUncheckedCreateNestedManyWithoutUserInput
invitations?: Prisma.InvitationUncheckedCreateNestedManyWithoutUserInput
}
export type UserCreateOrConnectWithoutNotificationInput = {
where: Prisma.UserWhereUniqueInput
create: Prisma.XOR<Prisma.UserCreateWithoutNotificationInput, Prisma.UserUncheckedCreateWithoutNotificationInput>
}
export type UserUpsertWithoutNotificationInput = {
update: Prisma.XOR<Prisma.UserUpdateWithoutNotificationInput, Prisma.UserUncheckedUpdateWithoutNotificationInput>
create: Prisma.XOR<Prisma.UserCreateWithoutNotificationInput, Prisma.UserUncheckedCreateWithoutNotificationInput>
where?: Prisma.UserWhereInput
}
export type UserUpdateToOneWithWhereWithoutNotificationInput = {
where?: Prisma.UserWhereInput
data: Prisma.XOR<Prisma.UserUpdateWithoutNotificationInput, Prisma.UserUncheckedUpdateWithoutNotificationInput>
}
export type UserUpdateWithoutNotificationInput = {
id?: Prisma.StringFieldUpdateOperationsInput | string
name?: Prisma.StringFieldUpdateOperationsInput | string
email?: Prisma.StringFieldUpdateOperationsInput | string
emailVerified?: Prisma.BoolFieldUpdateOperationsInput | boolean
image?: Prisma.NullableStringFieldUpdateOperationsInput | string | null
createdAt?: Prisma.DateTimeFieldUpdateOperationsInput | Date | string
updatedAt?: Prisma.DateTimeFieldUpdateOperationsInput | Date | string
role?: Prisma.NullableStringFieldUpdateOperationsInput | string | null
banned?: Prisma.NullableBoolFieldUpdateOperationsInput | boolean | null
banReason?: Prisma.NullableStringFieldUpdateOperationsInput | string | null
banExpires?: Prisma.NullableDateTimeFieldUpdateOperationsInput | Date | string | null
sessions?: Prisma.SessionUpdateManyWithoutUserNestedInput
accounts?: Prisma.AccountUpdateManyWithoutUserNestedInput
audit?: Prisma.AuditUpdateManyWithoutUserNestedInput
members?: Prisma.MemberUpdateManyWithoutUserNestedInput
invitations?: Prisma.InvitationUpdateManyWithoutUserNestedInput
}
export type UserUncheckedUpdateWithoutNotificationInput = {
id?: Prisma.StringFieldUpdateOperationsInput | string
name?: Prisma.StringFieldUpdateOperationsInput | string
email?: Prisma.StringFieldUpdateOperationsInput | string
emailVerified?: Prisma.BoolFieldUpdateOperationsInput | boolean
image?: Prisma.NullableStringFieldUpdateOperationsInput | string | null
createdAt?: Prisma.DateTimeFieldUpdateOperationsInput | Date | string
updatedAt?: Prisma.DateTimeFieldUpdateOperationsInput | Date | string
role?: Prisma.NullableStringFieldUpdateOperationsInput | string | null
banned?: Prisma.NullableBoolFieldUpdateOperationsInput | boolean | null
banReason?: Prisma.NullableStringFieldUpdateOperationsInput | string | null
banExpires?: Prisma.NullableDateTimeFieldUpdateOperationsInput | Date | string | null
sessions?: Prisma.SessionUncheckedUpdateManyWithoutUserNestedInput
accounts?: Prisma.AccountUncheckedUpdateManyWithoutUserNestedInput
audit?: Prisma.AuditUncheckedUpdateManyWithoutUserNestedInput
members?: Prisma.MemberUncheckedUpdateManyWithoutUserNestedInput members?: Prisma.MemberUncheckedUpdateManyWithoutUserNestedInput
invitations?: Prisma.InvitationUncheckedUpdateManyWithoutUserNestedInput invitations?: Prisma.InvitationUncheckedUpdateManyWithoutUserNestedInput
} }
@@ -1020,6 +1153,7 @@ export type UserCountOutputType = {
sessions: number sessions: number
accounts: number accounts: number
audit: number audit: number
notification: number
members: number members: number
invitations: number invitations: number
} }
@@ -1028,6 +1162,7 @@ export type UserCountOutputTypeSelect<ExtArgs extends runtime.Types.Extensions.I
sessions?: boolean | UserCountOutputTypeCountSessionsArgs sessions?: boolean | UserCountOutputTypeCountSessionsArgs
accounts?: boolean | UserCountOutputTypeCountAccountsArgs accounts?: boolean | UserCountOutputTypeCountAccountsArgs
audit?: boolean | UserCountOutputTypeCountAuditArgs audit?: boolean | UserCountOutputTypeCountAuditArgs
notification?: boolean | UserCountOutputTypeCountNotificationArgs
members?: boolean | UserCountOutputTypeCountMembersArgs members?: boolean | UserCountOutputTypeCountMembersArgs
invitations?: boolean | UserCountOutputTypeCountInvitationsArgs invitations?: boolean | UserCountOutputTypeCountInvitationsArgs
} }
@@ -1063,6 +1198,13 @@ export type UserCountOutputTypeCountAuditArgs<ExtArgs extends runtime.Types.Exte
where?: Prisma.AuditWhereInput where?: Prisma.AuditWhereInput
} }
/**
* UserCountOutputType without action
*/
export type UserCountOutputTypeCountNotificationArgs<ExtArgs extends runtime.Types.Extensions.InternalArgs = runtime.Types.Extensions.DefaultArgs> = {
where?: Prisma.NotificationWhereInput
}
/** /**
* UserCountOutputType without action * UserCountOutputType without action
*/ */
@@ -1093,6 +1235,7 @@ export type UserSelect<ExtArgs extends runtime.Types.Extensions.InternalArgs = r
sessions?: boolean | Prisma.User$sessionsArgs<ExtArgs> sessions?: boolean | Prisma.User$sessionsArgs<ExtArgs>
accounts?: boolean | Prisma.User$accountsArgs<ExtArgs> accounts?: boolean | Prisma.User$accountsArgs<ExtArgs>
audit?: boolean | Prisma.User$auditArgs<ExtArgs> audit?: boolean | Prisma.User$auditArgs<ExtArgs>
notification?: boolean | Prisma.User$notificationArgs<ExtArgs>
members?: boolean | Prisma.User$membersArgs<ExtArgs> members?: boolean | Prisma.User$membersArgs<ExtArgs>
invitations?: boolean | Prisma.User$invitationsArgs<ExtArgs> invitations?: boolean | Prisma.User$invitationsArgs<ExtArgs>
_count?: boolean | Prisma.UserCountOutputTypeDefaultArgs<ExtArgs> _count?: boolean | Prisma.UserCountOutputTypeDefaultArgs<ExtArgs>
@@ -1145,6 +1288,7 @@ export type UserInclude<ExtArgs extends runtime.Types.Extensions.InternalArgs =
sessions?: boolean | Prisma.User$sessionsArgs<ExtArgs> sessions?: boolean | Prisma.User$sessionsArgs<ExtArgs>
accounts?: boolean | Prisma.User$accountsArgs<ExtArgs> accounts?: boolean | Prisma.User$accountsArgs<ExtArgs>
audit?: boolean | Prisma.User$auditArgs<ExtArgs> audit?: boolean | Prisma.User$auditArgs<ExtArgs>
notification?: boolean | Prisma.User$notificationArgs<ExtArgs>
members?: boolean | Prisma.User$membersArgs<ExtArgs> members?: boolean | Prisma.User$membersArgs<ExtArgs>
invitations?: boolean | Prisma.User$invitationsArgs<ExtArgs> invitations?: boolean | Prisma.User$invitationsArgs<ExtArgs>
_count?: boolean | Prisma.UserCountOutputTypeDefaultArgs<ExtArgs> _count?: boolean | Prisma.UserCountOutputTypeDefaultArgs<ExtArgs>
@@ -1158,6 +1302,7 @@ export type $UserPayload<ExtArgs extends runtime.Types.Extensions.InternalArgs =
sessions: Prisma.$SessionPayload<ExtArgs>[] sessions: Prisma.$SessionPayload<ExtArgs>[]
accounts: Prisma.$AccountPayload<ExtArgs>[] accounts: Prisma.$AccountPayload<ExtArgs>[]
audit: Prisma.$AuditPayload<ExtArgs>[] audit: Prisma.$AuditPayload<ExtArgs>[]
notification: Prisma.$NotificationPayload<ExtArgs>[]
members: Prisma.$MemberPayload<ExtArgs>[] members: Prisma.$MemberPayload<ExtArgs>[]
invitations: Prisma.$InvitationPayload<ExtArgs>[] invitations: Prisma.$InvitationPayload<ExtArgs>[]
} }
@@ -1570,6 +1715,7 @@ export interface Prisma__UserClient<T, Null = never, ExtArgs extends runtime.Typ
sessions<T extends Prisma.User$sessionsArgs<ExtArgs> = {}>(args?: Prisma.Subset<T, Prisma.User$sessionsArgs<ExtArgs>>): Prisma.PrismaPromise<runtime.Types.Result.GetResult<Prisma.$SessionPayload<ExtArgs>, T, "findMany", GlobalOmitOptions> | Null> sessions<T extends Prisma.User$sessionsArgs<ExtArgs> = {}>(args?: Prisma.Subset<T, Prisma.User$sessionsArgs<ExtArgs>>): Prisma.PrismaPromise<runtime.Types.Result.GetResult<Prisma.$SessionPayload<ExtArgs>, T, "findMany", GlobalOmitOptions> | Null>
accounts<T extends Prisma.User$accountsArgs<ExtArgs> = {}>(args?: Prisma.Subset<T, Prisma.User$accountsArgs<ExtArgs>>): Prisma.PrismaPromise<runtime.Types.Result.GetResult<Prisma.$AccountPayload<ExtArgs>, T, "findMany", GlobalOmitOptions> | Null> accounts<T extends Prisma.User$accountsArgs<ExtArgs> = {}>(args?: Prisma.Subset<T, Prisma.User$accountsArgs<ExtArgs>>): Prisma.PrismaPromise<runtime.Types.Result.GetResult<Prisma.$AccountPayload<ExtArgs>, T, "findMany", GlobalOmitOptions> | Null>
audit<T extends Prisma.User$auditArgs<ExtArgs> = {}>(args?: Prisma.Subset<T, Prisma.User$auditArgs<ExtArgs>>): Prisma.PrismaPromise<runtime.Types.Result.GetResult<Prisma.$AuditPayload<ExtArgs>, T, "findMany", GlobalOmitOptions> | Null> audit<T extends Prisma.User$auditArgs<ExtArgs> = {}>(args?: Prisma.Subset<T, Prisma.User$auditArgs<ExtArgs>>): Prisma.PrismaPromise<runtime.Types.Result.GetResult<Prisma.$AuditPayload<ExtArgs>, T, "findMany", GlobalOmitOptions> | Null>
notification<T extends Prisma.User$notificationArgs<ExtArgs> = {}>(args?: Prisma.Subset<T, Prisma.User$notificationArgs<ExtArgs>>): Prisma.PrismaPromise<runtime.Types.Result.GetResult<Prisma.$NotificationPayload<ExtArgs>, T, "findMany", GlobalOmitOptions> | Null>
members<T extends Prisma.User$membersArgs<ExtArgs> = {}>(args?: Prisma.Subset<T, Prisma.User$membersArgs<ExtArgs>>): Prisma.PrismaPromise<runtime.Types.Result.GetResult<Prisma.$MemberPayload<ExtArgs>, T, "findMany", GlobalOmitOptions> | Null> members<T extends Prisma.User$membersArgs<ExtArgs> = {}>(args?: Prisma.Subset<T, Prisma.User$membersArgs<ExtArgs>>): Prisma.PrismaPromise<runtime.Types.Result.GetResult<Prisma.$MemberPayload<ExtArgs>, T, "findMany", GlobalOmitOptions> | Null>
invitations<T extends Prisma.User$invitationsArgs<ExtArgs> = {}>(args?: Prisma.Subset<T, Prisma.User$invitationsArgs<ExtArgs>>): Prisma.PrismaPromise<runtime.Types.Result.GetResult<Prisma.$InvitationPayload<ExtArgs>, T, "findMany", GlobalOmitOptions> | Null> invitations<T extends Prisma.User$invitationsArgs<ExtArgs> = {}>(args?: Prisma.Subset<T, Prisma.User$invitationsArgs<ExtArgs>>): Prisma.PrismaPromise<runtime.Types.Result.GetResult<Prisma.$InvitationPayload<ExtArgs>, T, "findMany", GlobalOmitOptions> | Null>
/** /**
@@ -2071,6 +2217,30 @@ export type User$auditArgs<ExtArgs extends runtime.Types.Extensions.InternalArgs
distinct?: Prisma.AuditScalarFieldEnum | Prisma.AuditScalarFieldEnum[] distinct?: Prisma.AuditScalarFieldEnum | Prisma.AuditScalarFieldEnum[]
} }
/**
* User.notification
*/
export type User$notificationArgs<ExtArgs extends runtime.Types.Extensions.InternalArgs = runtime.Types.Extensions.DefaultArgs> = {
/**
* Select specific fields to fetch from the Notification
*/
select?: Prisma.NotificationSelect<ExtArgs> | null
/**
* Omit specific fields from the Notification
*/
omit?: Prisma.NotificationOmit<ExtArgs> | null
/**
* Choose, which related nodes to fetch as well
*/
include?: Prisma.NotificationInclude<ExtArgs> | null
where?: Prisma.NotificationWhereInput
orderBy?: Prisma.NotificationOrderByWithRelationInput | Prisma.NotificationOrderByWithRelationInput[]
cursor?: Prisma.NotificationWhereUniqueInput
take?: number
skip?: number
distinct?: Prisma.NotificationScalarFieldEnum | Prisma.NotificationScalarFieldEnum[]
}
/** /**
* User.members * User.members
*/ */

24
src/hooks/use-sse.ts Normal file
View File

@@ -0,0 +1,24 @@
import { useEffect } from 'react';
export function useSSE(onMessage: (data: any) => void) {
useEffect(() => {
const eventSource = new EventSource('/api/notify');
eventSource.onmessage = (event) => {
try {
const parsed = JSON.parse(event.data);
onMessage(parsed);
} catch (err) {
console.error('Invalid SSE data', err);
}
};
eventSource.onerror = () => {
eventSource.close();
};
return () => {
eventSource.close();
};
}, [onMessage]);
}

52
src/lib/notification.ts Normal file
View File

@@ -0,0 +1,52 @@
type Controller = ReadableStreamDefaultController;
const userClients = new Map<string, Set<Controller>>();
/**
* Thêm connection cho user
*/
export function addClient(userId: string, controller: Controller) {
if (!userClients.has(userId)) {
userClients.set(userId, new Set());
}
userClients.get(userId)!.add(controller);
}
/**
* Xoá connection khi tab đóng
*/
export function removeClient(userId: string, controller: Controller) {
const controllers = userClients.get(userId);
if (!controllers) return;
controllers.delete(controller);
// nếu user không còn tab nào mở thì xoá luôn
if (controllers.size === 0) {
userClients.delete(userId);
}
}
/**
* Gửi notification cho 1 user
*/
export function sendToUser(userId: string, data: any) {
const controllers = userClients.get(userId);
if (!controllers) return;
for (const controller of controllers) {
controller.enqueue(`data: ${JSON.stringify(data)}\n\n`);
}
}
/**
* Gửi cho tất cả user
*/
export function broadcast(data: any) {
for (const controllers of userClients.values()) {
for (const controller of controllers) {
controller.enqueue(`data: ${JSON.stringify(data)}\n\n`);
}
}
}

View File

@@ -11,6 +11,7 @@
import { Route as rootRouteImport } from './routes/__root' import { Route as rootRouteImport } from './routes/__root'
import { Route as appRouteRouteImport } from './routes/(app)/route' import { Route as appRouteRouteImport } from './routes/(app)/route'
import { Route as appIndexRouteImport } from './routes/(app)/index' import { Route as appIndexRouteImport } from './routes/(app)/index'
import { Route as ApiNotifyRouteImport } from './routes/api/notify'
import { Route as authSignInRouteImport } from './routes/(auth)/sign-in' import { Route as authSignInRouteImport } from './routes/(auth)/sign-in'
import { Route as appauthRouteRouteImport } from './routes/(app)/(auth)/route' import { Route as appauthRouteRouteImport } from './routes/(app)/(auth)/route'
import { Route as ApiAuthSplatRouteImport } from './routes/api.auth.$' import { Route as ApiAuthSplatRouteImport } from './routes/api.auth.$'
@@ -20,6 +21,7 @@ import { Route as appauthAccountRouteRouteImport } from './routes/(app)/(auth)/a
import { Route as appauthManagementIndexRouteImport } from './routes/(app)/(auth)/management/index' import { Route as appauthManagementIndexRouteImport } from './routes/(app)/(auth)/management/index'
import { Route as appauthKanriIndexRouteImport } from './routes/(app)/(auth)/kanri/index' import { Route as appauthKanriIndexRouteImport } from './routes/(app)/(auth)/kanri/index'
import { Route as appauthAccountIndexRouteImport } from './routes/(app)/(auth)/account/index' import { Route as appauthAccountIndexRouteImport } from './routes/(app)/(auth)/account/index'
import { Route as appauthManagementNotificationsRouteImport } from './routes/(app)/(auth)/management/notifications'
import { Route as appauthManagementHousesRouteImport } from './routes/(app)/(auth)/management/houses' import { Route as appauthManagementHousesRouteImport } from './routes/(app)/(auth)/management/houses'
import { Route as appauthManagementDashboardRouteImport } from './routes/(app)/(auth)/management/dashboard' import { Route as appauthManagementDashboardRouteImport } from './routes/(app)/(auth)/management/dashboard'
import { Route as appauthKanriUsersRouteImport } from './routes/(app)/(auth)/kanri/users' import { Route as appauthKanriUsersRouteImport } from './routes/(app)/(auth)/kanri/users'
@@ -39,6 +41,11 @@ const appIndexRoute = appIndexRouteImport.update({
path: '/', path: '/',
getParentRoute: () => appRouteRoute, getParentRoute: () => appRouteRoute,
} as any) } as any)
const ApiNotifyRoute = ApiNotifyRouteImport.update({
id: '/api/notify',
path: '/api/notify',
getParentRoute: () => rootRouteImport,
} as any)
const authSignInRoute = authSignInRouteImport.update({ const authSignInRoute = authSignInRouteImport.update({
id: '/(auth)/sign-in', id: '/(auth)/sign-in',
path: '/sign-in', path: '/sign-in',
@@ -83,6 +90,12 @@ const appauthAccountIndexRoute = appauthAccountIndexRouteImport.update({
path: '/', path: '/',
getParentRoute: () => appauthAccountRouteRoute, getParentRoute: () => appauthAccountRouteRoute,
} as any) } as any)
const appauthManagementNotificationsRoute =
appauthManagementNotificationsRouteImport.update({
id: '/notifications',
path: '/notifications',
getParentRoute: () => appauthManagementRouteRoute,
} as any)
const appauthManagementHousesRoute = appauthManagementHousesRouteImport.update({ const appauthManagementHousesRoute = appauthManagementHousesRouteImport.update({
id: '/houses', id: '/houses',
path: '/houses', path: '/houses',
@@ -133,6 +146,7 @@ const appauthAccountChangePasswordRoute =
export interface FileRoutesByFullPath { export interface FileRoutesByFullPath {
'/sign-in': typeof authSignInRoute '/sign-in': typeof authSignInRoute
'/api/notify': typeof ApiNotifyRoute
'/': typeof appIndexRoute '/': typeof appIndexRoute
'/account': typeof appauthAccountRouteRouteWithChildren '/account': typeof appauthAccountRouteRouteWithChildren
'/kanri': typeof appauthKanriRouteRouteWithChildren '/kanri': typeof appauthKanriRouteRouteWithChildren
@@ -147,12 +161,14 @@ export interface FileRoutesByFullPath {
'/kanri/users': typeof appauthKanriUsersRoute '/kanri/users': typeof appauthKanriUsersRoute
'/management/dashboard': typeof appauthManagementDashboardRoute '/management/dashboard': typeof appauthManagementDashboardRoute
'/management/houses': typeof appauthManagementHousesRoute '/management/houses': typeof appauthManagementHousesRoute
'/management/notifications': typeof appauthManagementNotificationsRoute
'/account/': typeof appauthAccountIndexRoute '/account/': typeof appauthAccountIndexRoute
'/kanri/': typeof appauthKanriIndexRoute '/kanri/': typeof appauthKanriIndexRoute
'/management/': typeof appauthManagementIndexRoute '/management/': typeof appauthManagementIndexRoute
} }
export interface FileRoutesByTo { export interface FileRoutesByTo {
'/sign-in': typeof authSignInRoute '/sign-in': typeof authSignInRoute
'/api/notify': typeof ApiNotifyRoute
'/': typeof appIndexRoute '/': typeof appIndexRoute
'/api/auth/$': typeof ApiAuthSplatRoute '/api/auth/$': typeof ApiAuthSplatRoute
'/account/change-password': typeof appauthAccountChangePasswordRoute '/account/change-password': typeof appauthAccountChangePasswordRoute
@@ -164,6 +180,7 @@ export interface FileRoutesByTo {
'/kanri/users': typeof appauthKanriUsersRoute '/kanri/users': typeof appauthKanriUsersRoute
'/management/dashboard': typeof appauthManagementDashboardRoute '/management/dashboard': typeof appauthManagementDashboardRoute
'/management/houses': typeof appauthManagementHousesRoute '/management/houses': typeof appauthManagementHousesRoute
'/management/notifications': typeof appauthManagementNotificationsRoute
'/account': typeof appauthAccountIndexRoute '/account': typeof appauthAccountIndexRoute
'/kanri': typeof appauthKanriIndexRoute '/kanri': typeof appauthKanriIndexRoute
'/management': typeof appauthManagementIndexRoute '/management': typeof appauthManagementIndexRoute
@@ -173,6 +190,7 @@ export interface FileRoutesById {
'/(app)': typeof appRouteRouteWithChildren '/(app)': typeof appRouteRouteWithChildren
'/(app)/(auth)': typeof appauthRouteRouteWithChildren '/(app)/(auth)': typeof appauthRouteRouteWithChildren
'/(auth)/sign-in': typeof authSignInRoute '/(auth)/sign-in': typeof authSignInRoute
'/api/notify': typeof ApiNotifyRoute
'/(app)/': typeof appIndexRoute '/(app)/': typeof appIndexRoute
'/(app)/(auth)/account': typeof appauthAccountRouteRouteWithChildren '/(app)/(auth)/account': typeof appauthAccountRouteRouteWithChildren
'/(app)/(auth)/kanri': typeof appauthKanriRouteRouteWithChildren '/(app)/(auth)/kanri': typeof appauthKanriRouteRouteWithChildren
@@ -187,6 +205,7 @@ export interface FileRoutesById {
'/(app)/(auth)/kanri/users': typeof appauthKanriUsersRoute '/(app)/(auth)/kanri/users': typeof appauthKanriUsersRoute
'/(app)/(auth)/management/dashboard': typeof appauthManagementDashboardRoute '/(app)/(auth)/management/dashboard': typeof appauthManagementDashboardRoute
'/(app)/(auth)/management/houses': typeof appauthManagementHousesRoute '/(app)/(auth)/management/houses': typeof appauthManagementHousesRoute
'/(app)/(auth)/management/notifications': typeof appauthManagementNotificationsRoute
'/(app)/(auth)/account/': typeof appauthAccountIndexRoute '/(app)/(auth)/account/': typeof appauthAccountIndexRoute
'/(app)/(auth)/kanri/': typeof appauthKanriIndexRoute '/(app)/(auth)/kanri/': typeof appauthKanriIndexRoute
'/(app)/(auth)/management/': typeof appauthManagementIndexRoute '/(app)/(auth)/management/': typeof appauthManagementIndexRoute
@@ -195,6 +214,7 @@ export interface FileRouteTypes {
fileRoutesByFullPath: FileRoutesByFullPath fileRoutesByFullPath: FileRoutesByFullPath
fullPaths: fullPaths:
| '/sign-in' | '/sign-in'
| '/api/notify'
| '/' | '/'
| '/account' | '/account'
| '/kanri' | '/kanri'
@@ -209,12 +229,14 @@ export interface FileRouteTypes {
| '/kanri/users' | '/kanri/users'
| '/management/dashboard' | '/management/dashboard'
| '/management/houses' | '/management/houses'
| '/management/notifications'
| '/account/' | '/account/'
| '/kanri/' | '/kanri/'
| '/management/' | '/management/'
fileRoutesByTo: FileRoutesByTo fileRoutesByTo: FileRoutesByTo
to: to:
| '/sign-in' | '/sign-in'
| '/api/notify'
| '/' | '/'
| '/api/auth/$' | '/api/auth/$'
| '/account/change-password' | '/account/change-password'
@@ -226,6 +248,7 @@ export interface FileRouteTypes {
| '/kanri/users' | '/kanri/users'
| '/management/dashboard' | '/management/dashboard'
| '/management/houses' | '/management/houses'
| '/management/notifications'
| '/account' | '/account'
| '/kanri' | '/kanri'
| '/management' | '/management'
@@ -234,6 +257,7 @@ export interface FileRouteTypes {
| '/(app)' | '/(app)'
| '/(app)/(auth)' | '/(app)/(auth)'
| '/(auth)/sign-in' | '/(auth)/sign-in'
| '/api/notify'
| '/(app)/' | '/(app)/'
| '/(app)/(auth)/account' | '/(app)/(auth)/account'
| '/(app)/(auth)/kanri' | '/(app)/(auth)/kanri'
@@ -248,6 +272,7 @@ export interface FileRouteTypes {
| '/(app)/(auth)/kanri/users' | '/(app)/(auth)/kanri/users'
| '/(app)/(auth)/management/dashboard' | '/(app)/(auth)/management/dashboard'
| '/(app)/(auth)/management/houses' | '/(app)/(auth)/management/houses'
| '/(app)/(auth)/management/notifications'
| '/(app)/(auth)/account/' | '/(app)/(auth)/account/'
| '/(app)/(auth)/kanri/' | '/(app)/(auth)/kanri/'
| '/(app)/(auth)/management/' | '/(app)/(auth)/management/'
@@ -256,6 +281,7 @@ export interface FileRouteTypes {
export interface RootRouteChildren { export interface RootRouteChildren {
appRouteRoute: typeof appRouteRouteWithChildren appRouteRoute: typeof appRouteRouteWithChildren
authSignInRoute: typeof authSignInRoute authSignInRoute: typeof authSignInRoute
ApiNotifyRoute: typeof ApiNotifyRoute
ApiAuthSplatRoute: typeof ApiAuthSplatRoute ApiAuthSplatRoute: typeof ApiAuthSplatRoute
} }
@@ -275,6 +301,13 @@ declare module '@tanstack/react-router' {
preLoaderRoute: typeof appIndexRouteImport preLoaderRoute: typeof appIndexRouteImport
parentRoute: typeof appRouteRoute parentRoute: typeof appRouteRoute
} }
'/api/notify': {
id: '/api/notify'
path: '/api/notify'
fullPath: '/api/notify'
preLoaderRoute: typeof ApiNotifyRouteImport
parentRoute: typeof rootRouteImport
}
'/(auth)/sign-in': { '/(auth)/sign-in': {
id: '/(auth)/sign-in' id: '/(auth)/sign-in'
path: '/sign-in' path: '/sign-in'
@@ -338,6 +371,13 @@ declare module '@tanstack/react-router' {
preLoaderRoute: typeof appauthAccountIndexRouteImport preLoaderRoute: typeof appauthAccountIndexRouteImport
parentRoute: typeof appauthAccountRouteRoute parentRoute: typeof appauthAccountRouteRoute
} }
'/(app)/(auth)/management/notifications': {
id: '/(app)/(auth)/management/notifications'
path: '/notifications'
fullPath: '/management/notifications'
preLoaderRoute: typeof appauthManagementNotificationsRouteImport
parentRoute: typeof appauthManagementRouteRoute
}
'/(app)/(auth)/management/houses': { '/(app)/(auth)/management/houses': {
id: '/(app)/(auth)/management/houses' id: '/(app)/(auth)/management/houses'
path: '/houses' path: '/houses'
@@ -443,6 +483,7 @@ const appauthKanriRouteRouteWithChildren =
interface appauthManagementRouteRouteChildren { interface appauthManagementRouteRouteChildren {
appauthManagementDashboardRoute: typeof appauthManagementDashboardRoute appauthManagementDashboardRoute: typeof appauthManagementDashboardRoute
appauthManagementHousesRoute: typeof appauthManagementHousesRoute appauthManagementHousesRoute: typeof appauthManagementHousesRoute
appauthManagementNotificationsRoute: typeof appauthManagementNotificationsRoute
appauthManagementIndexRoute: typeof appauthManagementIndexRoute appauthManagementIndexRoute: typeof appauthManagementIndexRoute
} }
@@ -450,6 +491,7 @@ const appauthManagementRouteRouteChildren: appauthManagementRouteRouteChildren =
{ {
appauthManagementDashboardRoute: appauthManagementDashboardRoute, appauthManagementDashboardRoute: appauthManagementDashboardRoute,
appauthManagementHousesRoute: appauthManagementHousesRoute, appauthManagementHousesRoute: appauthManagementHousesRoute,
appauthManagementNotificationsRoute: appauthManagementNotificationsRoute,
appauthManagementIndexRoute: appauthManagementIndexRoute, appauthManagementIndexRoute: appauthManagementIndexRoute,
} }
@@ -491,6 +533,7 @@ const appRouteRouteWithChildren = appRouteRoute._addFileChildren(
const rootRouteChildren: RootRouteChildren = { const rootRouteChildren: RootRouteChildren = {
appRouteRoute: appRouteRouteWithChildren, appRouteRoute: appRouteRouteWithChildren,
authSignInRoute: authSignInRoute, authSignInRoute: authSignInRoute,
ApiNotifyRoute: ApiNotifyRoute,
ApiAuthSplatRoute: ApiAuthSplatRoute, ApiAuthSplatRoute: ApiAuthSplatRoute,
} }
export const routeTree = rootRouteImport export const routeTree = rootRouteImport

View File

@@ -0,0 +1,60 @@
import Pagination from '@/components/Pagination';
import { m } from '@/paraglide/messages';
import { notificationQueries } from '@/service/queries';
import NotificationItem from '@components/notification/notification-item';
import { BellRingingIcon } from '@phosphor-icons/react';
import { useQuery } from '@tanstack/react-query';
import { createFileRoute } from '@tanstack/react-router';
import { Card, CardHeader, CardTitle } from '@ui/card';
import { useState } from 'react';
export const Route = createFileRoute('/(app)/(auth)/management/notifications')({
component: RouteComponent,
staticData: { breadcrumb: () => m.ui_label_notifications() },
});
function RouteComponent() {
const [page, setPage] = useState(1);
const { data, isLoading } = useQuery(
notificationQueries.list({
page,
limit: 10,
}),
);
if (isLoading) return null;
return (
<div className="@container/main p-4">
<div className="*:data-[slot=card]:from-primary/5 *:data-[slot=card]:to-card dark:*:data-[slot=card]:bg-card *:data-[slot=card]:bg-linear-to-br *:data-[slot=card]:shadow-xs">
<Card>
<CardHeader>
<CardTitle className="text-xl flex items-center gap-2">
<BellRingingIcon
size={24}
className="origin-top animate-bell-ring"
/>
{m.ui_label_notifications()}
</CardTitle>
</CardHeader>
</Card>
{data && data.result.length > 0 ? (
<div className="mt-5 max-w-200 min-w-107 mx-auto flex flex-col gap-5">
{data.result.map((notify) => {
return <NotificationItem notify={notify} key={notify.id} />;
})}
<Pagination
currentPage={page}
totalPages={data?.pagination.totalPage}
onPageChange={setPage}
/>
</div>
) : (
<div className="mt-5 max-w-full h-50 mx-auto flex justify-center items-center">
{m.common_no_notify()}
</div>
)}
</div>
</div>
);
}

View File

@@ -2,9 +2,11 @@ import { AuthProvider } from '@/components/auth/auth-provider';
import Header from '@/components/Header'; import Header from '@/components/Header';
import AppSidebar from '@/components/sidebar/app-sidebar'; import AppSidebar from '@/components/sidebar/app-sidebar';
import { SidebarInset, SidebarProvider } from '@/components/ui/sidebar'; import { SidebarInset, SidebarProvider } from '@/components/ui/sidebar';
import { useSSE } from '@/hooks/use-sse';
import useNotificationStore from '@/store/useNotificationStore';
import { Locale, setLocale } from '@paraglide/runtime'; import { Locale, setLocale } from '@paraglide/runtime';
import { settingQueries } from '@service/queries'; import { notificationQueries, settingQueries } from '@service/queries';
import { useQuery } from '@tanstack/react-query'; import { useQuery, useQueryClient } from '@tanstack/react-query';
import { createFileRoute, Outlet } from '@tanstack/react-router'; import { createFileRoute, Outlet } from '@tanstack/react-router';
import { useEffect } from 'react'; import { useEffect } from 'react';
@@ -13,14 +15,25 @@ export const Route = createFileRoute('/(app)')({
}); });
function RouteComponent() { function RouteComponent() {
const setHasNew = useNotificationStore((state) => state.setHasNew);
const { data: language } = useQuery( const { data: language } = useQuery(
settingQueries.getCurrentUserLanguageSetting(), settingQueries.getCurrentUserLanguageSetting(),
); );
const queryClient = useQueryClient();
useSSE((data) => {
if (data.type === 'NEW_NOTIFICATION') {
setHasNew(true);
queryClient.invalidateQueries(notificationQueries.topFive());
}
});
useEffect(() => { useEffect(() => {
if (language) { if (language) {
setLocale(language as Locale); setLocale(language as Locale);
} }
}, [language]); }, [language]);
return ( return (
<AuthProvider> <AuthProvider>
<SidebarProvider> <SidebarProvider>

48
src/routes/api/notify.ts Normal file
View File

@@ -0,0 +1,48 @@
import { auth } from '@/lib/auth';
import { addClient, removeClient } from '@/lib/notification';
import { createFileRoute } from '@tanstack/react-router';
import { getRequestHeaders } from '@tanstack/react-start/server';
export const Route = createFileRoute('/api/notify')({
server: {
handlers: {
GET: async ({ request }) => {
const headers = getRequestHeaders();
const session = await auth.api.getSession({ headers });
if (!session?.session.userId) {
return new Response('Unauthorized', { status: 401 });
}
const clientId = session.session.userId;
const stream = new ReadableStream({
start(controller) {
addClient(clientId, controller);
controller.enqueue(
`data: ${JSON.stringify({ type: `connected` })}\n\n`,
);
const heartbeat = setInterval(() => {
controller.enqueue(`: keepalive\n\n`);
}, 20000);
request.signal.addEventListener('abort', () => {
clearInterval(heartbeat);
removeClient(clientId, controller);
});
},
});
return new Response(stream, {
headers: {
'Content-Type': 'text/event-stream',
'Cache-Control': 'no-cache',
Connection: 'keep-alive',
},
});
},
},
},
});

View File

@@ -34,23 +34,24 @@ export const getAllAudit = createServerFn({ method: 'GET' })
}, },
], ],
}; };
const [auditlog, total]: [AuditWithUser[], number] = await Promise.all([ const [auditlog, total]: [AuditWithUser[], number] =
await prisma.audit.findMany({ await prisma.$transaction([
where, prisma.audit.findMany({
orderBy: { createdAt: 'desc' }, where,
include: { orderBy: { createdAt: 'desc' },
user: { include: {
select: { user: {
id: true, select: {
name: true, id: true,
name: true,
},
}, },
}, },
}, take: data.limit,
take: data.limit, skip,
skip, }),
}), prisma.audit.count({ where }),
await prisma.audit.count({ where }), ]);
]);
const totalPage = Math.ceil(+total / data.limit); const totalPage = Math.ceil(+total / data.limit);

View File

@@ -1,19 +1,20 @@
import { prisma } from '@/db'; import { prisma } from '@/db';
import { OrganizationWhereInput } from '@/generated/prisma/models'; import { OrganizationWhereInput } from '@/generated/prisma/models';
import { DB_TABLE, LOG_ACTION } from '@/types/enum'; import { DB_TABLE, LOG_ACTION, NOTIFICATION_TYPE } from '@/types/enum';
import { auth } from '@lib/auth'; import { auth } from '@lib/auth';
import { parseError } from '@lib/errors'; import { parseError } from '@lib/errors';
import { authMiddleware } from '@lib/middleware'; import { authMiddleware } from '@lib/middleware';
import { createServerFn } from '@tanstack/react-start'; import { createServerFn } from '@tanstack/react-start';
import { getRequestHeaders } from '@tanstack/react-start/server'; import { getRequestHeaders } from '@tanstack/react-start/server';
import { import {
actionInvitationSchema,
baseHouse, baseHouse,
houseCreateBESchema, houseCreateBESchema,
houseEditBESchema, houseEditBESchema,
houseListSchema, houseListSchema,
invitationCreateBESchema, invitationCreateBESchema,
} from './house.schema'; } from './house.schema';
import { createAuditLog } from './repository'; import { createAuditLog, createNotification } from './repository';
export const getAllHouse = createServerFn({ method: 'GET' }) export const getAllHouse = createServerFn({ method: 'GET' })
.middleware([authMiddleware]) .middleware([authMiddleware])
@@ -266,12 +267,31 @@ export const invitationMember = createServerFn({ method: 'POST' })
organizationId: data.houseId, organizationId: data.houseId,
}; };
const cuser = await prisma.user.findUnique({
where: { email: data.email },
});
const chouse = await prisma.organization.findUnique({
where: { id: data.houseId },
});
const result = await auth.api.createInvitation({ const result = await auth.api.createInvitation({
body, body,
headers, headers,
}); });
if (result) { if (result && cuser && chouse) {
await createNotification({
type: NOTIFICATION_TYPE.INVITATION,
userId: cuser.id,
title: 'INVITATION_HOUSE',
message: 'INVITATION_HOUSE',
link: result.id,
metadata: JSON.stringify({
house: chouse,
}),
readAt: null,
});
await createAuditLog({ await createAuditLog({
action: LOG_ACTION.CREATE, action: LOG_ACTION.CREATE,
tableName: DB_TABLE.INVITATION, tableName: DB_TABLE.INVITATION,
@@ -302,6 +322,81 @@ export const cancelInvitation = createServerFn({ method: 'POST' })
}, },
headers, headers,
}); });
if (result) {
const notification = await prisma.notification.findFirst({
where: { link: data.id },
});
if (notification) {
await prisma.notification.update({
where: { id: notification.id },
data: { link: null },
});
}
}
return result;
} catch (error) {
console.error(error);
const { message, code } = parseError(error);
throw { message, code };
}
});
export const acceptInvitation = createServerFn({ method: 'POST' })
.middleware([authMiddleware])
.inputValidator(actionInvitationSchema)
.handler(async ({ data }) => {
try {
const result = await prisma.invitation.update({
where: { id: data.id },
data: { status: 'accepted' },
});
if (result) {
const notify = await prisma.notification.update({
where: { id: data.notificationId },
data: { link: null },
});
await auth.api.addMember({
body: {
userId: notify.userId,
organizationId: result.organizationId,
role: (result.role as 'admin' | 'owner' | 'member') || 'member',
},
});
}
return result;
} catch (error) {
console.error(error);
const { message, code } = parseError(error);
throw { message, code };
}
});
export const rejectInvitation = createServerFn({ method: 'POST' })
.middleware([authMiddleware])
.inputValidator(actionInvitationSchema)
.handler(async ({ data }) => {
try {
const headers = getRequestHeaders();
const result = await auth.api.rejectInvitation({
body: {
invitationId: data.id,
},
headers,
});
if (result) {
await prisma.notification.update({
where: { id: data.notificationId },
data: { link: null },
});
}
return result; return result;
} catch (error) { } catch (error) {
console.error(error); console.error(error);

View File

@@ -49,6 +49,10 @@ export const RoleHouseEnum = z.enum(
m.users_page_message_role_select(), m.users_page_message_role_select(),
); );
const baseInvitation = z.object({
id: z.string().nonempty(m.invitation_not_found()),
});
const invitationCreateSchema = z.object({ const invitationCreateSchema = z.object({
email: z email: z
.string() .string()
@@ -63,3 +67,7 @@ export const invitationCreateFESchema = invitationCreateSchema.extend({
export const invitationCreateBESchema = invitationCreateSchema.extend({ export const invitationCreateBESchema = invitationCreateSchema.extend({
role: RoleHouseEnum, role: RoleHouseEnum,
}); });
export const actionInvitationSchema = baseInvitation.extend({
notificationId: z.string().nonempty(m.notification_page_notify_not_found()),
});

115
src/service/notify.api.ts Normal file
View File

@@ -0,0 +1,115 @@
import { prisma } from '@/db';
import { Notification } from '@/generated/prisma/client';
import { parseError } from '@lib/errors';
import { authMiddleware } from '@lib/middleware';
import { createServerFn } from '@tanstack/react-start';
import { notificationListSchema } from './notify.schema';
export const getTopFiveNotification = createServerFn({ method: 'GET' })
.middleware([authMiddleware])
.handler(async ({ context: { user } }) => {
try {
const [list, total]: [Notification[], number] = await prisma.$transaction(
[
prisma.notification.findMany({
where: {
userId: user.id,
},
orderBy: {
createdAt: 'asc',
},
take: 5,
}),
prisma.notification.count({
where: {
userId: user.id,
readAt: null,
},
}),
],
);
return {
list,
hasNewNotify: total > 0,
};
} catch (error) {
console.error(error);
const { message, code } = parseError(error);
throw { message, code };
}
});
export const getAllNotifications = createServerFn({ method: 'GET' })
.middleware([authMiddleware])
.inputValidator(notificationListSchema)
.handler(async ({ data, context: { user } }) => {
try {
const skip = (data.page - 1) * data.limit;
const [list, total]: [NotificationWithUser[], number] =
await prisma.$transaction([
prisma.notification.findMany({
where: {
userId: user.id,
},
include: {
user: {
select: {
id: true,
name: true,
email: true,
image: true,
},
},
},
orderBy: {
createdAt: 'asc',
},
take: data.limit,
skip,
}),
prisma.notification.count({
where: {
userId: user.id,
},
}),
]);
const totalPage = Math.ceil(+total / data.limit);
return {
result: list,
pagination: {
currentPage: data.page,
totalPage,
totalItem: total,
},
};
} catch (error) {
console.error(error);
const { message, code } = parseError(error);
throw { message, code };
}
});
export const updateReadedNotification = createServerFn({ method: 'POST' })
.middleware([authMiddleware])
.handler(async ({ context: { user } }) => {
try {
const result = await prisma.notification.findMany({
where: { userId: user.id, readAt: null },
});
if (result.length > 0) {
await prisma.notification.updateMany({
where: { userId: user.id, readAt: null },
data: { readAt: new Date() },
});
}
} catch (error) {
console.error(error);
const { message, code } = parseError(error);
throw { message, code };
}
});

View File

@@ -0,0 +1,6 @@
import z from 'zod';
export const notificationListSchema = z.object({
page: z.coerce.number().min(1).default(1),
limit: z.coerce.number().min(10).max(100).default(10),
});

View File

@@ -2,6 +2,7 @@ import { getSession } from '@lib/auth/session';
import { queryOptions } from '@tanstack/react-query'; import { queryOptions } from '@tanstack/react-query';
import { getAllAudit } from './audit.api'; import { getAllAudit } from './audit.api';
import { getAllHouse, getCurrentUserHouses } from './house.api'; import { getAllHouse, getCurrentUserHouses } from './house.api';
import { getAllNotifications, getTopFiveNotification } from './notify.api';
import { import {
getAdminSettings, getAdminSettings,
getCurrentUserLanguage, getCurrentUserLanguage,
@@ -75,3 +76,17 @@ export const housesQueries = {
queryFn: () => getCurrentUserHouses(), queryFn: () => getCurrentUserHouses(),
}), }),
}; };
export const notificationQueries = {
all: ['notification'],
list: (params: { page: number; limit: number }) =>
queryOptions({
queryKey: [...notificationQueries.all, 'list', params],
queryFn: () => getAllNotifications({ data: params }),
}),
topFive: () =>
queryOptions({
queryKey: [...notificationQueries.all, 'topFive'],
queryFn: () => getTopFiveNotification(),
}),
};

View File

@@ -1,5 +1,6 @@
import { prisma } from '@/db'; import { prisma } from '@/db';
import { Audit, Setting } from '@/generated/prisma/client'; import { Audit, Notification, Setting } from '@/generated/prisma/client';
import { sendToUser } from '@/lib/notification';
type AdminSettingValue = Pick<Setting, 'id' | 'key' | 'value'>; type AdminSettingValue = Pick<Setting, 'id' | 'key' | 'value'>;
@@ -61,3 +62,19 @@ export const getInitialOrganization = async (userId: string) => {
return organization; return organization;
}; };
export const createNotification = async (
data: Omit<Notification, 'id' | 'createdAt'>,
) => {
await prisma.notification.create({
data: {
...data,
},
});
sendToUser(data.userId, {
type: 'NEW_NOTIFICATION',
title: data.title,
message: data.message,
});
};

View File

@@ -0,0 +1,14 @@
import { create } from 'zustand';
type NotifyState = {
hasNew: boolean;
setHasNew: (hasNew: boolean) => void;
};
const useNotificationStore = create<NotifyState>((set) => ({
hasNew: false,
setHasNew: (hasNew: boolean) => set({ hasNew }),
toggleNew: () => set((state) => ({ hasNew: !state.hasNew })),
}));
export default useNotificationStore;

View File

@@ -5,6 +5,22 @@
@custom-variant dark (&:is(.dark *)); @custom-variant dark (&:is(.dark *));
@theme {
--animate-bell-ring: bell-ring 4s .7s ease-in-out infinite;
@keyframes bell-ring {
0% { transform: rotate(0deg); }
10% { transform: rotate(15deg); }
20% { transform: rotate(-15deg); }
30% { transform: rotate(10deg); }
40% { transform: rotate(-10deg); }
50% { transform: rotate(5deg); }
60% { transform: rotate(-5deg); }
70% { transform: rotate(0deg); }
100% { transform: rotate(0deg); }
}
}
body { body {
@apply m-0; @apply m-0;
font-family: var(--font-sans); font-family: var(--font-sans);

13
src/types/db.d.ts vendored
View File

@@ -36,6 +36,19 @@ declare global {
}; };
}; };
type NotificationWithUser = Prisma.NotificationGetPayload<{
include: {
user: {
select: {
id: true;
name: true;
email: true;
image: true;
};
};
};
}>;
type ReturnError = Error & { type ReturnError = Error & {
code: string; code: string;
message: string; message: string;

View File

@@ -38,10 +38,21 @@ export type ROLE_NAME = (typeof ROLE_NAME)[keyof typeof ROLE_NAME];
export const INVITE_STATUS = { export const INVITE_STATUS = {
PENDING: 'pending', PENDING: 'pending',
ACCEPT: 'accept', ACCEPT: 'accepted',
REJECT: 'reject', REJECT: 'rejected',
CANCELED: 'canceled', CANCELED: 'canceled',
EXPIRED: 'expired', EXPIRED: 'expired',
} as const; } as const;
export type INVITE_STATUS = (typeof INVITE_STATUS)[keyof typeof INVITE_STATUS]; export type INVITE_STATUS = (typeof INVITE_STATUS)[keyof typeof INVITE_STATUS];
export const NOTIFICATION_TYPE = {
SYSTEM: 'system',
ERROR: 'error',
INVITATION: 'invitation',
HOUSE: 'house',
EXPIRED: 'expired',
} as const;
export type NOTIFICATION_TYPE =
(typeof NOTIFICATION_TYPE)[keyof typeof NOTIFICATION_TYPE];

View File

@@ -1,3 +1,5 @@
import { m } from '@paraglide/messages';
export function jsonSupport(jsonSTR: string) { export function jsonSupport(jsonSTR: string) {
try { try {
const data = JSON.parse(jsonSTR); const data = JSON.parse(jsonSTR);
@@ -36,3 +38,28 @@ export function slugify(text: string) {
.trim() .trim()
.replace(/\s+/g, '-'); .replace(/\s+/g, '-');
} }
const UNITS = [
{ limit: 60, divisor: 1, key: 'common_time_ago_second' },
{ limit: 60, divisor: 60, key: 'common_time_ago_minute' },
{ limit: 24, divisor: 60 * 60, key: 'common_time_ago_hour' },
{ limit: 30, divisor: 60 * 60 * 24, key: 'common_time_ago_day' },
{ limit: 12, divisor: 60 * 60 * 24 * 30, key: 'common_time_ago_month' },
{ limit: Infinity, divisor: 60 * 60 * 24 * 365, key: 'common_time_ago_year' },
] as const;
export function formatTimeAgo(input: Date | string): string {
const date = typeof input === 'string' ? new Date(input) : input;
const diffSec = Math.floor((Date.now() - date.getTime()) / 1000);
if (diffSec < 10) return m.common_time_ago_second({ value: 1 });
for (const unit of UNITS) {
const value = Math.floor(diffSec / unit.divisor);
if (value < unit.limit) {
return m[unit.key]({ value });
}
}
return '';
}