feature/notification #12

Merged
sam merged 2 commits from feature/notification into develop 2026-02-21 15:35:42 +00:00
35 changed files with 2592 additions and 112 deletions
Showing only changes of commit fa689ea4aa - Show all commits

View File

@@ -13,7 +13,15 @@
"common_per_page": "Show",
"common_select_page_size": "Select page size",
"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_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": [
{
"match": {
@@ -170,23 +178,42 @@
"houses_page_message_house_not_found": "House not found!",
"houses_page_message_update_house_success": "Updated house successfully!",
"houses_page_message_delete_house_success": "Delete house successfully!",
"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_user_page_message_active_house_success": "Active \"<b>{house}</b>\" successfully!",
"houses_user_page_block_action_title": "Action",
"houses_user_page_action_invite_user": "Invite member",
"houses_user_page_invite_label_to": "To",
"houses_user_page_invite_label_status": "Status",
"invitation_not_found": "Invitation not found!",
"notification_page_notify_not_found": "Notification not found!",
"invite_status": [
{
"match": {
"status=pending": "Pending",
"status=accept": "Accept",
"status=reject": "Reject",
"status=accepted": "Accept",
"status=rejected": "Reject",
"status=expired": "Expired",
"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": [
{
"match": {
@@ -197,7 +224,8 @@
"code=BANNED_USER": "Your account get banned, please contact administrator for more information!",
"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_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_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_notify": "Không có thống báo",
"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": [
{
"match": {
@@ -50,6 +58,8 @@
"ui_ban_btn": "Khóa",
"ui_unban_btn": "Mở khóa",
"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_change_role_btn": "Đặt lại quyền hạn",
"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_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_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_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_action_invite_user": "Mời thành viên",
"houses_user_page_invite_label_to": "Đến",
"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": [
{
"match": {
"status=pending": "Đang chờ",
"status=accept": "Đồng ý",
"status=reject": "Không đồng ý",
"status=accepted": "Đồng ý",
"status=rejected": "Không đồng ý",
"status=expired": "Hết hạn",
"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": [
{
"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=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_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",
"check": "prettier --write . && eslint --fix",
"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:push": "dotenv -e .env.local -- prisma db push",
"db:migrate": "dotenv -e .env.local -- prisma migrate dev",

View File

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

View File

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

View File

@@ -1,19 +1,7 @@
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 { useAuth } from './auth/auth-provider';
import Notification from './Notification';
import RouterBreadcrumb from './sidebar/router-breadcrumb';
export default function Header() {
@@ -30,48 +18,7 @@ export default function Header() {
/>
<RouterBreadcrumb />
</div>
<div className="flex mr-2">
{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>
<div className="flex mr-2">{session?.user && <Notification />}</div>
</header>
</>
);

View File

@@ -0,0 +1,83 @@
import { notificationQueries } from '@/service/queries';
import { formatTimeAgo } from '@/utils/helper';
import { cn } from '@lib/utils';
import { m } from '@paraglide/messages';
import { BellIcon } from '@phosphor-icons/react';
import { 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 { Item, ItemContent, ItemDescription, ItemTitle } from './ui/item';
const Notification = () => {
const { data: notifications } = useQuery(notificationQueries.topFive());
if (!notifications) return null;
return (
<DropdownMenu>
<DropdownMenuTrigger asChild>
<Button
size="icon-lg"
variant="ghost"
className="relative rounded-full"
>
<BellIcon
size={32}
className={cn('origin-top', { 'animate-bell-ring': true })}
/>
{true && (
<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>
{notifications.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>
);
})}
</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);
refetch();
toast.success(m.houses_page_message_create_house_success(), {
toast.success(m.houses_page_message_invite_member_success(), {
richColors: true,
});
},

View File

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

View File

@@ -18,7 +18,7 @@ type ActionProps = {};
const InviteUserAction = ({}: ActionProps) => {
const { hasPermission, isLoading } = useHasPermission(
'house',
'invitation',
'create',
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 (
<Dialog open={_open} onOpenChange={_setOpen}>
<DialogTrigger asChild>
<Button type="button" variant="default">
<Button type="button" variant="default" className="cursor-pointer">
<PlusIcon />
{m.nav_add_new()}
</Button>

View File

@@ -62,3 +62,8 @@ export type Setting = Prisma.SettingModel
*
*/
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
/**
* 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',
Invitation: 'Invitation',
Setting: 'Setting',
Audit: 'Audit'
Audit: 'Audit',
Notification: 'Notification'
} as const
export type ModelName = (typeof ModelName)[keyof typeof ModelName]
@@ -408,7 +409,7 @@ export type TypeMap<ExtArgs extends runtime.Types.Extensions.InternalArgs = runt
omit: GlobalOmitOptions
}
meta: {
modelProps: "user" | "session" | "account" | "verification" | "organization" | "member" | "invitation" | "setting" | "audit"
modelProps: "user" | "session" | "account" | "verification" | "organization" | "member" | "invitation" | "setting" | "audit" | "notification"
txIsolationLevel: TransactionIsolationLevel
}
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: {
@@ -1246,6 +1321,21 @@ export const 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 = {
asc: 'asc',
desc: 'desc'
@@ -1428,6 +1518,7 @@ export type GlobalOmitConfig = {
invitation?: Prisma.InvitationOmit
setting?: Prisma.SettingOmit
audit?: Prisma.AuditOmit
notification?: Prisma.NotificationOmit
}
/* Types for Logging */

View File

@@ -59,7 +59,8 @@ export const ModelName = {
Member: 'Member',
Invitation: 'Invitation',
Setting: 'Setting',
Audit: 'Audit'
Audit: 'Audit',
Notification: 'Notification'
} as const
export type ModelName = (typeof ModelName)[keyof typeof ModelName]
@@ -207,6 +208,21 @@ export const 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 = {
asc: 'asc',
desc: 'desc'

View File

@@ -17,4 +17,5 @@ export type * from './models/Member.ts'
export type * from './models/Invitation.ts'
export type * from './models/Setting.ts'
export type * from './models/Audit.ts'
export type * from './models/Notification.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
accounts?: Prisma.AccountListRelationFilter
audit?: Prisma.AuditListRelationFilter
notification?: Prisma.NotificationListRelationFilter
members?: Prisma.MemberListRelationFilter
invitations?: Prisma.InvitationListRelationFilter
}
@@ -252,6 +253,7 @@ export type UserOrderByWithRelationInput = {
sessions?: Prisma.SessionOrderByRelationAggregateInput
accounts?: Prisma.AccountOrderByRelationAggregateInput
audit?: Prisma.AuditOrderByRelationAggregateInput
notification?: Prisma.NotificationOrderByRelationAggregateInput
members?: Prisma.MemberOrderByRelationAggregateInput
invitations?: Prisma.InvitationOrderByRelationAggregateInput
}
@@ -274,6 +276,7 @@ export type UserWhereUniqueInput = Prisma.AtLeast<{
sessions?: Prisma.SessionListRelationFilter
accounts?: Prisma.AccountListRelationFilter
audit?: Prisma.AuditListRelationFilter
notification?: Prisma.NotificationListRelationFilter
members?: Prisma.MemberListRelationFilter
invitations?: Prisma.InvitationListRelationFilter
}, "id" | "email">
@@ -327,6 +330,7 @@ export type UserCreateInput = {
sessions?: Prisma.SessionCreateNestedManyWithoutUserInput
accounts?: Prisma.AccountCreateNestedManyWithoutUserInput
audit?: Prisma.AuditCreateNestedManyWithoutUserInput
notification?: Prisma.NotificationCreateNestedManyWithoutUserInput
members?: Prisma.MemberCreateNestedManyWithoutUserInput
invitations?: Prisma.InvitationCreateNestedManyWithoutUserInput
}
@@ -346,6 +350,7 @@ export type UserUncheckedCreateInput = {
sessions?: Prisma.SessionUncheckedCreateNestedManyWithoutUserInput
accounts?: Prisma.AccountUncheckedCreateNestedManyWithoutUserInput
audit?: Prisma.AuditUncheckedCreateNestedManyWithoutUserInput
notification?: Prisma.NotificationUncheckedCreateNestedManyWithoutUserInput
members?: Prisma.MemberUncheckedCreateNestedManyWithoutUserInput
invitations?: Prisma.InvitationUncheckedCreateNestedManyWithoutUserInput
}
@@ -365,6 +370,7 @@ export type UserUpdateInput = {
sessions?: Prisma.SessionUpdateManyWithoutUserNestedInput
accounts?: Prisma.AccountUpdateManyWithoutUserNestedInput
audit?: Prisma.AuditUpdateManyWithoutUserNestedInput
notification?: Prisma.NotificationUpdateManyWithoutUserNestedInput
members?: Prisma.MemberUpdateManyWithoutUserNestedInput
invitations?: Prisma.InvitationUpdateManyWithoutUserNestedInput
}
@@ -384,6 +390,7 @@ export type UserUncheckedUpdateInput = {
sessions?: Prisma.SessionUncheckedUpdateManyWithoutUserNestedInput
accounts?: Prisma.AccountUncheckedUpdateManyWithoutUserNestedInput
audit?: Prisma.AuditUncheckedUpdateManyWithoutUserNestedInput
notification?: Prisma.NotificationUncheckedUpdateManyWithoutUserNestedInput
members?: Prisma.MemberUncheckedUpdateManyWithoutUserNestedInput
invitations?: Prisma.InvitationUncheckedUpdateManyWithoutUserNestedInput
}
@@ -571,6 +578,20 @@ export type UserUpdateOneRequiredWithoutAuditNestedInput = {
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 = {
id?: string
name: string
@@ -585,6 +606,7 @@ export type UserCreateWithoutSessionsInput = {
banExpires?: Date | string | null
accounts?: Prisma.AccountCreateNestedManyWithoutUserInput
audit?: Prisma.AuditCreateNestedManyWithoutUserInput
notification?: Prisma.NotificationCreateNestedManyWithoutUserInput
members?: Prisma.MemberCreateNestedManyWithoutUserInput
invitations?: Prisma.InvitationCreateNestedManyWithoutUserInput
}
@@ -603,6 +625,7 @@ export type UserUncheckedCreateWithoutSessionsInput = {
banExpires?: Date | string | null
accounts?: Prisma.AccountUncheckedCreateNestedManyWithoutUserInput
audit?: Prisma.AuditUncheckedCreateNestedManyWithoutUserInput
notification?: Prisma.NotificationUncheckedCreateNestedManyWithoutUserInput
members?: Prisma.MemberUncheckedCreateNestedManyWithoutUserInput
invitations?: Prisma.InvitationUncheckedCreateNestedManyWithoutUserInput
}
@@ -637,6 +660,7 @@ export type UserUpdateWithoutSessionsInput = {
banExpires?: Prisma.NullableDateTimeFieldUpdateOperationsInput | Date | string | null
accounts?: Prisma.AccountUpdateManyWithoutUserNestedInput
audit?: Prisma.AuditUpdateManyWithoutUserNestedInput
notification?: Prisma.NotificationUpdateManyWithoutUserNestedInput
members?: Prisma.MemberUpdateManyWithoutUserNestedInput
invitations?: Prisma.InvitationUpdateManyWithoutUserNestedInput
}
@@ -655,6 +679,7 @@ export type UserUncheckedUpdateWithoutSessionsInput = {
banExpires?: Prisma.NullableDateTimeFieldUpdateOperationsInput | Date | string | null
accounts?: Prisma.AccountUncheckedUpdateManyWithoutUserNestedInput
audit?: Prisma.AuditUncheckedUpdateManyWithoutUserNestedInput
notification?: Prisma.NotificationUncheckedUpdateManyWithoutUserNestedInput
members?: Prisma.MemberUncheckedUpdateManyWithoutUserNestedInput
invitations?: Prisma.InvitationUncheckedUpdateManyWithoutUserNestedInput
}
@@ -673,6 +698,7 @@ export type UserCreateWithoutAccountsInput = {
banExpires?: Date | string | null
sessions?: Prisma.SessionCreateNestedManyWithoutUserInput
audit?: Prisma.AuditCreateNestedManyWithoutUserInput
notification?: Prisma.NotificationCreateNestedManyWithoutUserInput
members?: Prisma.MemberCreateNestedManyWithoutUserInput
invitations?: Prisma.InvitationCreateNestedManyWithoutUserInput
}
@@ -691,6 +717,7 @@ export type UserUncheckedCreateWithoutAccountsInput = {
banExpires?: Date | string | null
sessions?: Prisma.SessionUncheckedCreateNestedManyWithoutUserInput
audit?: Prisma.AuditUncheckedCreateNestedManyWithoutUserInput
notification?: Prisma.NotificationUncheckedCreateNestedManyWithoutUserInput
members?: Prisma.MemberUncheckedCreateNestedManyWithoutUserInput
invitations?: Prisma.InvitationUncheckedCreateNestedManyWithoutUserInput
}
@@ -725,6 +752,7 @@ export type UserUpdateWithoutAccountsInput = {
banExpires?: Prisma.NullableDateTimeFieldUpdateOperationsInput | Date | string | null
sessions?: Prisma.SessionUpdateManyWithoutUserNestedInput
audit?: Prisma.AuditUpdateManyWithoutUserNestedInput
notification?: Prisma.NotificationUpdateManyWithoutUserNestedInput
members?: Prisma.MemberUpdateManyWithoutUserNestedInput
invitations?: Prisma.InvitationUpdateManyWithoutUserNestedInput
}
@@ -743,6 +771,7 @@ export type UserUncheckedUpdateWithoutAccountsInput = {
banExpires?: Prisma.NullableDateTimeFieldUpdateOperationsInput | Date | string | null
sessions?: Prisma.SessionUncheckedUpdateManyWithoutUserNestedInput
audit?: Prisma.AuditUncheckedUpdateManyWithoutUserNestedInput
notification?: Prisma.NotificationUncheckedUpdateManyWithoutUserNestedInput
members?: Prisma.MemberUncheckedUpdateManyWithoutUserNestedInput
invitations?: Prisma.InvitationUncheckedUpdateManyWithoutUserNestedInput
}
@@ -762,6 +791,7 @@ export type UserCreateWithoutMembersInput = {
sessions?: Prisma.SessionCreateNestedManyWithoutUserInput
accounts?: Prisma.AccountCreateNestedManyWithoutUserInput
audit?: Prisma.AuditCreateNestedManyWithoutUserInput
notification?: Prisma.NotificationCreateNestedManyWithoutUserInput
invitations?: Prisma.InvitationCreateNestedManyWithoutUserInput
}
@@ -780,6 +810,7 @@ export type UserUncheckedCreateWithoutMembersInput = {
sessions?: Prisma.SessionUncheckedCreateNestedManyWithoutUserInput
accounts?: Prisma.AccountUncheckedCreateNestedManyWithoutUserInput
audit?: Prisma.AuditUncheckedCreateNestedManyWithoutUserInput
notification?: Prisma.NotificationUncheckedCreateNestedManyWithoutUserInput
invitations?: Prisma.InvitationUncheckedCreateNestedManyWithoutUserInput
}
@@ -814,6 +845,7 @@ export type UserUpdateWithoutMembersInput = {
sessions?: Prisma.SessionUpdateManyWithoutUserNestedInput
accounts?: Prisma.AccountUpdateManyWithoutUserNestedInput
audit?: Prisma.AuditUpdateManyWithoutUserNestedInput
notification?: Prisma.NotificationUpdateManyWithoutUserNestedInput
invitations?: Prisma.InvitationUpdateManyWithoutUserNestedInput
}
@@ -832,6 +864,7 @@ export type UserUncheckedUpdateWithoutMembersInput = {
sessions?: Prisma.SessionUncheckedUpdateManyWithoutUserNestedInput
accounts?: Prisma.AccountUncheckedUpdateManyWithoutUserNestedInput
audit?: Prisma.AuditUncheckedUpdateManyWithoutUserNestedInput
notification?: Prisma.NotificationUncheckedUpdateManyWithoutUserNestedInput
invitations?: Prisma.InvitationUncheckedUpdateManyWithoutUserNestedInput
}
@@ -850,6 +883,7 @@ export type UserCreateWithoutInvitationsInput = {
sessions?: Prisma.SessionCreateNestedManyWithoutUserInput
accounts?: Prisma.AccountCreateNestedManyWithoutUserInput
audit?: Prisma.AuditCreateNestedManyWithoutUserInput
notification?: Prisma.NotificationCreateNestedManyWithoutUserInput
members?: Prisma.MemberCreateNestedManyWithoutUserInput
}
@@ -868,6 +902,7 @@ export type UserUncheckedCreateWithoutInvitationsInput = {
sessions?: Prisma.SessionUncheckedCreateNestedManyWithoutUserInput
accounts?: Prisma.AccountUncheckedCreateNestedManyWithoutUserInput
audit?: Prisma.AuditUncheckedCreateNestedManyWithoutUserInput
notification?: Prisma.NotificationUncheckedCreateNestedManyWithoutUserInput
members?: Prisma.MemberUncheckedCreateNestedManyWithoutUserInput
}
@@ -902,6 +937,7 @@ export type UserUpdateWithoutInvitationsInput = {
sessions?: Prisma.SessionUpdateManyWithoutUserNestedInput
accounts?: Prisma.AccountUpdateManyWithoutUserNestedInput
audit?: Prisma.AuditUpdateManyWithoutUserNestedInput
notification?: Prisma.NotificationUpdateManyWithoutUserNestedInput
members?: Prisma.MemberUpdateManyWithoutUserNestedInput
}
@@ -920,6 +956,7 @@ export type UserUncheckedUpdateWithoutInvitationsInput = {
sessions?: Prisma.SessionUncheckedUpdateManyWithoutUserNestedInput
accounts?: Prisma.AccountUncheckedUpdateManyWithoutUserNestedInput
audit?: Prisma.AuditUncheckedUpdateManyWithoutUserNestedInput
notification?: Prisma.NotificationUncheckedUpdateManyWithoutUserNestedInput
members?: Prisma.MemberUncheckedUpdateManyWithoutUserNestedInput
}
@@ -937,6 +974,7 @@ export type UserCreateWithoutAuditInput = {
banExpires?: Date | string | null
sessions?: Prisma.SessionCreateNestedManyWithoutUserInput
accounts?: Prisma.AccountCreateNestedManyWithoutUserInput
notification?: Prisma.NotificationCreateNestedManyWithoutUserInput
members?: Prisma.MemberCreateNestedManyWithoutUserInput
invitations?: Prisma.InvitationCreateNestedManyWithoutUserInput
}
@@ -955,6 +993,7 @@ export type UserUncheckedCreateWithoutAuditInput = {
banExpires?: Date | string | null
sessions?: Prisma.SessionUncheckedCreateNestedManyWithoutUserInput
accounts?: Prisma.AccountUncheckedCreateNestedManyWithoutUserInput
notification?: Prisma.NotificationUncheckedCreateNestedManyWithoutUserInput
members?: Prisma.MemberUncheckedCreateNestedManyWithoutUserInput
invitations?: Prisma.InvitationUncheckedCreateNestedManyWithoutUserInput
}
@@ -989,6 +1028,7 @@ export type UserUpdateWithoutAuditInput = {
banExpires?: Prisma.NullableDateTimeFieldUpdateOperationsInput | Date | string | null
sessions?: Prisma.SessionUpdateManyWithoutUserNestedInput
accounts?: Prisma.AccountUpdateManyWithoutUserNestedInput
notification?: Prisma.NotificationUpdateManyWithoutUserNestedInput
members?: Prisma.MemberUpdateManyWithoutUserNestedInput
invitations?: Prisma.InvitationUpdateManyWithoutUserNestedInput
}
@@ -1007,6 +1047,99 @@ export type UserUncheckedUpdateWithoutAuditInput = {
banExpires?: Prisma.NullableDateTimeFieldUpdateOperationsInput | Date | string | null
sessions?: Prisma.SessionUncheckedUpdateManyWithoutUserNestedInput
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
invitations?: Prisma.InvitationUncheckedUpdateManyWithoutUserNestedInput
}
@@ -1020,6 +1153,7 @@ export type UserCountOutputType = {
sessions: number
accounts: number
audit: number
notification: number
members: number
invitations: number
}
@@ -1028,6 +1162,7 @@ export type UserCountOutputTypeSelect<ExtArgs extends runtime.Types.Extensions.I
sessions?: boolean | UserCountOutputTypeCountSessionsArgs
accounts?: boolean | UserCountOutputTypeCountAccountsArgs
audit?: boolean | UserCountOutputTypeCountAuditArgs
notification?: boolean | UserCountOutputTypeCountNotificationArgs
members?: boolean | UserCountOutputTypeCountMembersArgs
invitations?: boolean | UserCountOutputTypeCountInvitationsArgs
}
@@ -1063,6 +1198,13 @@ export type UserCountOutputTypeCountAuditArgs<ExtArgs extends runtime.Types.Exte
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
*/
@@ -1093,6 +1235,7 @@ export type UserSelect<ExtArgs extends runtime.Types.Extensions.InternalArgs = r
sessions?: boolean | Prisma.User$sessionsArgs<ExtArgs>
accounts?: boolean | Prisma.User$accountsArgs<ExtArgs>
audit?: boolean | Prisma.User$auditArgs<ExtArgs>
notification?: boolean | Prisma.User$notificationArgs<ExtArgs>
members?: boolean | Prisma.User$membersArgs<ExtArgs>
invitations?: boolean | Prisma.User$invitationsArgs<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>
accounts?: boolean | Prisma.User$accountsArgs<ExtArgs>
audit?: boolean | Prisma.User$auditArgs<ExtArgs>
notification?: boolean | Prisma.User$notificationArgs<ExtArgs>
members?: boolean | Prisma.User$membersArgs<ExtArgs>
invitations?: boolean | Prisma.User$invitationsArgs<ExtArgs>
_count?: boolean | Prisma.UserCountOutputTypeDefaultArgs<ExtArgs>
@@ -1158,6 +1302,7 @@ export type $UserPayload<ExtArgs extends runtime.Types.Extensions.InternalArgs =
sessions: Prisma.$SessionPayload<ExtArgs>[]
accounts: Prisma.$AccountPayload<ExtArgs>[]
audit: Prisma.$AuditPayload<ExtArgs>[]
notification: Prisma.$NotificationPayload<ExtArgs>[]
members: Prisma.$MemberPayload<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>
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>
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>
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[]
}
/**
* 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
*/

View File

@@ -20,6 +20,7 @@ import { Route as appauthAccountRouteRouteImport } from './routes/(app)/(auth)/a
import { Route as appauthManagementIndexRouteImport } from './routes/(app)/(auth)/management/index'
import { Route as appauthKanriIndexRouteImport } from './routes/(app)/(auth)/kanri/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 appauthManagementDashboardRouteImport } from './routes/(app)/(auth)/management/dashboard'
import { Route as appauthKanriUsersRouteImport } from './routes/(app)/(auth)/kanri/users'
@@ -83,6 +84,12 @@ const appauthAccountIndexRoute = appauthAccountIndexRouteImport.update({
path: '/',
getParentRoute: () => appauthAccountRouteRoute,
} as any)
const appauthManagementNotificationsRoute =
appauthManagementNotificationsRouteImport.update({
id: '/notifications',
path: '/notifications',
getParentRoute: () => appauthManagementRouteRoute,
} as any)
const appauthManagementHousesRoute = appauthManagementHousesRouteImport.update({
id: '/houses',
path: '/houses',
@@ -147,6 +154,7 @@ export interface FileRoutesByFullPath {
'/kanri/users': typeof appauthKanriUsersRoute
'/management/dashboard': typeof appauthManagementDashboardRoute
'/management/houses': typeof appauthManagementHousesRoute
'/management/notifications': typeof appauthManagementNotificationsRoute
'/account/': typeof appauthAccountIndexRoute
'/kanri/': typeof appauthKanriIndexRoute
'/management/': typeof appauthManagementIndexRoute
@@ -164,6 +172,7 @@ export interface FileRoutesByTo {
'/kanri/users': typeof appauthKanriUsersRoute
'/management/dashboard': typeof appauthManagementDashboardRoute
'/management/houses': typeof appauthManagementHousesRoute
'/management/notifications': typeof appauthManagementNotificationsRoute
'/account': typeof appauthAccountIndexRoute
'/kanri': typeof appauthKanriIndexRoute
'/management': typeof appauthManagementIndexRoute
@@ -187,6 +196,7 @@ export interface FileRoutesById {
'/(app)/(auth)/kanri/users': typeof appauthKanriUsersRoute
'/(app)/(auth)/management/dashboard': typeof appauthManagementDashboardRoute
'/(app)/(auth)/management/houses': typeof appauthManagementHousesRoute
'/(app)/(auth)/management/notifications': typeof appauthManagementNotificationsRoute
'/(app)/(auth)/account/': typeof appauthAccountIndexRoute
'/(app)/(auth)/kanri/': typeof appauthKanriIndexRoute
'/(app)/(auth)/management/': typeof appauthManagementIndexRoute
@@ -209,6 +219,7 @@ export interface FileRouteTypes {
| '/kanri/users'
| '/management/dashboard'
| '/management/houses'
| '/management/notifications'
| '/account/'
| '/kanri/'
| '/management/'
@@ -226,6 +237,7 @@ export interface FileRouteTypes {
| '/kanri/users'
| '/management/dashboard'
| '/management/houses'
| '/management/notifications'
| '/account'
| '/kanri'
| '/management'
@@ -248,6 +260,7 @@ export interface FileRouteTypes {
| '/(app)/(auth)/kanri/users'
| '/(app)/(auth)/management/dashboard'
| '/(app)/(auth)/management/houses'
| '/(app)/(auth)/management/notifications'
| '/(app)/(auth)/account/'
| '/(app)/(auth)/kanri/'
| '/(app)/(auth)/management/'
@@ -338,6 +351,13 @@ declare module '@tanstack/react-router' {
preLoaderRoute: typeof appauthAccountIndexRouteImport
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': {
id: '/(app)/(auth)/management/houses'
path: '/houses'
@@ -443,6 +463,7 @@ const appauthKanriRouteRouteWithChildren =
interface appauthManagementRouteRouteChildren {
appauthManagementDashboardRoute: typeof appauthManagementDashboardRoute
appauthManagementHousesRoute: typeof appauthManagementHousesRoute
appauthManagementNotificationsRoute: typeof appauthManagementNotificationsRoute
appauthManagementIndexRoute: typeof appauthManagementIndexRoute
}
@@ -450,6 +471,7 @@ const appauthManagementRouteRouteChildren: appauthManagementRouteRouteChildren =
{
appauthManagementDashboardRoute: appauthManagementDashboardRoute,
appauthManagementHousesRoute: appauthManagementHousesRoute,
appauthManagementNotificationsRoute: appauthManagementNotificationsRoute,
appauthManagementIndexRoute: appauthManagementIndexRoute,
}

View File

@@ -0,0 +1,59 @@
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,
});
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

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

View File

@@ -1,19 +1,20 @@
import { prisma } from '@/db';
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 { parseError } from '@lib/errors';
import { authMiddleware } from '@lib/middleware';
import { createServerFn } from '@tanstack/react-start';
import { getRequestHeaders } from '@tanstack/react-start/server';
import {
actionInvitationSchema,
baseHouse,
houseCreateBESchema,
houseEditBESchema,
houseListSchema,
invitationCreateBESchema,
} from './house.schema';
import { createAuditLog } from './repository';
import { createAuditLog, createNotification } from './repository';
export const getAllHouse = createServerFn({ method: 'GET' })
.middleware([authMiddleware])
@@ -266,12 +267,31 @@ export const invitationMember = createServerFn({ method: 'POST' })
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({
body,
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({
action: LOG_ACTION.CREATE,
tableName: DB_TABLE.INVITATION,
@@ -302,6 +322,81 @@ export const cancelInvitation = createServerFn({ method: 'POST' })
},
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;
} catch (error) {
console.error(error);

View File

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

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

@@ -0,0 +1,80 @@
import { prisma } from '@/db';
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 = await prisma.notification.findMany({
where: {
userId: user.id,
},
orderBy: {
createdAt: 'asc',
},
take: 5,
});
return list;
} 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 };
}
});

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 { getAllAudit } from './audit.api';
import { getAllHouse, getCurrentUserHouses } from './house.api';
import { getAllNotifications, getTopFiveNotification } from './notify.api';
import {
getAdminSettings,
getCurrentUserLanguage,
@@ -75,3 +76,17 @@ export const housesQueries = {
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,5 @@
import { prisma } from '@/db';
import { Audit, Setting } from '@/generated/prisma/client';
import { Audit, Notification, Setting } from '@/generated/prisma/client';
type AdminSettingValue = Pick<Setting, 'id' | 'key' | 'value'>;
@@ -61,3 +61,13 @@ export const getInitialOrganization = async (userId: string) => {
return organization;
};
export const createNotification = async (
data: Omit<Notification, 'id' | 'createdAt'>,
) => {
await prisma.notification.create({
data: {
...data,
},
});
};

View File

@@ -5,6 +5,22 @@
@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 {
@apply m-0;
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 & {
code: string;
message: string;

View File

@@ -38,10 +38,21 @@ export type ROLE_NAME = (typeof ROLE_NAME)[keyof typeof ROLE_NAME];
export const INVITE_STATUS = {
PENDING: 'pending',
ACCEPT: 'accept',
REJECT: 'reject',
ACCEPT: 'accepted',
REJECT: 'rejected',
CANCELED: 'canceled',
EXPIRED: 'expired',
} as const;
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) {
try {
const data = JSON.parse(jsonSTR);
@@ -36,3 +38,28 @@ export function slugify(text: string) {
.trim()
.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 '';
}