+
+
-
- {session?.user && (
-
-
-
-
-
-
- {m.ui_label_notifications()}
-
-
-
-
-
-
System
-
- 1 hour ago
-
-
-
-
-
-
-
- {m.ui_view_all_notifications()}
-
-
-
-
- )}
-
+
{session?.user && }
>
);
diff --git a/src/components/Notification.tsx b/src/components/Notification.tsx
new file mode 100644
index 0000000..e6351e7
--- /dev/null
+++ b/src/components/Notification.tsx
@@ -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 (
+
+
+
+
+
+
+ {m.ui_label_notifications()}
+
+
+
+ {data.list && data.list.length > 0 ? (
+ data.list.map((notify) => {
+ return (
+
+ -
+
+
+ {m.templates_title_notification({
+ title: notify.title as Parameters<
+ typeof m.templates_title_notification
+ >[0]['title'],
+ })}
+
+
+ {formatTimeAgo(new Date(notify.createdAt))}
+
+
+
+
+ );
+ })
+ ) : (
+
+ {m.common_no_notify()}
+
+ )}
+
+
+
+
+
+ {m.ui_view_all_notifications()}
+
+
+
+
+
+ );
+};
+
+export default Notification;
diff --git a/src/components/Pagination.tsx b/src/components/Pagination.tsx
index 5e3bcfe..8052519 100644
--- a/src/components/Pagination.tsx
+++ b/src/components/Pagination.tsx
@@ -3,8 +3,8 @@ import {
CaretRightIcon,
DotsThreeIcon,
} from '@phosphor-icons/react';
-import { Button } from './ui/button';
-import { ButtonGroup } from './ui/button-group';
+import { Button } from '@ui/button';
+import { ButtonGroup } from '@ui/button-group';
type PaginationProps = {
currentPage: number;
@@ -20,18 +20,33 @@ const Pagination = ({
const getPageNumbers = () => {
const pages: (number | string)[] = [];
- if (totalPages <= 5) {
+ if (totalPages <= 6) {
// Hiển thị tất cả nếu trang ít
for (let i = 1; i <= totalPages; i++) {
pages.push(i);
}
} else {
if (currentPage <= 3) {
- pages.push(1, 2, 3, 'dot', totalPages);
+ pages.push(1, 2, 3, 4, 'dot', totalPages);
} else if (currentPage >= totalPages - 2) {
- pages.push(1, 'dot', totalPages - 2, totalPages - 1, totalPages);
+ pages.push(
+ 1,
+ 'dot',
+ totalPages - 3,
+ totalPages - 2,
+ totalPages - 1,
+ totalPages,
+ );
} else {
- pages.push(1, 'dot', currentPage, 'dot', totalPages);
+ pages.push(
+ 1,
+ 'dot',
+ currentPage - 1,
+ currentPage,
+ currentPage + 1,
+ 'dot',
+ totalPages,
+ );
}
}
@@ -48,6 +63,7 @@ const Pagination = ({
variant="outline"
size="icon-sm"
disabled={currentPage === 1}
+ onClick={() => onPageChange(Number(currentPage - 1))}
className="cursor-pointer"
>
@@ -61,6 +77,7 @@ const Pagination = ({
size="icon-sm"
key={idx}
disabled={true}
+ data-dot={true}
>
@@ -84,6 +101,7 @@ const Pagination = ({
variant="outline"
size="icon-sm"
disabled={currentPage === totalPages}
+ onClick={() => onPageChange(Number(currentPage + 1))}
className="cursor-pointer"
>
diff --git a/src/components/audit/action-badge.tsx b/src/components/audit/action-badge.tsx
index e2bc009..e1e715d 100644
--- a/src/components/audit/action-badge.tsx
+++ b/src/components/audit/action-badge.tsx
@@ -1,6 +1,6 @@
-import { m } from '@/paraglide/messages';
import { LOG_ACTION } from '@/types/enum';
-import { Badge } from '../ui/badge';
+import { m } from '@paraglide/messages';
+import { Badge } from '@ui/badge';
export type UserActionType = {
create: string;
diff --git a/src/components/audit/audit-columns.tsx b/src/components/audit/audit-columns.tsx
index 299991d..acba04a 100644
--- a/src/components/audit/audit-columns.tsx
+++ b/src/components/audit/audit-columns.tsx
@@ -1,11 +1,11 @@
-import { m } from '@/paraglide/messages';
-import { formatters } from '@/utils/formatters';
+import { m } from '@paraglide/messages';
import { ColumnDef } from '@tanstack/react-table';
-import { Badge } from '../ui/badge';
+import { Badge } from '@ui/badge';
+import { formatters } from '@utils/formatters';
import { LOG_ACTION } from '@/types/enum';
import ActionBadge from './action-badge';
-import ViewDetail from './view-detail-dialog';
+import ViewDetailAudit from './view-log-detail-dialog';
export const logColumns: ColumnDef
[] = [
{
@@ -56,7 +56,7 @@ export const logColumns: ColumnDef[] = [
},
cell: ({ row }) => (
-
+
),
},
diff --git a/src/components/audit/view-detail-dialog.tsx b/src/components/audit/view-log-detail-dialog.tsx
similarity index 88%
rename from src/components/audit/view-detail-dialog.tsx
rename to src/components/audit/view-log-detail-dialog.tsx
index 7606448..1718f0a 100644
--- a/src/components/audit/view-detail-dialog.tsx
+++ b/src/components/audit/view-log-detail-dialog.tsx
@@ -1,12 +1,10 @@
-import { useCopyToClipboard } from '@/hooks/use-copy-to-clipboard';
-import usePreventAutoFocus from '@/hooks/use-prevent-auto-focus';
-import { m } from '@/paraglide/messages';
import { LOG_ACTION } from '@/types/enum';
-import { formatters } from '@/utils/formatters';
-import { jsonSupport } from '@/utils/helper';
+import { useCopyToClipboard } from '@hooks/use-copy-to-clipboard';
+import usePreventAutoFocus from '@hooks/use-prevent-auto-focus';
+import { m } from '@paraglide/messages';
import { CheckIcon, CopyIcon, EyeIcon } from '@phosphor-icons/react';
-import { Badge } from '../ui/badge';
-import { Button } from '../ui/button';
+import { Badge } from '@ui/badge';
+import { Button } from '@ui/button';
import {
Dialog,
DialogContent,
@@ -14,16 +12,18 @@ import {
DialogHeader,
DialogTitle,
DialogTrigger,
-} from '../ui/dialog';
-import { Label } from '../ui/label';
-import { Tooltip, TooltipContent, TooltipTrigger } from '../ui/tooltip';
+} from '@ui/dialog';
+import { Label } from '@ui/label';
+import { Tooltip, TooltipContent, TooltipTrigger } from '@ui/tooltip';
+import { formatters } from '@utils/formatters';
+import { jsonSupport } from '@utils/helper';
import ActionBadge from './action-badge';
type ViewDetailProps = {
data: AuditWithUser;
};
-const ViewDetail = ({ data }: ViewDetailProps) => {
+const ViewDetailAudit = ({ data }: ViewDetailProps) => {
const prevent = usePreventAutoFocus();
const { isCopied, copyToClipboard } = useCopyToClipboard();
@@ -134,4 +134,4 @@ const ViewDetail = ({ data }: ViewDetailProps) => {
);
};
-export default ViewDetail;
+export default ViewDetailAudit;
diff --git a/src/components/auth/auth-provider.tsx b/src/components/auth/auth-provider.tsx
index 89c7729..bbca018 100644
--- a/src/components/auth/auth-provider.tsx
+++ b/src/components/auth/auth-provider.tsx
@@ -1,22 +1,24 @@
-import { ClientSession, useSession } from '@/lib/auth-client';
-import { BetterFetchError } from 'better-auth/client';
+import { sessionQueries } from '@/service/queries';
+import { ClientSession } from '@lib/auth-client';
+import { useQuery } from '@tanstack/react-query';
import { createContext, useContext, useMemo } from 'react';
export type UserContext = {
- data: ClientSession;
+ session: ClientSession;
isAuth: boolean;
isAdmin: boolean;
isPending: boolean;
- error: BetterFetchError | null;
+ error: Error | null;
};
const AuthContext = createContext(null);
+
export function AuthProvider({ children }: { children: React.ReactNode }) {
- const { data: session, isPending, error } = useSession();
+ const { data: session, isPending, error } = useQuery(sessionQueries.user());
const contextSession: UserContext = useMemo(
() => ({
- data: session as ClientSession,
+ session: session as ClientSession,
isPending,
error,
isAuth: !!session,
diff --git a/src/components/avatar/avatar-user.tsx b/src/components/avatar/avatar-user.tsx
index 3f535bc..f3c03fc 100644
--- a/src/components/avatar/avatar-user.tsx
+++ b/src/components/avatar/avatar-user.tsx
@@ -1,6 +1,6 @@
-import { cn } from '@/lib/utils';
+import { cn } from '@lib/utils';
+import { Avatar, AvatarFallback, AvatarImage } from '@ui/avatar';
import { useAuth } from '../auth/auth-provider';
-import { Avatar, AvatarFallback, AvatarImage } from '../ui/avatar';
import RoleRing from './role-ring';
interface AvatarUserProps {
@@ -9,7 +9,7 @@ interface AvatarUserProps {
}
const AvatarUser = ({ className, textSize = 'md' }: AvatarUserProps) => {
- const { data: session } = useAuth();
+ const { session } = useAuth();
const imagePath = session?.user?.image
? new URL(`../../../data/avatar/${session?.user?.image}`, import.meta.url)
.href
diff --git a/src/components/avatar/role-badge.tsx b/src/components/avatar/role-badge.tsx
index b316c94..b4ddc29 100644
--- a/src/components/avatar/role-badge.tsx
+++ b/src/components/avatar/role-badge.tsx
@@ -1,6 +1,7 @@
-import { m } from '@/paraglide/messages';
+import { ROLE_NAME } from '@/types/enum';
+import { m } from '@paraglide/messages';
+import { Badge, badgeVariants } from '@ui/badge';
import { VariantProps } from 'class-variance-authority';
-import { Badge, badgeVariants } from '../ui/badge';
type BadgeVariant = VariantProps['variant'];
@@ -27,7 +28,7 @@ const RoleBadge = ({ type, className }: RoleProps) => {
return (
- {m.role_tags({ role: type as string })}
+ {m.role_tags({ role: type as ROLE_NAME })}
);
};
diff --git a/src/components/avatar/role-ring.tsx b/src/components/avatar/role-ring.tsx
index c78702c..ccb2a37 100644
--- a/src/components/avatar/role-ring.tsx
+++ b/src/components/avatar/role-ring.tsx
@@ -1,4 +1,4 @@
-import { cn } from '@/lib/utils';
+import { cn } from '@lib/utils';
const RING_TYPE = {
admin: 'after:inset-ring-cyan-500',
diff --git a/src/components/form/change-password-form.tsx b/src/components/form/account/change-password-form.tsx
similarity index 59%
rename from src/components/form/change-password-form.tsx
rename to src/components/form/account/change-password-form.tsx
index 25eaa92..7833837 100644
--- a/src/components/form/change-password-form.tsx
+++ b/src/components/form/account/change-password-form.tsx
@@ -1,14 +1,12 @@
-import { useAppForm } from '@/hooks/use-app-form';
-import { authClient } from '@/lib/auth-client';
-import { m } from '@/paraglide/messages';
-import {
- ChangePassword,
- ChangePasswordFormSchema,
-} from '@/service/user.schema';
+import { useAppForm } from '@hooks/use-app-form';
+import { m } from '@paraglide/messages';
import { KeyIcon } from '@phosphor-icons/react';
+import { changePassword } from '@service/profile.api';
+import { ChangePassword, changePasswordFormSchema } from '@service/user.schema';
+import { useMutation } from '@tanstack/react-query';
+import { Card, CardContent, CardHeader, CardTitle } from '@ui/card';
+import { Field, FieldGroup } from '@ui/field';
import { toast } from 'sonner';
-import { Card, CardContent, CardHeader, CardTitle } from '../ui/card';
-import { Field, FieldGroup } from '../ui/field';
const defaultValues: ChangePassword = {
currentPassword: '',
@@ -17,37 +15,33 @@ const defaultValues: ChangePassword = {
};
const ChangePasswordForm = () => {
+ const { mutate: changePasswordMutation, isPending } = useMutation({
+ mutationFn: changePassword,
+ onSuccess: () => {
+ form.reset();
+ toast.success(m.change_password_messages_change_password_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 form = useAppForm({
defaultValues,
validators: {
- onSubmit: ChangePasswordFormSchema,
- onChange: ChangePasswordFormSchema,
+ onSubmit: changePasswordFormSchema,
+ onChange: changePasswordFormSchema,
},
onSubmit: async ({ value }) => {
- await authClient.changePassword(
- {
- newPassword: value.newPassword,
- currentPassword: value.currentPassword,
- revokeOtherSessions: true,
- },
- {
- onSuccess: () => {
- form.reset();
- toast.success(
- m.change_password_messages_change_password_success(),
- {
- richColors: true,
- },
- );
- },
- onError: (ctx) => {
- console.error(ctx.error.code);
- toast.error(m.backend_message({ code: ctx.error.code }), {
- richColors: true,
- });
- },
- },
- );
+ changePasswordMutation({ data: value });
},
});
@@ -73,6 +67,7 @@ const ChangePasswordForm = () => {
{(field) => (
)}
@@ -94,7 +89,10 @@ const ChangePasswordForm = () => {
-
+
diff --git a/src/components/form/profile-form.tsx b/src/components/form/account/profile-form.tsx
similarity index 55%
rename from src/components/form/profile-form.tsx
rename to src/components/form/account/profile-form.tsx
index fdb8d7b..dd95e18 100644
--- a/src/components/form/profile-form.tsx
+++ b/src/components/form/account/profile-form.tsx
@@ -1,18 +1,18 @@
-import { useAppForm } from '@/hooks/use-app-form';
-import { authClient } from '@/lib/auth-client';
-import { m } from '@/paraglide/messages';
-import { uploadProfileImage } from '@/service/profile.api';
-import { ProfileInput, profileUpdateSchema } from '@/service/profile.schema';
+import { Skeleton } from '@/components/ui/skeleton';
+import { updateProfile } from '@/service/profile.api';
+import { useAuth } from '@components/auth/auth-provider';
+import AvatarUser from '@components/avatar/avatar-user';
+import RoleBadge from '@components/avatar/role-badge';
+import { useAppForm } from '@hooks/use-app-form';
+import { m } from '@paraglide/messages';
import { UserCircleIcon } from '@phosphor-icons/react';
-import { useQueryClient } from '@tanstack/react-query';
+import { ProfileInput, profileUpdateSchema } from '@service/profile.schema';
+import { useMutation, useQueryClient } from '@tanstack/react-query';
+import { Card, CardContent, CardHeader, CardTitle } from '@ui/card';
+import { Field, FieldGroup, FieldLabel } from '@ui/field';
+import { Input } from '@ui/input';
import { useRef } from 'react';
import { toast } from 'sonner';
-import { useAuth } from '../auth/auth-provider';
-import AvatarUser from '../avatar/avatar-user';
-import RoleBadge from '../avatar/role-badge';
-import { Card, CardContent, CardHeader, CardTitle } from '../ui/card';
-import { Field, FieldGroup, FieldLabel } from '../ui/field';
-import { Input } from '../ui/input';
const defaultValues: ProfileInput = {
name: '',
@@ -21,9 +21,34 @@ const defaultValues: ProfileInput = {
const ProfileForm = () => {
const fileInputRef = useRef(null);
- const { data: session, isPending } = useAuth();
+ const { session, isPending } = useAuth();
const queryClient = useQueryClient();
+ const { mutate: updateProfileMutation, isPending: isRunning } = useMutation({
+ mutationFn: updateProfile,
+ onSuccess: () => {
+ form.reset();
+ if (fileInputRef.current) {
+ fileInputRef.current.value = '';
+ }
+ queryClient.refetchQueries({
+ queryKey: ['auth', 'session'],
+ });
+ toast.success(m.profile_messages_update_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 form = useAppForm({
defaultValues: {
...defaultValues,
@@ -34,52 +59,19 @@ const ProfileForm = () => {
onChange: profileUpdateSchema,
},
onSubmit: async ({ value }) => {
- try {
- let imageKey;
- if (value.image) {
- // upload image
- const formData = new FormData();
- formData.set('file', value.image);
- const { imageKey: uploadedKey } = await uploadProfileImage({
- data: formData,
- });
- imageKey = uploadedKey;
- }
-
- await authClient.updateUser(
- {
- name: value.name,
- image: imageKey,
- },
- {
- onSuccess: () => {
- form.reset();
- if (fileInputRef.current) {
- fileInputRef.current.value = '';
- }
- queryClient.refetchQueries({
- queryKey: ['auth', 'session'],
- });
- toast.success(m.profile_messages_update_success(), {
- richColors: true,
- });
- },
- onError: (ctx) => {
- console.error(ctx.error.code);
- toast.error(m.backend_message({ code: ctx.error.code }), {
- richColors: true,
- });
- },
- },
- );
- } catch (error) {
- console.error('update load file', error);
+ const formData = new FormData();
+ formData.set('name', value.name);
+ if (value.image) {
+ formData.set('file', value.image);
}
+
+ updateProfileMutation({ data: formData });
},
});
- if (isPending) return null;
- if (!session?.user?.name) return null;
+ if (isPending || !session?.user?.name) {
+ return ;
+ }
return (
@@ -133,7 +125,10 @@ const ProfileForm = () => {
-
+
diff --git a/src/components/form/user-settings-form.tsx b/src/components/form/account/user-settings-form.tsx
similarity index 73%
rename from src/components/form/user-settings-form.tsx
rename to src/components/form/account/user-settings-form.tsx
index 4e3c314..c15b014 100644
--- a/src/components/form/user-settings-form.tsx
+++ b/src/components/form/account/user-settings-form.tsx
@@ -1,17 +1,16 @@
-import { useAppForm } from '@/hooks/use-app-form';
-import { m } from '@/paraglide/messages';
-import { Locale, setLocale } from '@/paraglide/runtime';
-import { settingQueries } from '@/service/queries';
-import { updateUserSettings } from '@/service/setting.api';
-import { UserSettingInput, userSettingSchema } from '@/service/setting.schema';
-import { ReturnError } from '@/types/common';
+import { useAppForm } from '@hooks/use-app-form';
+import { m } from '@paraglide/messages';
+import { Locale, setLocale } from '@paraglide/runtime';
import { GearIcon } from '@phosphor-icons/react';
+import { settingQueries } from '@service/queries';
+import { updateUserSettings } from '@service/setting.api';
+import { UserSettingInput, userSettingSchema } from '@service/setting.schema';
import { useMutation, useQuery, useQueryClient } from '@tanstack/react-query';
+import { Card, CardContent, CardHeader, CardTitle } from '@ui/card';
+import { Field, FieldGroup } from '@ui/field';
+import { Skeleton } from '@ui/skeleton';
import { useEffect } from 'react';
import { toast } from 'sonner';
-import { Card, CardContent, CardHeader, CardTitle } from '../ui/card';
-import { Field, FieldGroup } from '../ui/field';
-import { Skeleton } from '../ui/skeleton';
const defaultValues: UserSettingInput = {
language: '',
@@ -22,7 +21,7 @@ const UserSettingsForm = () => {
const { data, isLoading } = useQuery(settingQueries.listUser());
- const updateMutation = useMutation({
+ const { mutate: updateMutation, isPending } = useMutation({
mutationFn: updateUserSettings,
onSuccess: (_, variables) => {
setLocale(variables.data.language as Locale);
@@ -35,7 +34,10 @@ const UserSettingsForm = () => {
},
onError: (error: ReturnError) => {
console.error(error);
- toast.error(m.backend_message({ code: error.code }), {
+ const code = error.code as Parameters<
+ typeof m.backend_message
+ >[0]['code'];
+ toast.error(m.backend_message({ code }), {
richColors: true,
});
},
@@ -48,7 +50,7 @@ const UserSettingsForm = () => {
onChange: userSettingSchema,
},
onSubmit: ({ value }) => {
- updateMutation.mutate({ data: value as UserSettingInput });
+ updateMutation({ data: value as UserSettingInput });
},
});
@@ -99,7 +101,10 @@ const UserSettingsForm = () => {
-
+
diff --git a/src/components/form/form-components.tsx b/src/components/form/form-components.tsx
index a0b046e..6280f36 100644
--- a/src/components/form/form-components.tsx
+++ b/src/components/form/form-components.tsx
@@ -1,27 +1,37 @@
-import { useFieldContext, useFormContext } from '@/hooks/use-app-form';
-import { RoleEnum } from '@/service/user.schema';
+import { useFieldContext, useFormContext } from '@hooks/use-app-form';
import { useStore } from '@tanstack/react-form';
+import { Button, buttonVariants } from '@ui/button';
+import { Field, FieldError, FieldLabel } from '@ui/field';
+import { Input } from '@ui/input';
+import * as ShadcnSelect from '@ui/select';
+import { SelectUser as SelectUserUI } from '@ui/select-user';
+import { Textarea } from '@ui/textarea';
import { type VariantProps } from 'class-variance-authority';
-import { Button, buttonVariants } from '../ui/button';
-import { Field, FieldError, FieldLabel } from '../ui/field';
-import { Input } from '../ui/input';
-import * as ShadcnSelect from '../ui/select';
-import { Textarea } from '../ui/textarea';
+import { Spinner } from '../ui/spinner';
export function SubscribeButton({
label,
variant = 'default',
+ disabled = false,
}: {
label: string;
+ disabled?: boolean;
} & VariantProps) {
const form = useFormContext();
return (
state.isSubmitting}>
- {(isSubmitting) => (
-
- )}
+ {(isSubmitting) => {
+ return (
+
+ );
+ }}
);
}
@@ -135,12 +145,11 @@ export function Select({
label,
values,
placeholder,
- isRole = false,
+ // isRole = false,
}: {
label: string;
values: Array<{ label: string; value: string }>;
placeholder?: string;
- isRole?: boolean;
}) {
const field = useFieldContext();
const errors = useStore(field.store, (state) => state.meta.errors);
@@ -152,11 +161,7 @@ export function Select({
- isRole
- ? field.handleChange(RoleEnum.parse(value))
- : field.handleChange(value)
- }
+ onValueChange={(value) => field.handleChange(value)}
>
@@ -216,3 +221,46 @@ export function SelectNumber({
);
}
+
+export function SelectUser({
+ label,
+ values,
+ placeholder,
+ keyword,
+ onKeywordChange,
+ searchPlaceholder = 'Tìm theo tên hoặc email...',
+ selectKey = 'id',
+}: {
+ label: string;
+ values: Array<{ id: string; name: string; email: string }>;
+ placeholder?: string;
+ /** Khi truyền cùng onKeywordChange: tìm kiếm theo API (keyword gửi lên server) */
+ keyword?: string;
+ onKeywordChange?: (value: string) => void;
+ searchPlaceholder?: string;
+ selectKey?: 'id' | 'email';
+}) {
+ const field = useFieldContext();
+ const errors = useStore(field.store, (state) => state.meta.errors);
+ const isInvalid = field.state.meta.isTouched && !field.state.meta.isValid;
+
+ return (
+
+ {label}:
+ field.handleChange(userId)}
+ values={values}
+ placeholder={placeholder}
+ keyword={keyword}
+ onKeywordChange={onKeywordChange}
+ searchPlaceholder={searchPlaceholder}
+ aria-invalid={isInvalid}
+ selectKey={selectKey}
+ />
+ {isInvalid && }
+
+ );
+}
diff --git a/src/components/form/house/admin-create-house-form.tsx b/src/components/form/house/admin-create-house-form.tsx
new file mode 100644
index 0000000..e345f55
--- /dev/null
+++ b/src/components/form/house/admin-create-house-form.tsx
@@ -0,0 +1,141 @@
+import { useAuth } from '@/components/auth/auth-provider';
+import { useAppForm } from '@hooks/use-app-form';
+import useDebounced from '@hooks/use-debounced';
+import { authClient } from '@lib/auth-client';
+import { m } from '@paraglide/messages';
+import { createHouse } from '@service/house.api';
+import { houseCreateSchema } from '@service/house.schema';
+import { housesQueries, usersQueries } from '@service/queries';
+import { uuid } from '@tanstack/react-form';
+import { useMutation, useQuery, useQueryClient } from '@tanstack/react-query';
+import { Button } from '@ui/button';
+import { DialogClose, DialogFooter } from '@ui/dialog';
+import { Field, FieldGroup } from '@ui/field';
+import { slugify } from '@utils/helper';
+import { useEffect, useState } from 'react';
+import { toast } from 'sonner';
+
+type FormProps = {
+ onSubmit: (open: boolean) => void;
+ isPersonal?: boolean;
+};
+
+const CreateNewHouseForm = ({ onSubmit, isPersonal = false }: FormProps) => {
+ const { session } = useAuth();
+ const [userKeyword, setUserKeyword] = useState('');
+ const debouncedUserKeyword = useDebounced(userKeyword, 300);
+ const { data: users } = useQuery(
+ usersQueries.select({ keyword: debouncedUserKeyword }),
+ );
+
+ const queryClient = useQueryClient();
+
+ const queryKey = isPersonal ? 'currentUser' : 'list';
+
+ const { mutate: createHouseMutation, isPending } = useMutation({
+ mutationFn: createHouse,
+ onSuccess: () => {
+ queryClient.invalidateQueries({
+ queryKey: [...housesQueries.all, queryKey],
+ });
+ onSubmit(false);
+ toast.success(m.houses_page_message_create_house_success(), {
+ richColors: true,
+ });
+ },
+ onError: (error: ReturnError) => {
+ console.error(error);
+ const code = error.code as Parameters<
+ typeof m.backend_message
+ >[0]['code'];
+ toast.error(m.backend_message({ code }), {
+ richColors: true,
+ });
+ },
+ });
+
+ const form = useAppForm({
+ defaultValues: {
+ name: '',
+ userId: '',
+ color: '#000000',
+ },
+ validators: {
+ onChange: houseCreateSchema,
+ onSubmit: houseCreateSchema,
+ },
+ onSubmit: async ({ value }) => {
+ const slug = `${slugify(value.name)}-${uuid().slice(0, 5)}`;
+ const { data, error } = await authClient.organization.checkSlug({
+ slug,
+ });
+ if (error) {
+ toast.error(error.message, {
+ richColors: true,
+ });
+ }
+ if (data?.status) {
+ createHouseMutation({ data: { ...value, slug } });
+ }
+ },
+ });
+
+ useEffect(() => {
+ if (isPersonal) {
+ form.setFieldValue('userId', session.user.id);
+ }
+ }, []);
+
+ return (
+
+ );
+};
+
+export default CreateNewHouseForm;
diff --git a/src/components/form/house/admin-edit-house-form.tsx b/src/components/form/house/admin-edit-house-form.tsx
new file mode 100644
index 0000000..8cc8354
--- /dev/null
+++ b/src/components/form/house/admin-edit-house-form.tsx
@@ -0,0 +1,121 @@
+import { useAppForm } from '@hooks/use-app-form';
+import { authClient } from '@lib/auth-client';
+import { m } from '@paraglide/messages';
+import { updateHouse } from '@service/house.api';
+import { houseEditSchema } from '@service/house.schema';
+import { housesQueries } from '@service/queries';
+import { uuid } from '@tanstack/react-form';
+import { useMutation, useQueryClient } from '@tanstack/react-query';
+import { Button } from '@ui/button';
+import { DialogClose, DialogFooter } from '@ui/dialog';
+import { Field, FieldGroup } from '@ui/field';
+import { slugify } from '@utils/helper';
+import { toast } from 'sonner';
+
+type EditHouseFormProps = {
+ data: HouseWithMembers;
+ onSubmit: (open: boolean) => void;
+ mutateKey: string;
+};
+
+const EditHouseForm = ({ data, onSubmit, mutateKey }: EditHouseFormProps) => {
+ const { refetch } = authClient.useActiveOrganization();
+ const queryClient = useQueryClient();
+
+ const { mutate: updateHouseMutation, isPending } = useMutation({
+ mutationFn: updateHouse,
+ onSuccess: () => {
+ queryClient.invalidateQueries({
+ queryKey: [...housesQueries.all, mutateKey],
+ });
+ onSubmit(false);
+ refetch();
+ toast.success(m.houses_page_message_update_house_success(), {
+ richColors: true,
+ });
+ },
+ onError: (error: ReturnError) => {
+ console.error(error);
+ const code = error.code as Parameters<
+ typeof m.backend_message
+ >[0]['code'];
+ toast.error(m.backend_message({ code }), {
+ richColors: true,
+ });
+ },
+ });
+
+ const form = useAppForm({
+ defaultValues: {
+ id: data.id,
+ name: data.name,
+ color: data.color || '#000000',
+ },
+ validators: {
+ onChange: houseEditSchema,
+ onSubmit: houseEditSchema,
+ },
+ onSubmit: async ({ value }) => {
+ const slug = `${slugify(value.name)}-${uuid().slice(0, 5)}`;
+ const { data, error } = await authClient.organization.checkSlug({
+ slug,
+ });
+ if (error) {
+ toast.error(error.message, {
+ richColors: true,
+ });
+ }
+ if (data?.status) {
+ updateHouseMutation({ data: { ...value, slug } });
+ }
+ },
+ });
+
+ return (
+
+ );
+};
+
+export default EditHouseForm;
diff --git a/src/components/form/house/user-invite-member-form.tsx b/src/components/form/house/user-invite-member-form.tsx
new file mode 100644
index 0000000..c9891fd
--- /dev/null
+++ b/src/components/form/house/user-invite-member-form.tsx
@@ -0,0 +1,132 @@
+import { Button } from '@/components/ui/button';
+import { DialogClose, DialogFooter } from '@/components/ui/dialog';
+import { useAppForm } from '@/hooks/use-app-form';
+import useDebounced from '@/hooks/use-debounced';
+import { authClient } from '@/lib/auth-client';
+import { m } from '@/paraglide/messages';
+import { invitationMember } from '@/service/house.api';
+import {
+ invitationCreateBESchema,
+ invitationCreateFESchema,
+} from '@/service/house.schema';
+import { housesQueries, usersQueries } from '@/service/queries';
+import { useMutation, useQuery, useQueryClient } from '@tanstack/react-query';
+import { Field, FieldGroup, FieldLabel } from '@ui/field';
+import { useState } from 'react';
+import { toast } from 'sonner';
+
+type FormProps = {
+ onSubmit: (open: boolean) => void;
+};
+
+const UserInviteMemberForm = ({ onSubmit }: FormProps) => {
+ const { data: activeHouse, refetch } = authClient.useActiveOrganization();
+ const [userKeyword, setUserKeyword] = useState('');
+ const debouncedUserKeyword = useDebounced(userKeyword, 300);
+ const { data: users } = useQuery(
+ usersQueries.select({ keyword: debouncedUserKeyword }, true),
+ );
+ const queryClient = useQueryClient();
+
+ if (!activeHouse) return null;
+
+ const { mutate: invitationMemberMutation, isPending } = useMutation({
+ mutationFn: invitationMember,
+ onSuccess: () => {
+ queryClient.invalidateQueries({
+ queryKey: [...housesQueries.all, 'currentUser'],
+ });
+ onSubmit(false);
+ refetch();
+ toast.success(m.houses_page_message_invite_member_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 form = useAppForm({
+ defaultValues: {
+ email: '',
+ houseId: activeHouse.id,
+ role: '',
+ },
+ validators: {
+ onChange: invitationCreateFESchema,
+ onSubmit: invitationCreateFESchema,
+ },
+ onSubmit: async ({ value }) => {
+ invitationMemberMutation({ data: invitationCreateBESchema.parse(value) });
+ },
+ });
+ return (
+
+ );
+};
+
+export default UserInviteMemberForm;
diff --git a/src/components/form/settings-form.tsx b/src/components/form/settings-form.tsx
index 11e7e70..a42f10d 100644
--- a/src/components/form/settings-form.tsx
+++ b/src/components/form/settings-form.tsx
@@ -1,15 +1,14 @@
-import { useAppForm } from '@/hooks/use-app-form';
-import { m } from '@/paraglide/messages';
-import { settingQueries } from '@/service/queries';
-import { updateAdminSettings } from '@/service/setting.api';
-import { settingSchema, SettingsInput } from '@/service/setting.schema';
-import { ReturnError } from '@/types/common';
+import { useAppForm } from '@hooks/use-app-form';
+import { m } from '@paraglide/messages';
import { GearIcon } from '@phosphor-icons/react';
+import { settingQueries } from '@service/queries';
+import { updateAdminSettings } from '@service/setting.api';
+import { settingSchema, SettingsInput } from '@service/setting.schema';
import { useMutation, useQuery, useQueryClient } from '@tanstack/react-query';
+import { Card, CardContent, CardHeader, CardTitle } from '@ui/card';
+import { Field, FieldGroup } from '@ui/field';
+import { Skeleton } from '@ui/skeleton';
import { toast } from 'sonner';
-import { Card, CardContent, CardHeader, CardTitle } from '../ui/card';
-import { Field, FieldGroup } from '../ui/field';
-import { Skeleton } from '../ui/skeleton';
const defaultValues: SettingsInput = {
site_name: '',
@@ -22,7 +21,7 @@ const SettingsForm = () => {
const { data: settings, isLoading } = useQuery(settingQueries.listAdmin());
- const updateMutation = useMutation({
+ const { mutate: updateMutation, isPending } = useMutation({
mutationFn: updateAdminSettings,
onSuccess: () => {
queryClient.invalidateQueries(settingQueries.listAdmin());
@@ -32,9 +31,10 @@ const SettingsForm = () => {
},
onError: (error: ReturnError) => {
console.error(error);
- toast.error(m.backend_message({ code: error.code }), {
- richColors: true,
- });
+ const code = error.code as Parameters<
+ typeof m.backend_message
+ >[0]['code'];
+ toast.error(m.backend_message({ code }), { richColors: true });
},
});
@@ -50,7 +50,7 @@ const SettingsForm = () => {
onChange: settingSchema,
},
onSubmit: async ({ value }) => {
- updateMutation.mutate({ data: value as SettingsInput });
+ updateMutation({ data: value as SettingsInput });
},
});
@@ -88,7 +88,10 @@ const SettingsForm = () => {
-
+
diff --git a/src/components/form/signin-form.tsx b/src/components/form/signin-form.tsx
index 115af79..13c8cb5 100644
--- a/src/components/form/signin-form.tsx
+++ b/src/components/form/signin-form.tsx
@@ -1,13 +1,13 @@
-import { useAppForm } from '@/hooks/use-app-form';
-import { authClient } from '@/lib/auth-client';
-import { m } from '@/paraglide/messages';
+import { useAppForm } from '@hooks/use-app-form';
+import { authClient } from '@lib/auth-client';
+import { m } from '@paraglide/messages';
import { useQueryClient } from '@tanstack/react-query';
import { createLink, useNavigate } from '@tanstack/react-router';
+import { Button } from '@ui/button';
+import { Card, CardContent, CardHeader, CardTitle } from '@ui/card';
+import { Field, FieldGroup } from '@ui/field';
import { toast } from 'sonner';
import z from 'zod';
-import { Button } from '../ui/button';
-import { Card, CardContent, CardHeader, CardTitle } from '../ui/card';
-import { Field, FieldGroup } from '../ui/field';
const SignInFormSchema = z.object({
email: z
diff --git a/src/components/form/signup-form.tsx b/src/components/form/signup-form.tsx
index ca6a3be..6c23681 100644
--- a/src/components/form/signup-form.tsx
+++ b/src/components/form/signup-form.tsx
@@ -1,15 +1,15 @@
-import { m } from '@/paraglide/messages';
+import { m } from '@paraglide/messages';
import { createLink, Link } from '@tanstack/react-router';
-import { Button } from '../ui/button';
+import { Button } from '@ui/button';
import {
Card,
CardContent,
CardDescription,
CardHeader,
CardTitle,
-} from '../ui/card';
-import { Field, FieldDescription, FieldGroup, FieldLabel } from '../ui/field';
-import { Input } from '../ui/input';
+} from '@ui/card';
+import { Field, FieldDescription, FieldGroup, FieldLabel } from '@ui/field';
+import { Input } from '@ui/input';
const ButtonLink = createLink(Button);
diff --git a/src/components/form/admin-ban-user-form.tsx b/src/components/form/user/admin-ban-user-form.tsx
similarity index 86%
rename from src/components/form/admin-ban-user-form.tsx
rename to src/components/form/user/admin-ban-user-form.tsx
index 1d09900..db7cbfc 100644
--- a/src/components/form/admin-ban-user-form.tsx
+++ b/src/components/form/user/admin-ban-user-form.tsx
@@ -1,13 +1,13 @@
-import { useAppForm } from '@/hooks/use-app-form';
-import { m } from '@/paraglide/messages';
-import { userBanSchema } from '@/service/user.schema';
+import { useBanContext } from '@components/user/ban-user-dialog';
+import { useAppForm } from '@hooks/use-app-form';
+import { m } from '@paraglide/messages';
import { WarningIcon } from '@phosphor-icons/react';
+import { userBanSchema } from '@service/user.schema';
+import { Alert, AlertDescription, AlertTitle } from '@ui/alert';
+import { Button } from '@ui/button';
+import { DialogClose, DialogFooter } from '@ui/dialog';
+import { Field, FieldGroup } from '@ui/field';
import { UserWithRole } from 'better-auth/plugins';
-import { Alert, AlertDescription, AlertTitle } from '../ui/alert';
-import { Button } from '../ui/button';
-import { DialogClose, DialogFooter } from '../ui/dialog';
-import { Field, FieldGroup } from '../ui/field';
-import { useBanContext } from '../user/ban-user-dialog';
type FormProps = {
data: UserWithRole;
diff --git a/src/components/form/admin-create-user-form.tsx b/src/components/form/user/admin-create-user-form.tsx
similarity index 72%
rename from src/components/form/admin-create-user-form.tsx
rename to src/components/form/user/admin-create-user-form.tsx
index 9343b54..d3f8180 100644
--- a/src/components/form/admin-create-user-form.tsx
+++ b/src/components/form/user/admin-create-user-form.tsx
@@ -1,14 +1,13 @@
-import { useAppForm } from '@/hooks/use-app-form';
-import { m } from '@/paraglide/messages';
-import { usersQueries } from '@/service/queries';
-import { createUser } from '@/service/user.api';
-import { userCreateSchema } from '@/service/user.schema';
-import { ReturnError } from '@/types/common';
+import { useAppForm } from '@hooks/use-app-form';
+import { m } from '@paraglide/messages';
+import { usersQueries } from '@service/queries';
+import { createUser } from '@service/user.api';
+import { userCreateBESchema, userCreateFESchema } from '@service/user.schema';
import { useMutation, useQueryClient } from '@tanstack/react-query';
+import { Button } from '@ui/button';
+import { DialogClose, DialogFooter } from '@ui/dialog';
+import { Field, FieldGroup } from '@ui/field';
import { toast } from 'sonner';
-import { Button } from '../ui/button';
-import { DialogClose, DialogFooter } from '../ui/dialog';
-import { Field, FieldGroup } from '../ui/field';
type FormProps = {
onSubmit: (open: boolean) => void;
@@ -17,7 +16,7 @@ type FormProps = {
const AdminCreateUserForm = ({ onSubmit }: FormProps) => {
const queryClient = useQueryClient();
- const { mutate: createUserMutation } = useMutation({
+ const { mutate: createUserMutation, isPending } = useMutation({
mutationFn: createUser,
onSuccess: () => {
queryClient.invalidateQueries({
@@ -30,7 +29,10 @@ const AdminCreateUserForm = ({ onSubmit }: FormProps) => {
},
onError: (error: ReturnError) => {
console.error(error);
- toast.error(m.backend_message({ code: error.code }), {
+ const code = error.code as Parameters<
+ typeof m.backend_message
+ >[0]['code'];
+ toast.error(m.backend_message({ code }), {
richColors: true,
});
},
@@ -44,11 +46,11 @@ const AdminCreateUserForm = ({ onSubmit }: FormProps) => {
role: '',
},
validators: {
- onChange: userCreateSchema,
- onSubmit: userCreateSchema,
+ onChange: userCreateFESchema,
+ onSubmit: userCreateFESchema,
},
onSubmit: ({ value }) => {
- createUserMutation({ data: userCreateSchema.parse(value) });
+ createUserMutation({ data: userCreateBESchema.parse(value) });
},
});
@@ -81,7 +83,6 @@ const AdminCreateUserForm = ({ onSubmit }: FormProps) => {
{
-
+
diff --git a/src/components/form/admin-set-password-form.tsx b/src/components/form/user/admin-set-password-form.tsx
similarity index 65%
rename from src/components/form/admin-set-password-form.tsx
rename to src/components/form/user/admin-set-password-form.tsx
index a0a3f4c..263a07c 100644
--- a/src/components/form/admin-set-password-form.tsx
+++ b/src/components/form/user/admin-set-password-form.tsx
@@ -1,15 +1,14 @@
-import { useAppForm } from '@/hooks/use-app-form';
-import { m } from '@/paraglide/messages';
-import { usersQueries } from '@/service/queries';
-import { setUserPassword } from '@/service/user.api';
-import { userSetPasswordSchema } from '@/service/user.schema';
-import { ReturnError } from '@/types/common';
+import { Button } from '@/components/ui/button';
+import { useAppForm } from '@hooks/use-app-form';
+import { m } from '@paraglide/messages';
+import { usersQueries } from '@service/queries';
+import { setUserPassword } from '@service/user.api';
+import { userSetPasswordSchema } from '@service/user.schema';
import { useMutation, useQueryClient } from '@tanstack/react-query';
+import { DialogClose, DialogFooter } from '@ui/dialog';
+import { Field, FieldGroup } from '@ui/field';
import { UserWithRole } from 'better-auth/plugins';
import { toast } from 'sonner';
-import { Button } from '../ui/button';
-import { DialogClose, DialogFooter } from '../ui/dialog';
-import { Field, FieldGroup } from '../ui/field';
type FormProps = {
data: UserWithRole;
@@ -19,7 +18,7 @@ type FormProps = {
const AdminSetPasswordForm = ({ data, onSubmit }: FormProps) => {
const queryClient = useQueryClient();
- const setUserPasswordMutation = useMutation({
+ const { mutate: setUserPasswordMutation, isPending } = useMutation({
mutationFn: setUserPassword,
onSuccess: () => {
queryClient.invalidateQueries({
@@ -32,7 +31,10 @@ const AdminSetPasswordForm = ({ data, onSubmit }: FormProps) => {
},
onError: (error: ReturnError) => {
console.error(error);
- toast.error(m.backend_message({ code: error.code }), {
+ const code = error.code as Parameters<
+ typeof m.backend_message
+ >[0]['code'];
+ toast.error(m.backend_message({ code }), {
richColors: true,
});
},
@@ -47,7 +49,7 @@ const AdminSetPasswordForm = ({ data, onSubmit }: FormProps) => {
onSubmit: userSetPasswordSchema,
},
onSubmit: async ({ value }) => {
- setUserPasswordMutation.mutate({ data: value });
+ setUserPasswordMutation({ data: value });
},
});
@@ -66,7 +68,10 @@ const AdminSetPasswordForm = ({ data, onSubmit }: FormProps) => {
{(field) => (
-
+
)}
@@ -77,7 +82,10 @@ const AdminSetPasswordForm = ({ data, onSubmit }: FormProps) => {
-
+
diff --git a/src/components/form/admin-set-user-role-form.tsx b/src/components/form/user/admin-set-user-role-form.tsx
similarity index 72%
rename from src/components/form/admin-set-user-role-form.tsx
rename to src/components/form/user/admin-set-user-role-form.tsx
index 00c3094..5f81783 100644
--- a/src/components/form/admin-set-user-role-form.tsx
+++ b/src/components/form/user/admin-set-user-role-form.tsx
@@ -1,15 +1,17 @@
-import { useAppForm } from '@/hooks/use-app-form';
-import { m } from '@/paraglide/messages';
-import { usersQueries } from '@/service/queries';
-import { setUserRole } from '@/service/user.api';
-import { userUpdateRoleSchema } from '@/service/user.schema';
-import { ReturnError } from '@/types/common';
+import { useAppForm } from '@hooks/use-app-form';
+import { m } from '@paraglide/messages';
+import { usersQueries } from '@service/queries';
+import { setUserRole } from '@service/user.api';
+import {
+ userUpdateRoleBESchema,
+ userUpdateRoleSchema,
+} from '@service/user.schema';
import { useMutation, useQueryClient } from '@tanstack/react-query';
+import { Button } from '@ui/button';
+import { DialogClose, DialogFooter } from '@ui/dialog';
+import { Field, FieldGroup } from '@ui/field';
import { UserWithRole } from 'better-auth/plugins';
import { toast } from 'sonner';
-import { Button } from '../ui/button';
-import { DialogClose, DialogFooter } from '../ui/dialog';
-import { Field, FieldGroup } from '../ui/field';
type SetRoleFormProps = {
data: UserWithRole;
@@ -24,7 +26,7 @@ const AdminSetUserRoleForm = ({ data, onSubmit }: SetRoleFormProps) => {
role: data.role,
};
- const updateRoleMutation = useMutation({
+ const { mutate: updateRoleMutation, isPending } = useMutation({
mutationFn: setUserRole,
onSuccess: () => {
queryClient.refetchQueries({
@@ -37,7 +39,10 @@ const AdminSetUserRoleForm = ({ data, onSubmit }: SetRoleFormProps) => {
},
onError: (error: ReturnError) => {
console.error(error);
- toast.error(m.backend_message({ code: error.code }), {
+ const code = error.code as Parameters<
+ typeof m.backend_message
+ >[0]['code'];
+ toast.error(m.backend_message({ code }), {
richColors: true,
});
},
@@ -50,7 +55,7 @@ const AdminSetUserRoleForm = ({ data, onSubmit }: SetRoleFormProps) => {
onSubmit: userUpdateRoleSchema,
},
onSubmit: async ({ value }) => {
- updateRoleMutation.mutate({ data: value });
+ updateRoleMutation({ data: userUpdateRoleBESchema.parse(value) });
},
});
@@ -87,7 +92,10 @@ const AdminSetUserRoleForm = ({ data, onSubmit }: SetRoleFormProps) => {
-
+
diff --git a/src/components/form/admin-update-user-info-form.tsx b/src/components/form/user/admin-update-user-info-form.tsx
similarity index 69%
rename from src/components/form/admin-update-user-info-form.tsx
rename to src/components/form/user/admin-update-user-info-form.tsx
index e16b08f..dcc6069 100644
--- a/src/components/form/admin-update-user-info-form.tsx
+++ b/src/components/form/user/admin-update-user-info-form.tsx
@@ -1,15 +1,14 @@
-import { useAppForm } from '@/hooks/use-app-form';
-import { m } from '@/paraglide/messages';
-import { usersQueries } from '@/service/queries';
-import { updateUserInformation } from '@/service/user.api';
-import { userUpdateInfoSchema } from '@/service/user.schema';
-import { ReturnError } from '@/types/common';
+import { useAppForm } from '@hooks/use-app-form';
+import { m } from '@paraglide/messages';
+import { usersQueries } from '@service/queries';
+import { updateUserInformation } from '@service/user.api';
+import { userUpdateInfoSchema } from '@service/user.schema';
import { useMutation, useQueryClient } from '@tanstack/react-query';
+import { Button } from '@ui/button';
+import { DialogClose, DialogFooter } from '@ui/dialog';
+import { Field, FieldGroup } from '@ui/field';
import { UserWithRole } from 'better-auth/plugins';
import { toast } from 'sonner';
-import { Button } from '../ui/button';
-import { DialogClose, DialogFooter } from '../ui/dialog';
-import { Field, FieldGroup } from '../ui/field';
type UpdateUserFormProps = {
data: UserWithRole;
@@ -19,7 +18,7 @@ type UpdateUserFormProps = {
const AdminUpdateUserInfoForm = ({ data, onSubmit }: UpdateUserFormProps) => {
const queryClient = useQueryClient();
- const updateUserMutation = useMutation({
+ const { mutate: updateUserMutation, isPending } = useMutation({
mutationFn: updateUserInformation,
onSuccess: () => {
queryClient.refetchQueries({
@@ -32,7 +31,10 @@ const AdminUpdateUserInfoForm = ({ data, onSubmit }: UpdateUserFormProps) => {
},
onError: (error: ReturnError) => {
console.error(error);
- toast.error(m.backend_message({ code: error.code }), {
+ const code = error.code as Parameters<
+ typeof m.backend_message
+ >[0]['code'];
+ toast.error(m.backend_message({ code }), {
richColors: true,
});
},
@@ -46,7 +48,7 @@ const AdminUpdateUserInfoForm = ({ data, onSubmit }: UpdateUserFormProps) => {
onChange: userUpdateInfoSchema,
},
onSubmit: async ({ value }) => {
- updateUserMutation.mutate({ data: value });
+ updateUserMutation({ data: value });
},
});
@@ -74,7 +76,10 @@ const AdminUpdateUserInfoForm = ({ data, onSubmit }: UpdateUserFormProps) => {
-
+
diff --git a/src/components/house/create-house-dialog.tsx b/src/components/house/create-house-dialog.tsx
new file mode 100644
index 0000000..ac8d09e
--- /dev/null
+++ b/src/components/house/create-house-dialog.tsx
@@ -0,0 +1,66 @@
+import CreateNewHouseForm from '@form/house/admin-create-house-form';
+import useHasPermission from '@hooks/use-has-permission';
+import usePreventAutoFocus from '@hooks/use-prevent-auto-focus';
+import { cn } from '@lib/utils';
+import { m } from '@paraglide/messages';
+import { PlusIcon } from '@phosphor-icons/react';
+import { Button } from '@ui/button';
+import {
+ Dialog,
+ DialogContent,
+ DialogDescription,
+ DialogHeader,
+ DialogTitle,
+ DialogTrigger,
+} from '@ui/dialog';
+import { useState } from 'react';
+import { Skeleton } from '../ui/skeleton';
+
+type CreateNewHouseProp = {
+ isPersonal?: boolean;
+ className?: string;
+};
+
+const CreateNewHouse = ({
+ className,
+ isPersonal = false,
+}: CreateNewHouseProp) => {
+ const { hasPermission, isLoading } = useHasPermission('house', 'create');
+ const [_open, _setOpen] = useState(false);
+ const prevent = usePreventAutoFocus();
+
+ if (isLoading) {
+ return ;
+ }
+
+ if (!hasPermission) return null;
+
+ return (
+
+ );
+};
+
+export default CreateNewHouse;
diff --git a/src/components/house/current-user-action-group.tsx b/src/components/house/current-user-action-group.tsx
new file mode 100644
index 0000000..dd26d6a
--- /dev/null
+++ b/src/components/house/current-user-action-group.tsx
@@ -0,0 +1,40 @@
+import { authClient } from '@lib/auth-client';
+import { m } from '@paraglide/messages';
+import { GearIcon } from '@phosphor-icons/react';
+import { Card, CardContent, CardHeader, CardTitle } from '@ui/card';
+import { Skeleton } from '../ui/skeleton';
+import DeleteUserHouseAction from './delete-user-house-dialog';
+import EditUserHouseAction from './edit-user-house-dialog';
+import LeaveHouseAction from './leave-house-dialog';
+
+type CurrentUserActionGroupProps = {
+ oneHouse: boolean;
+ activeHouse: ReturnType['data'];
+};
+
+const CurrentUserActionGroup = ({
+ oneHouse,
+ activeHouse,
+}: CurrentUserActionGroupProps) => {
+ if (!activeHouse) {
+ return ;
+ }
+
+ return (
+
+
+
+
+ {m.houses_user_page_block_action_title()}
+
+
+
+
+ {!oneHouse && }
+
+
+
+ );
+};
+
+export default CurrentUserActionGroup;
diff --git a/src/components/house/current-user-house-list.tsx b/src/components/house/current-user-house-list.tsx
new file mode 100644
index 0000000..88fa624
--- /dev/null
+++ b/src/components/house/current-user-house-list.tsx
@@ -0,0 +1,132 @@
+import { authClient } from '@lib/auth-client';
+import { cn } from '@lib/utils';
+import { m } from '@paraglide/messages';
+import { CheckIcon, WarehouseIcon } from '@phosphor-icons/react';
+import { Button } from '@ui/button';
+import { Card, CardContent, CardHeader, CardTitle } from '@ui/card';
+import {
+ Item,
+ ItemActions,
+ ItemContent,
+ ItemDescription,
+ ItemTitle,
+} from '@ui/item';
+import { ScrollArea, ScrollBar } from '@ui/scroll-area';
+import { Skeleton } from '@ui/skeleton';
+import parse from 'html-react-parser';
+import { toast } from 'sonner';
+import CreateNewHouse from './create-house-dialog';
+
+type CurrentUserHouseListProps = {
+ houses: HouseWithMembersCount[];
+ activeHouse: ReturnType['data'];
+};
+
+const CurrentUserHouseList = ({
+ activeHouse,
+ houses,
+}: CurrentUserHouseListProps) => {
+ const activeHouseAction = async ({
+ id,
+ slug,
+ }: {
+ id: string;
+ slug: string;
+ }) => {
+ const { data, error } = await authClient.organization.setActive({
+ organizationId: id,
+ organizationSlug: slug,
+ });
+
+ if (error) {
+ toast.error(error.message, { richColors: true });
+ }
+
+ if (data) {
+ toast.success(
+ parse(
+ m.houses_user_page_message_active_house_success({ house: data.name }),
+ ),
+ {
+ richColors: true,
+ },
+ );
+ }
+ };
+
+ if (!activeHouse || !houses) {
+ return ;
+ }
+
+ return (
+
+
+
+
+ {m.houses_page_ui_title()}
+
+
+
+
+
+
+ {houses.map((house) => {
+ const isActive = house.id === activeHouse.id;
+
+ return (
+ -
+
+
+ {house.name}
+
+
+ {m.houses_page_ui_table_header_members()}
+ :
+ {house._count.members}
+
+
+
+
+
+
+ );
+ })}
+
+
+
+
+
+ );
+};
+
+export default CurrentUserHouseList;
diff --git a/src/components/house/current-user-invitation-list.tsx b/src/components/house/current-user-invitation-list.tsx
new file mode 100644
index 0000000..1932f4e
--- /dev/null
+++ b/src/components/house/current-user-invitation-list.tsx
@@ -0,0 +1,123 @@
+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';
+import { m } from '@paraglide/messages';
+import { useMutation } from '@tanstack/react-query';
+import { Item, ItemContent, ItemDescription, ItemTitle } from '@ui/item';
+import { Skeleton } from '@ui/skeleton';
+import {
+ Table,
+ TableBody,
+ TableCell,
+ TableHead,
+ TableHeader,
+ TableRow,
+} from '@ui/table';
+import { toast } from 'sonner';
+import RoleBadge from '../avatar/role-badge';
+import { Button } from '../ui/button';
+
+type InvitationListProps = {
+ activeHouse: ReturnType['data'];
+};
+
+const CurrentUserInvitationList = ({ activeHouse }: InvitationListProps) => {
+ const { refetch } = authClient.useActiveOrganization();
+ const { hasPermission, isLoading } = useHasPermission(
+ 'invitation',
+ 'cancel',
+ true,
+ );
+
+ const { mutate: cancelInvitationMutation } = useMutation({
+ mutationFn: cancelInvitation,
+ onSuccess: () => {
+ refetch();
+ toast.success(m.houses_page_message_cancel_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 handleCancelInvitation = (id: string) => {
+ cancelInvitationMutation({ data: { id } });
+ };
+
+ if (!activeHouse || isLoading) {
+ return (
+
+ );
+ }
+
+ return (
+
+
+
+
+
+ {m.houses_page_ui_view_table_header_invite()}
+
+
+
+
+
+ {activeHouse.invitations.length > 0 ? (
+ activeHouse.invitations.map((item) => (
+
+
+ -
+
+
+ {m.houses_user_page_invite_label_to()}:{' '}
+ {item.email} -
+
+
+
+ {m.houses_user_page_invite_label_status()}:{' '}
+ {m.invite_status({
+ status: item.status as INVITE_STATUS,
+ })}
+
+
+
+
+
+
+
+ {item.status === INVITE_STATUS.PENDING && hasPermission && (
+
+ )}
+
+
+
+ ))
+ ) : (
+
+
+ No results.
+
+
+ )}
+
+
+
+ );
+};
+
+export default CurrentUserInvitationList;
diff --git a/src/components/house/current-user-member-list.tsx b/src/components/house/current-user-member-list.tsx
new file mode 100644
index 0000000..6cf13c2
--- /dev/null
+++ b/src/components/house/current-user-member-list.tsx
@@ -0,0 +1,78 @@
+import { authClient } from '@lib/auth-client';
+import { m } from '@paraglide/messages';
+import { Item, ItemContent, ItemDescription, ItemTitle } from '@ui/item';
+import { Skeleton } from '@ui/skeleton';
+import {
+ Table,
+ TableBody,
+ TableCell,
+ TableHead,
+ TableHeader,
+ TableRow,
+} from '@ui/table';
+import { useAuth } from '../auth/auth-provider';
+import RoleBadge from '../avatar/role-badge';
+import InviteUserAction from './invite-user-dialog';
+import RemoveUserFormHouse from './remove-user-form-house';
+
+type CurrentUserMemberListProps = {
+ activeHouse: ReturnType['data'];
+};
+
+const CurrentUserMemberList = ({ activeHouse }: CurrentUserMemberListProps) => {
+ const { session } = useAuth();
+
+ if (!activeHouse) {
+ return ;
+ }
+
+ return (
+
+
+
+
+
+ {m.houses_page_ui_table_header_name()} &{' '}
+ {m.houses_page_ui_view_table_header_email()}
+
+
+ {m.houses_page_ui_view_table_header_role()}
+
+
+
+
+
+
+
+
+
+ {activeHouse.members.map((member) => (
+
+
+ -
+
+ {member.user.name}
+ {member.user.email}
+
+
+
+
+
+
+
+
+ {member.role !== 'owner' &&
+ session.user.id !== member.user.id && (
+
+ )}
+
+
+
+ ))}
+
+
+
+ );
+};
+
+export default CurrentUserMemberList;
diff --git a/src/components/house/delete-house-dialog.tsx b/src/components/house/delete-house-dialog.tsx
new file mode 100644
index 0000000..3139bcd
--- /dev/null
+++ b/src/components/house/delete-house-dialog.tsx
@@ -0,0 +1,162 @@
+import useHasPermission from '@hooks/use-has-permission';
+import usePreventAutoFocus from '@hooks/use-prevent-auto-focus';
+import { m } from '@paraglide/messages';
+import { ShieldWarningIcon, TrashIcon } from '@phosphor-icons/react';
+import { deleteHouse } from '@service/house.api';
+import { housesQueries } from '@service/queries';
+import { useMutation, useQueryClient } from '@tanstack/react-query';
+import { Button } from '@ui/button';
+import {
+ Dialog,
+ DialogClose,
+ DialogContent,
+ DialogDescription,
+ DialogFooter,
+ DialogHeader,
+ DialogTitle,
+ DialogTrigger,
+} from '@ui/dialog';
+import { Label } from '@ui/label';
+import { Spinner } from '@ui/spinner';
+import {
+ Table,
+ TableBody,
+ TableCell,
+ TableHead,
+ TableHeader,
+ TableRow,
+} from '@ui/table';
+import { Tooltip, TooltipContent, TooltipTrigger } from '@ui/tooltip';
+import parse from 'html-react-parser';
+import { useState } from 'react';
+import { toast } from 'sonner';
+import RoleBadge from '../avatar/role-badge';
+import { Skeleton } from '../ui/skeleton';
+
+type DeleteHouseProps = {
+ data: HouseWithMembers;
+};
+
+const DeleteHouseAction = ({ data }: DeleteHouseProps) => {
+ const [_open, _setOpen] = useState(false);
+ const prevent = usePreventAutoFocus();
+ const { hasPermission, isLoading } = useHasPermission('house', 'delete');
+
+ const queryClient = useQueryClient();
+
+ const { mutate: deleteHouseMutation, isPending } = useMutation({
+ mutationFn: deleteHouse,
+ onSuccess: () => {
+ queryClient.invalidateQueries({
+ queryKey: [...housesQueries.all, 'list'],
+ });
+ _setOpen(false);
+ toast.success(m.houses_page_message_delete_house_success(), {
+ richColors: true,
+ });
+ },
+ onError: (error: ReturnError) => {
+ console.error(error);
+ const code = error.code as Parameters<
+ typeof m.backend_message
+ >[0]['code'];
+ toast.error(m.backend_message({ code }), {
+ richColors: true,
+ });
+ },
+ });
+
+ const onConfirm = () => {
+ deleteHouseMutation({ data });
+ };
+
+ if (isLoading) {
+ return ;
+ }
+
+ if (!hasPermission) return null;
+
+ return (
+
+ );
+};
+
+export default DeleteHouseAction;
diff --git a/src/components/house/delete-user-house-dialog.tsx b/src/components/house/delete-user-house-dialog.tsx
new file mode 100644
index 0000000..78cce41
--- /dev/null
+++ b/src/components/house/delete-user-house-dialog.tsx
@@ -0,0 +1,168 @@
+import useHasPermission from '@hooks/use-has-permission';
+import usePreventAutoFocus from '@hooks/use-prevent-auto-focus';
+import { authClient } from '@lib/auth-client';
+import { m } from '@paraglide/messages';
+import { ShieldWarningIcon, TrashIcon } from '@phosphor-icons/react';
+import { deleteUserHouse } from '@service/house.api';
+import { housesQueries } from '@service/queries';
+import { useMutation, useQueryClient } from '@tanstack/react-query';
+import { Button } from '@ui/button';
+import {
+ Dialog,
+ DialogClose,
+ DialogContent,
+ DialogDescription,
+ DialogFooter,
+ DialogHeader,
+ DialogTitle,
+ DialogTrigger,
+} from '@ui/dialog';
+import { Label } from '@ui/label';
+import { Spinner } from '@ui/spinner';
+import {
+ Table,
+ TableBody,
+ TableCell,
+ TableHead,
+ TableHeader,
+ TableRow,
+} from '@ui/table';
+import { Tooltip, TooltipContent, TooltipTrigger } from '@ui/tooltip';
+import parse from 'html-react-parser';
+import { useState } from 'react';
+import { toast } from 'sonner';
+import RoleBadge from '../avatar/role-badge';
+import { Skeleton } from '../ui/skeleton';
+
+type DeleteUserHouseProps = {
+ activeHouse: ReturnType['data'];
+};
+
+const DeleteUserHouseAction = ({ activeHouse }: DeleteUserHouseProps) => {
+ const [_open, _setOpen] = useState(false);
+ const prevent = usePreventAutoFocus();
+ const { hasPermission, isLoading } = useHasPermission(
+ 'house',
+ 'delete',
+ true,
+ );
+
+ const queryClient = useQueryClient();
+
+ const { mutate: deleteHouseMutation, isPending } = useMutation({
+ mutationFn: deleteUserHouse,
+ onSuccess: () => {
+ queryClient.invalidateQueries({
+ queryKey: [...housesQueries.all, 'currentUser'],
+ });
+ _setOpen(false);
+ toast.success(m.houses_page_message_delete_house_success(), {
+ richColors: true,
+ });
+ },
+ onError: (error: ReturnError) => {
+ console.error(error);
+ const code = error.code as Parameters<
+ typeof m.backend_message
+ >[0]['code'];
+ toast.error(m.backend_message({ code }), {
+ richColors: true,
+ });
+ },
+ });
+
+ if (isLoading || !activeHouse) {
+ return ;
+ }
+
+ const onConfirm = async () => {
+ deleteHouseMutation({ data: { id: activeHouse.id } });
+ };
+
+ if (!hasPermission) return null;
+
+ return (
+
+ );
+};
+
+export default DeleteUserHouseAction;
diff --git a/src/components/house/edit-house-dialog.tsx b/src/components/house/edit-house-dialog.tsx
new file mode 100644
index 0000000..75a1244
--- /dev/null
+++ b/src/components/house/edit-house-dialog.tsx
@@ -0,0 +1,75 @@
+import EditHouseForm from '@form/house/admin-edit-house-form';
+import useHasPermission from '@hooks/use-has-permission';
+import usePreventAutoFocus from '@hooks/use-prevent-auto-focus';
+import { m } from '@paraglide/messages';
+import { PenIcon } from '@phosphor-icons/react';
+import { Button } from '@ui/button';
+import {
+ Dialog,
+ DialogContent,
+ DialogDescription,
+ DialogHeader,
+ DialogTitle,
+ DialogTrigger,
+} from '@ui/dialog';
+import { Label } from '@ui/label';
+import { Skeleton } from '@ui/skeleton';
+import { Tooltip, TooltipContent, TooltipTrigger } from '@ui/tooltip';
+import { useState } from 'react';
+
+type EditHouseProps = {
+ data: HouseWithMembers;
+};
+
+const EditHouseAction = ({ data }: EditHouseProps) => {
+ const [_open, _setOpen] = useState(false);
+ const prevent = usePreventAutoFocus();
+ const { hasPermission, isLoading } = useHasPermission('house', 'update');
+
+ if (isLoading) {
+ return ;
+ }
+
+ if (!hasPermission) return null;
+
+ return (
+
+ );
+};
+
+export default EditHouseAction;
diff --git a/src/components/house/edit-user-house-dialog.tsx b/src/components/house/edit-user-house-dialog.tsx
new file mode 100644
index 0000000..a87b8d4
--- /dev/null
+++ b/src/components/house/edit-user-house-dialog.tsx
@@ -0,0 +1,83 @@
+import EditHouseForm from '@form/house/admin-edit-house-form';
+import useHasPermission from '@hooks/use-has-permission';
+import usePreventAutoFocus from '@hooks/use-prevent-auto-focus';
+import { authClient } from '@lib/auth-client';
+import { m } from '@paraglide/messages';
+import { PenIcon } from '@phosphor-icons/react';
+import { Button } from '@ui/button';
+import {
+ Dialog,
+ DialogContent,
+ DialogDescription,
+ DialogHeader,
+ DialogTitle,
+ DialogTrigger,
+} from '@ui/dialog';
+import { Label } from '@ui/label';
+import { Skeleton } from '@ui/skeleton';
+import { Tooltip, TooltipContent, TooltipTrigger } from '@ui/tooltip';
+import { useState } from 'react';
+
+type EditUserHouseProps = {
+ data: ReturnType['data'];
+};
+
+const EditUserHouseAction = ({ data }: EditUserHouseProps) => {
+ const [_open, _setOpen] = useState(false);
+ const prevent = usePreventAutoFocus();
+ const { hasPermission, isLoading } = useHasPermission(
+ 'house',
+ 'update',
+ true,
+ );
+
+ if (isLoading) {
+ return ;
+ }
+
+ if (!hasPermission) return null;
+
+ return (
+
+ );
+};
+
+export default EditUserHouseAction;
diff --git a/src/components/house/house-column.tsx b/src/components/house/house-column.tsx
new file mode 100644
index 0000000..598f5e9
--- /dev/null
+++ b/src/components/house/house-column.tsx
@@ -0,0 +1,51 @@
+import { m } from '@paraglide/messages';
+import { ColumnDef } from '@tanstack/react-table';
+import { formatters } from '@utils/formatters';
+import DeleteHouseAction from './delete-house-dialog';
+import EditHouseAction from './edit-house-dialog';
+import ViewDetailHouse from './view-house-detail-dialog';
+
+export const houseColumns: ColumnDef[] = [
+ {
+ accessorKey: 'name',
+ header: m.houses_page_ui_table_header_name(),
+ meta: {
+ thClass: 'w-1/6',
+ },
+ },
+ {
+ accessorKey: 'members',
+ header: m.houses_page_ui_table_header_members(),
+ meta: {
+ thClass: 'w-1/6',
+ },
+ cell: ({ row }) => {
+ return row.original.members.length;
+ },
+ },
+ {
+ accessorKey: 'createdAt',
+ header: m.houses_page_ui_table_header_created_at(),
+ meta: {
+ thClass: 'w-1/6',
+ },
+ cell: ({ row }) => {
+ return formatters.dateTime(new Date(row.original.createdAt));
+ },
+ },
+ {
+ id: 'actions',
+ meta: {
+ thClass: 'w-1/6',
+ },
+ cell: ({ row }) => {
+ return (
+
+
+
+
+
+ );
+ },
+ },
+];
diff --git a/src/components/house/invite-user-dialog.tsx b/src/components/house/invite-user-dialog.tsx
new file mode 100644
index 0000000..ec7e846
--- /dev/null
+++ b/src/components/house/invite-user-dialog.tsx
@@ -0,0 +1,68 @@
+import UserInviteMemberForm from '@form/house/user-invite-member-form';
+import useHasPermission from '@hooks/use-has-permission';
+import usePreventAutoFocus from '@hooks/use-prevent-auto-focus';
+import { m } from '@paraglide/messages';
+import { PaperPlaneTiltIcon } from '@phosphor-icons/react';
+import { Button } from '@ui/button';
+import {
+ Dialog,
+ DialogContent,
+ DialogDescription,
+ DialogHeader,
+ DialogTitle,
+ DialogTrigger,
+} from '@ui/dialog';
+import { useState } from 'react';
+import { Skeleton } from '../ui/skeleton';
+
+type ActionProps = {};
+
+const InviteUserAction = ({}: ActionProps) => {
+ const { hasPermission, isLoading } = useHasPermission(
+ 'invitation',
+ 'create',
+ true,
+ );
+ const [_open, _setOpen] = useState(false);
+ const prevent = usePreventAutoFocus();
+
+ if (isLoading) {
+ return ;
+ }
+
+ if (!hasPermission) return null;
+
+ return (
+
+ );
+};
+
+export default InviteUserAction;
diff --git a/src/components/house/leave-house-dialog.tsx b/src/components/house/leave-house-dialog.tsx
new file mode 100644
index 0000000..14fb298
--- /dev/null
+++ b/src/components/house/leave-house-dialog.tsx
@@ -0,0 +1,120 @@
+import useHasPermission from '@/hooks/use-has-permission';
+import usePreventAutoFocus from '@/hooks/use-prevent-auto-focus';
+import { m } from '@/paraglide/messages';
+import { leaveHouse } from '@/service/house.api';
+import { FootprintsIcon } from '@phosphor-icons/react';
+import { useMutation } from '@tanstack/react-query';
+import { Button } from '@ui/button';
+import {
+ Dialog,
+ DialogClose,
+ DialogContent,
+ DialogDescription,
+ DialogFooter,
+ DialogHeader,
+ DialogTitle,
+ DialogTrigger,
+} from '@ui/dialog';
+import { Label } from '@ui/label';
+import { Skeleton } from '@ui/skeleton';
+import { Spinner } from '@ui/spinner';
+import { Tooltip, TooltipContent, TooltipTrigger } from '@ui/tooltip';
+import { useState } from 'react';
+import { toast } from 'sonner';
+
+type LeaveHouseProps = {
+ activeHouseId: string;
+};
+
+const LeaveHouseAction = ({ activeHouseId }: LeaveHouseProps) => {
+ const [_open, _setOpen] = useState(false);
+ const prevent = usePreventAutoFocus();
+ const { hasPermission, isLoading } = useHasPermission('house', 'leave', true);
+
+ const { mutate: leaveHouseMutation, isPending } = useMutation({
+ mutationFn: leaveHouse,
+ onSuccess: () => {
+ toast.success(m.houses_page_message_leave_house_success(), {
+ richColors: true,
+ });
+ },
+ onError: (error: ReturnError) => {
+ const code = error.code as Parameters<
+ typeof m.backend_message
+ >[0]['code'];
+ toast.error(m.backend_message({ code }), {
+ richColors: true,
+ });
+ },
+ });
+
+ if (isLoading || !activeHouseId) {
+ return ;
+ }
+
+ const onConfirm = async () => {
+ leaveHouseMutation({ data: { id: activeHouseId } });
+ };
+
+ if (!hasPermission) return null;
+
+ return (
+
+ );
+};
+
+export default LeaveHouseAction;
diff --git a/src/components/house/remove-user-form-house.tsx b/src/components/house/remove-user-form-house.tsx
new file mode 100644
index 0000000..8da9762
--- /dev/null
+++ b/src/components/house/remove-user-form-house.tsx
@@ -0,0 +1,136 @@
+import usePreventAutoFocus from '@/hooks/use-prevent-auto-focus';
+import { authClient } from '@/lib/auth-client';
+import { m } from '@/paraglide/messages';
+import { removeMember } from '@/service/house.api';
+import { housesQueries } from '@/service/queries';
+import { UserCircleMinusIcon } from '@phosphor-icons/react';
+import { useMutation, useQueryClient } from '@tanstack/react-query';
+import { Button } from '@ui/button';
+import {
+ Dialog,
+ DialogClose,
+ DialogContent,
+ DialogDescription,
+ DialogFooter,
+ DialogHeader,
+ DialogTitle,
+ DialogTrigger,
+} from '@ui/dialog';
+import { Label } from '@ui/label';
+import { Spinner } from '@ui/spinner';
+import { Tooltip, TooltipContent, TooltipTrigger } from '@ui/tooltip';
+import { useState } from 'react';
+import { toast } from 'sonner';
+
+type RemoveUserFormProps = {
+ member: {
+ id: string;
+ organizationId: string;
+ userId: string;
+ user: {
+ email: string;
+ name: string;
+ };
+ };
+};
+
+const RemoveUserFormHouse = ({ member }: RemoveUserFormProps) => {
+ const [_open, _setOpen] = useState(false);
+ const prevent = usePreventAutoFocus();
+ const { refetch } = authClient.useActiveOrganization();
+
+ const queryClient = useQueryClient();
+
+ const { mutate: removeMemberMutation, isPending } = useMutation({
+ mutationFn: removeMember,
+ onSuccess: () => {
+ refetch();
+ queryClient.invalidateQueries({
+ queryKey: [...housesQueries.all, 'currentUser'],
+ });
+ _setOpen(false);
+ toast.success(m.houses_page_message_remove_member_success(), {
+ richColors: true,
+ });
+ },
+ onError: (error: ReturnError) => {
+ const code = error.code as Parameters<
+ typeof m.backend_message
+ >[0]['code'];
+ toast.error(m.backend_message({ code }), {
+ richColors: true,
+ });
+ },
+ });
+
+ const onConfirm = () => {
+ removeMemberMutation({
+ data: {
+ houseId: member.organizationId,
+ memberId: member.id,
+ },
+ });
+ };
+
+ return (
+
+ );
+};
+
+export default RemoveUserFormHouse;
diff --git a/src/components/house/view-house-detail-dialog.tsx b/src/components/house/view-house-detail-dialog.tsx
new file mode 100644
index 0000000..05a8ec0
--- /dev/null
+++ b/src/components/house/view-house-detail-dialog.tsx
@@ -0,0 +1,123 @@
+import usePreventAutoFocus from '@hooks/use-prevent-auto-focus';
+import { m } from '@paraglide/messages';
+import { EyeIcon } from '@phosphor-icons/react';
+import { Button } from '@ui/button';
+import {
+ Dialog,
+ DialogContent,
+ DialogDescription,
+ DialogHeader,
+ DialogTitle,
+ DialogTrigger,
+} from '@ui/dialog';
+import { Label } from '@ui/label';
+import {
+ Table,
+ TableBody,
+ TableCell,
+ TableHead,
+ TableHeader,
+ TableRow,
+} from '@ui/table';
+import { Tooltip, TooltipContent, TooltipTrigger } from '@ui/tooltip';
+import { formatters } from '@utils/formatters';
+import RoleBadge from '../avatar/role-badge';
+
+type ViewDetailProps = {
+ data: HouseWithMembers;
+};
+
+const ViewDetailHouse = ({ data }: ViewDetailProps) => {
+ const prevent = usePreventAutoFocus();
+
+ return (
+
+ );
+};
+export default ViewDetailHouse;
diff --git a/src/components/notification/notification-item.tsx b/src/components/notification/notification-item.tsx
new file mode 100644
index 0000000..39fe36a
--- /dev/null
+++ b/src/components/notification/notification-item.tsx
@@ -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 (
+ - ['variant'],
+ className,
+ }),
+ )}
+ {...props}
+ >
+
+ {m.templates_title_notification({
+ title: notify.title as Parameters<
+ typeof m.templates_title_notification
+ >[0]['title'],
+ })}
+
+ {notify.type === NOTIFICATION_TYPE.INVITATION && (
+
+ )}
+
+ );
+};
+
+export default NotificationItem;
diff --git a/src/components/notification/notification-type/invitation.tsx b/src/components/notification/notification-type/invitation.tsx
new file mode 100644
index 0000000..e15e80d
--- /dev/null
+++ b/src/components/notification/notification-type/invitation.tsx
@@ -0,0 +1,117 @@
+import { Button } from '@/components/ui/button';
+import { formatTimeAgo } from '@/utils/helper';
+import { m } from '@paraglide/messages';
+import { acceptInvitation, rejectInvitation } from '@service/house.api';
+import { notificationQueries } from '@service/queries';
+import { useMutation, useQueryClient } from '@tanstack/react-query';
+import { ItemActions, ItemContent, ItemDescription, ItemTitle } from '@ui/item';
+import { Spinner } from '@ui/spinner';
+import { toast } from 'sonner';
+
+type NotifyProps = {
+ notify: NotificationWithUser;
+};
+
+const NotificationInvitation = ({ notify }: NotifyProps) => {
+ const { house } = JSON.parse(notify.metadata || '');
+
+ const queryClient = useQueryClient();
+
+ const { mutate: acceptInvitationMutation, isPending: isAcceptPending } =
+ useMutation({
+ mutationFn: acceptInvitation,
+ onSuccess: () => {
+ queryClient.invalidateQueries({
+ queryKey: [...notificationQueries.all, 'list'],
+ });
+ toast.success(m.notification_page_message_invitation_success(), {
+ richColors: true,
+ });
+ },
+ onError: (error: ReturnError) => {
+ console.error(error);
+ const code = error.code as Parameters<
+ typeof m.backend_message
+ >[0]['code'];
+ toast.error(m.backend_message({ code }), {
+ richColors: true,
+ });
+ },
+ });
+
+ const { mutate: rejectInvitationMutation, isPending: isRejectPending } =
+ useMutation({
+ mutationFn: rejectInvitation,
+ onSuccess: () => {
+ queryClient.invalidateQueries({
+ queryKey: [...notificationQueries.all, 'list'],
+ });
+ toast.success(m.notification_page_message_invitation_rejected(), {
+ richColors: true,
+ });
+ },
+ onError: (error: ReturnError) => {
+ console.error(error);
+ const code = error.code as Parameters<
+ typeof m.backend_message
+ >[0]['code'];
+ toast.error(m.backend_message({ code }), {
+ richColors: true,
+ });
+ },
+ });
+
+ const handleAgreeAction = async () => {
+ if (notify.link) {
+ acceptInvitationMutation({
+ data: { id: notify.link, notificationId: notify.id },
+ });
+ }
+ };
+
+ const handleRejectAction = () => {
+ if (notify.link) {
+ rejectInvitationMutation({
+ data: { id: notify.link, notificationId: notify.id },
+ });
+ }
+ };
+
+ return (
+ <>
+
+
+ {`${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,
+ })}
+
+ {formatTimeAgo(notify.createdAt)}
+
+ {notify.link && (
+
+
+
+
+ )}
+ >
+ );
+};
+
+export default NotificationInvitation;
diff --git a/src/components/sidebar/nav-main.tsx b/src/components/sidebar/nav-main.tsx
index 2bceb1e..7d7d4b3 100644
--- a/src/components/sidebar/nav-main.tsx
+++ b/src/components/sidebar/nav-main.tsx
@@ -1,14 +1,13 @@
-import { m } from '@/paraglide/messages';
+import { m } from '@paraglide/messages';
import {
CircuitryIcon,
GaugeIcon,
GearIcon,
HouseIcon,
UsersIcon,
+ WarehouseIcon,
} from '@phosphor-icons/react';
import { createLink } from '@tanstack/react-router';
-import AdminShow from '../auth/AdminShow';
-import AuthShow from '../auth/AuthShow';
import {
SidebarGroup,
SidebarGroupContent,
@@ -16,95 +15,121 @@ import {
SidebarMenu,
SidebarMenuButton,
SidebarMenuItem,
-} from '../ui/sidebar';
+} from '@ui/sidebar';
+import React from 'react';
+import AdminShow from '../auth/AdminShow';
+import AuthShow from '../auth/AuthShow';
const SidebarMenuButtonLink = createLink(SidebarMenuButton);
const NAV_MAIN = [
{
id: '1',
- title: 'Basic',
+ title: m.nav_label_basic(),
+ isAuth: false,
+ admin: false,
items: [
{
title: m.nav_home(),
path: '/',
icon: HouseIcon,
- isAuth: true,
- admin: false,
- },
- {
- title: m.nav_dashboard(),
- path: '/dashboard',
- icon: GaugeIcon,
- isAuth: true,
- admin: false,
},
],
},
{
id: '2',
- title: 'Management',
+ title: m.nav_label_management(),
+ isAuth: true,
+ admin: false,
+ items: [
+ {
+ title: m.nav_dashboard(),
+ path: '/management/dashboard',
+ icon: GaugeIcon,
+ },
+ {
+ title: m.nav_houses(),
+ path: '/management/houses',
+ icon: WarehouseIcon,
+ },
+ ],
+ },
+ {
+ id: '3',
+ title: m.nav_label_kanri(),
+ isAuth: true,
+ admin: true,
items: [
{
title: m.nav_users(),
path: '/kanri/users',
icon: UsersIcon,
- isAuth: false,
- admin: true,
+ },
+ {
+ title: m.nav_houses(),
+ path: '/kanri/houses',
+ icon: WarehouseIcon,
},
{
title: m.nav_logs(),
path: '/kanri/logs',
icon: CircuitryIcon,
- isAuth: false,
- admin: true,
},
{
title: m.nav_settings(),
path: '/kanri/settings',
icon: GearIcon,
- isAuth: false,
- admin: true,
},
],
},
];
+function EmptyComponent({ children }: { children: React.ReactNode }) {
+ return <>{children}>;
+}
+
const NavMain = () => {
return (
<>
- {NAV_MAIN.map((nav) => (
-
- {nav.title}
-
-
- {nav.items.map((item) => {
- const Icon = item.icon;
- const Menu = (
-
-
-
- {item.title}
-
-
- );
- return item.isAuth ? (
- {Menu}
- ) : item.admin ? (
- {Menu}
- ) : (
- Menu
- );
- })}
-
-
-
- ))}
+ {NAV_MAIN.map((nav) => {
+ const { isAuth, admin } = nav;
+ const Component = admin
+ ? AdminShow
+ : isAuth
+ ? AuthShow
+ : EmptyComponent;
+
+ return (
+
+
+ {nav.title}
+
+
+ {nav.items.map((item) => {
+ const Icon = item.icon;
+ return (
+
+
+
+ {item.title}
+
+
+ );
+ })}
+
+
+
+
+ );
+ })}
>
);
};
diff --git a/src/components/sidebar/nav-user.tsx b/src/components/sidebar/nav-user.tsx
index 5437fde..a7dc409 100644
--- a/src/components/sidebar/nav-user.tsx
+++ b/src/components/sidebar/nav-user.tsx
@@ -1,5 +1,5 @@
-import { authClient } from '@/lib/auth-client';
-import { m } from '@/paraglide/messages';
+import { authClient } from '@lib/auth-client';
+import { m } from '@paraglide/messages';
import {
DotsThreeVerticalIcon,
KeyIcon,
@@ -9,10 +9,6 @@ import {
} from '@phosphor-icons/react';
import { useQueryClient } from '@tanstack/react-query';
import { createLink, Link, useNavigate } from '@tanstack/react-router';
-import { toast } from 'sonner';
-import { useAuth } from '../auth/auth-provider';
-import AvatarUser from '../avatar/avatar-user';
-import RoleBadge from '../avatar/role-badge';
import {
DropdownMenu,
DropdownMenuContent,
@@ -21,13 +17,17 @@ import {
DropdownMenuLabel,
DropdownMenuSeparator,
DropdownMenuTrigger,
-} from '../ui/dropdown-menu';
+} from '@ui/dropdown-menu';
import {
SidebarMenu,
SidebarMenuButton,
SidebarMenuItem,
useSidebar,
-} from '../ui/sidebar';
+} from '@ui/sidebar';
+import { toast } from 'sonner';
+import { useAuth } from '../auth/auth-provider';
+import AvatarUser from '../avatar/avatar-user';
+import RoleBadge from '../avatar/role-badge';
const SidebarMenuButtonLink = createLink(SidebarMenuButton);
@@ -35,7 +35,7 @@ const NavUser = () => {
const navigate = useNavigate();
const { isMobile } = useSidebar();
const queryClient = useQueryClient();
- const { data: session } = useAuth();
+ const { session } = useAuth();
const signout = async () => {
await authClient.signOut({
diff --git a/src/components/sidebar/router-breadcrumb.tsx b/src/components/sidebar/router-breadcrumb.tsx
index 37c6a4d..0b4afa0 100644
--- a/src/components/sidebar/router-breadcrumb.tsx
+++ b/src/components/sidebar/router-breadcrumb.tsx
@@ -1,5 +1,4 @@
-import { AnyRouteMatch, Link, useMatches } from '@tanstack/react-router'
-import { Fragment } from 'react/jsx-runtime'
+import { AnyRouteMatch, Link, useMatches } from '@tanstack/react-router';
import {
Breadcrumb,
BreadcrumbItem,
@@ -7,15 +6,16 @@ import {
BreadcrumbList,
BreadcrumbPage,
BreadcrumbSeparator,
-} from '../ui/breadcrumb'
+} from '@ui/breadcrumb';
+import { Fragment } from 'react/jsx-runtime';
export type BreadcrumbValue =
| string
| string[]
- | ((match: AnyRouteMatch) => string | string[])
+ | ((match: AnyRouteMatch) => string | string[]);
const RouterBreadcrumb = () => {
- const matches = useMatches()
+ const matches = useMatches();
const breadcrumbs = matches.flatMap((match) => {
const staticData = match.staticData;
@@ -40,7 +40,7 @@ const RouterBreadcrumb = () => {
{breadcrumbs.map((crumb, index) => {
- const isLast = index === breadcrumbs.length - 1
+ const isLast = index === breadcrumbs.length - 1;
return (
@@ -55,11 +55,11 @@ const RouterBreadcrumb = () => {
{!isLast && }
- )
+ );
})}
- )
-}
+ );
+};
-export default RouterBreadcrumb
+export default RouterBreadcrumb;
diff --git a/src/components/ui/alert.tsx b/src/components/ui/alert.tsx
index bb4ad7e..c1d4c06 100644
--- a/src/components/ui/alert.tsx
+++ b/src/components/ui/alert.tsx
@@ -1,7 +1,7 @@
import { cva, type VariantProps } from 'class-variance-authority';
import * as React from 'react';
-import { cn } from '@/lib/utils';
+import { cn } from '@lib/utils';
const alertVariants = cva(
"grid gap-0.5 rounded-lg border px-2 py-1.5 text-left text-xs/relaxed has-data-[slot=alert-action]:relative has-data-[slot=alert-action]:pr-18 has-[>svg]:grid-cols-[auto_1fr] has-[>svg]:gap-x-1.5 *:[svg]:row-span-2 *:[svg]:translate-y-0.5 *:[svg]:text-current *:[svg:not([class*='size-'])]:size-3.5 w-full relative group/alert",
diff --git a/src/components/ui/avatar.tsx b/src/components/ui/avatar.tsx
index 12c48a0..a14c9f6 100644
--- a/src/components/ui/avatar.tsx
+++ b/src/components/ui/avatar.tsx
@@ -1,26 +1,26 @@
-import * as React from "react"
-import { Avatar as AvatarPrimitive } from "radix-ui"
+import { Avatar as AvatarPrimitive } from 'radix-ui';
+import * as React from 'react';
-import { cn } from "@/lib/utils"
+import { cn } from '@lib/utils';
function Avatar({
className,
- size = "default",
+ size = 'default',
...props
}: React.ComponentProps & {
- size?: "default" | "sm" | "lg"
+ size?: 'default' | 'sm' | 'lg';
}) {
return (
- )
+ );
}
function AvatarImage({
@@ -31,12 +31,12 @@ function AvatarImage({
- )
+ );
}
function AvatarFallback({
@@ -47,61 +47,64 @@ function AvatarFallback({
- )
+ );
}
-function AvatarBadge({ className, ...props }: React.ComponentProps<"span">) {
+function AvatarBadge({ className, ...props }: React.ComponentProps<'span'>) {
return (
svg]:hidden",
- "group-data-[size=default]/avatar:size-2.5 group-data-[size=default]/avatar:[&>svg]:size-2",
- "group-data-[size=lg]/avatar:size-3 group-data-[size=lg]/avatar:[&>svg]:size-2",
- className
+ 'bg-primary text-primary-foreground ring-background absolute right-0 bottom-0 z-10 inline-flex items-center justify-center rounded-full bg-blend-color ring-2 select-none',
+ 'group-data-[size=sm]/avatar:size-2 group-data-[size=sm]/avatar:[&>svg]:hidden',
+ 'group-data-[size=default]/avatar:size-2.5 group-data-[size=default]/avatar:[&>svg]:size-2',
+ 'group-data-[size=lg]/avatar:size-3 group-data-[size=lg]/avatar:[&>svg]:size-2',
+ className,
)}
{...props}
/>
- )
+ );
}
-function AvatarGroup({ className, ...props }: React.ComponentProps<"div">) {
+function AvatarGroup({ className, ...props }: React.ComponentProps<'div'>) {
return (
- )
+ );
}
function AvatarGroupCount({
className,
...props
-}: React.ComponentProps<"div">) {
+}: React.ComponentProps<'div'>) {
return (
svg]:size-4 group-has-data-[size=lg]/avatar-group:[&>svg]:size-5 group-has-data-[size=sm]/avatar-group:[&>svg]:size-3 ring-background relative flex shrink-0 items-center justify-center ring-2", className)}
+ className={cn(
+ 'bg-muted text-muted-foreground size-8 rounded-full text-xs/relaxed group-has-data-[size=lg]/avatar-group:size-10 group-has-data-[size=sm]/avatar-group:size-6 [&>svg]:size-4 group-has-data-[size=lg]/avatar-group:[&>svg]:size-5 group-has-data-[size=sm]/avatar-group:[&>svg]:size-3 ring-background relative flex shrink-0 items-center justify-center ring-2',
+ className,
+ )}
{...props}
/>
- )
+ );
}
export {
Avatar,
- AvatarImage,
+ AvatarBadge,
AvatarFallback,
AvatarGroup,
AvatarGroupCount,
- AvatarBadge,
-}
+ AvatarImage,
+};
diff --git a/src/components/ui/badge.tsx b/src/components/ui/badge.tsx
index 5e37384..792b5b0 100644
--- a/src/components/ui/badge.tsx
+++ b/src/components/ui/badge.tsx
@@ -2,7 +2,7 @@ import { cva, type VariantProps } from 'class-variance-authority';
import { Slot } from 'radix-ui';
import * as React from 'react';
-import { cn } from '@/lib/utils';
+import { cn } from '@lib/utils';
const badgeVariants = cva(
'h-5 gap-1 rounded-full border border-transparent px-2 py-0.5 text-[0.625rem] font-medium transition-all has-data-[icon=inline-end]:pr-1.5 has-data-[icon=inline-start]:pl-1.5 [&>svg]:size-2.5! inline-flex items-center justify-center w-fit whitespace-nowrap shrink-0 [&>svg]:pointer-events-none focus-visible:border-ring focus-visible:ring-ring/50 focus-visible:ring-[3px] aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 aria-invalid:border-destructive transition-colors overflow-hidden group/badge',
diff --git a/src/components/ui/breadcrumb.tsx b/src/components/ui/breadcrumb.tsx
index 014383b..87b352f 100644
--- a/src/components/ui/breadcrumb.tsx
+++ b/src/components/ui/breadcrumb.tsx
@@ -1,7 +1,7 @@
import { Slot } from 'radix-ui';
import * as React from 'react';
-import { cn } from '@/lib/utils';
+import { cn } from '@lib/utils';
import { CaretRightIcon, DotsThreeIcon } from '@phosphor-icons/react';
function Breadcrumb({ className, ...props }: React.ComponentProps<'nav'>) {
diff --git a/src/components/ui/button-group.tsx b/src/components/ui/button-group.tsx
index 9d448bb..b9db415 100644
--- a/src/components/ui/button-group.tsx
+++ b/src/components/ui/button-group.tsx
@@ -1,8 +1,8 @@
import { cva, type VariantProps } from "class-variance-authority"
import { Slot } from "radix-ui"
-import { cn } from "@/lib/utils"
-import { Separator } from "@/components/ui/separator"
+import { Separator } from '@/components/ui/separator';
+import { cn } from '@lib/utils';
const buttonGroupVariants = cva(
"has-[>[data-slot=button-group]]:gap-2 has-[select[aria-hidden=true]:last-child]:[&>[data-slot=select-trigger]:last-of-type]:rounded-r-md flex w-fit items-stretch [&>*]:focus-visible:z-10 [&>*]:focus-visible:relative [&>[data-slot=select-trigger]:not([class*='w-'])]:w-fit [&>input]:flex-1",
@@ -10,22 +10,22 @@ const buttonGroupVariants = cva(
variants: {
orientation: {
horizontal:
- "[&>[data-slot]:not(:has(~[data-slot]))]:rounded-r-md! [&>*:not(:first-child)]:rounded-l-none [&>*:not(:first-child)]:border-l-0 [&>*:not(:last-child)]:rounded-r-none",
+ '[&>[data-slot]:not(:has(~[data-slot]))]:rounded-r-md! [&>*:not(:first-child)]:rounded-l-none [&>*:not(:first-child)]:border-l-0 [&>*:not(:last-child)]:rounded-r-none',
vertical:
- "[&>[data-slot]:not(:has(~[data-slot]))]:rounded-b-md! flex-col [&>*:not(:first-child)]:rounded-t-none [&>*:not(:first-child)]:border-t-0 [&>*:not(:last-child)]:rounded-b-none",
+ '[&>[data-slot]:not(:has(~[data-slot]))]:rounded-b-md! flex-col [&>*:not(:first-child)]:rounded-t-none [&>*:not(:first-child)]:border-t-0 [&>*:not(:last-child)]:rounded-b-none',
},
},
defaultVariants: {
- orientation: "horizontal",
+ orientation: 'horizontal',
},
- }
-)
+ },
+);
function ButtonGroup({
className,
orientation,
...props
-}: React.ComponentProps<"div"> & VariantProps
) {
+}: React.ComponentProps<'div'> & VariantProps) {
return (
- )
+ );
}
function ButtonGroupText({
className,
asChild = false,
...props
-}: React.ComponentProps<"div"> & {
- asChild?: boolean
+}: React.ComponentProps<'div'> & {
+ asChild?: boolean;
}) {
- const Comp = asChild ? Slot.Root : "div"
+ const Comp = asChild ? Slot.Root : 'div';
return (
- )
+ );
}
function ButtonGroupSeparator({
className,
- orientation = "vertical",
+ orientation = 'vertical',
...props
}: React.ComponentProps) {
return (
@@ -67,12 +67,12 @@ function ButtonGroupSeparator({
data-slot="button-group-separator"
orientation={orientation}
className={cn(
- "bg-input relative self-stretch data-[orientation=horizontal]:mx-px data-[orientation=horizontal]:w-auto data-[orientation=vertical]:my-px data-[orientation=vertical]:h-auto",
- className
+ 'bg-input relative self-stretch data-[orientation=horizontal]:mx-px data-[orientation=horizontal]:w-auto data-[orientation=vertical]:my-px data-[orientation=vertical]:h-auto',
+ className,
)}
{...props}
/>
- )
+ );
}
export {
@@ -80,4 +80,4 @@ export {
ButtonGroupSeparator,
ButtonGroupText,
buttonGroupVariants,
-}
+};
diff --git a/src/components/ui/button.tsx b/src/components/ui/button.tsx
index d1eed66..b0fd244 100644
--- a/src/components/ui/button.tsx
+++ b/src/components/ui/button.tsx
@@ -2,10 +2,10 @@ import { cva, type VariantProps } from 'class-variance-authority';
import { Slot } from 'radix-ui';
import * as React from 'react';
-import { cn } from '@/lib/utils';
+import { cn } from '@lib/utils';
const buttonVariants = cva(
- "focus-visible:border-ring focus-visible:ring-ring/30 aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 aria-invalid:border-destructive dark:aria-invalid:border-destructive/50 rounded-md border border-transparent bg-clip-padding text-xs/relaxed font-medium focus-visible:ring-[2px] aria-invalid:ring-[2px] [&_svg:not([class*='size-'])]:size-4 inline-flex items-center justify-center whitespace-nowrap transition-all disabled:pointer-events-none disabled:opacity-50 disabled:data-[active=true]:opacity-100 disabled:data-[active=true]:bg-primary disabled:data-[active=true]:border-primary disabled:data-[active=true]:text-white [&_svg]:pointer-events-none shrink-0 [&_svg]:shrink-0 outline-none group/button select-none",
+ "focus-visible:border-ring focus-visible:ring-ring/30 aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 aria-invalid:border-destructive dark:aria-invalid:border-destructive/50 rounded-md border border-transparent bg-clip-padding text-xs/relaxed font-medium focus-visible:ring-[2px] aria-invalid:ring-[2px] [&_svg:not([class*='size-'])]:size-4 inline-flex items-center justify-center whitespace-nowrap transition-all disabled:pointer-events-none disabled:opacity-50 disabled:data-[active=true]:opacity-100 disabled:data-[active=true]:bg-primary disabled:data-[active=true]:border-primary disabled:data-[active=true]:text-white disabled:data-[dot=true]:opacity-100 [&_svg]:pointer-events-none shrink-0 [&_svg]:shrink-0 outline-none group/button select-none cursor-pointer",
{
variants: {
variant: {
diff --git a/src/components/ui/card.tsx b/src/components/ui/card.tsx
index f64f3a5..bae621a 100644
--- a/src/components/ui/card.tsx
+++ b/src/components/ui/card.tsx
@@ -1,6 +1,6 @@
import * as React from "react"
-import { cn } from "@/lib/utils"
+import { cn } from '@lib/utils';
function Card({
className,
@@ -85,10 +85,10 @@ function CardFooter({ className, ...props }: React.ComponentProps<"div">) {
export {
Card,
- CardHeader,
- CardFooter,
- CardTitle,
CardAction,
- CardDescription,
CardContent,
-}
+ CardDescription,
+ CardFooter,
+ CardHeader,
+ CardTitle,
+};
diff --git a/src/components/ui/dialog.tsx b/src/components/ui/dialog.tsx
index 502a1c2..88060a8 100644
--- a/src/components/ui/dialog.tsx
+++ b/src/components/ui/dialog.tsx
@@ -2,7 +2,7 @@ import { Dialog as DialogPrimitive } from 'radix-ui';
import * as React from 'react';
import { Button } from '@/components/ui/button';
-import { cn } from '@/lib/utils';
+import { cn } from '@lib/utils';
import { XIcon } from '@phosphor-icons/react';
function Dialog({
@@ -59,7 +59,7 @@ function DialogContent({
) {
return (
diff --git a/src/components/ui/input-group.tsx b/src/components/ui/input-group.tsx
index 1161ffe..c43b3bf 100644
--- a/src/components/ui/input-group.tsx
+++ b/src/components/ui/input-group.tsx
@@ -1,23 +1,23 @@
-import * as React from "react"
-import { cva, type VariantProps } from "class-variance-authority"
+import { cva, type VariantProps } from 'class-variance-authority';
+import * as React from 'react';
-import { cn } from "@/lib/utils"
-import { Button } from "@/components/ui/button"
-import { Input } from "@/components/ui/input"
-import { Textarea } from "@/components/ui/textarea"
+import { Button } from '@/components/ui/button';
+import { Input } from '@/components/ui/input';
+import { Textarea } from '@/components/ui/textarea';
+import { cn } from '@lib/utils';
-function InputGroup({ className, ...props }: React.ComponentProps<"div">) {
+function InputGroup({ className, ...props }: React.ComponentProps<'div'>) {
return (
[data-align=block-end]]:h-auto has-[>[data-align=block-end]]:flex-col has-[>[data-align=block-start]]:h-auto has-[>[data-align=block-start]]:flex-col has-[>[data-align=block-end]]:[&>input]:pt-3 has-[>[data-align=block-start]]:[&>input]:pb-3 has-[>[data-align=inline-end]]:[&>input]:pr-1.5 has-[>[data-align=inline-start]]:[&>input]:pl-1.5 [[data-slot=combobox-content]_&]:focus-within:border-inherit [[data-slot=combobox-content]_&]:focus-within:ring-0 group/input-group relative flex w-full min-w-0 items-center outline-none has-[>textarea]:h-auto",
- className
+ 'border-input bg-input/20 dark:bg-input/30 has-[[data-slot=input-group-control]:focus-visible]:border-ring has-[[data-slot=input-group-control]:focus-visible]:ring-ring/30 has-[[data-slot][aria-invalid=true]]:ring-destructive/20 has-[[data-slot][aria-invalid=true]]:border-destructive dark:has-[[data-slot][aria-invalid=true]]:ring-destructive/40 h-7 rounded-md border transition-colors has-data-[align=block-end]:rounded-md has-data-[align=block-start]:rounded-md has-[[data-slot=input-group-control]:focus-visible]:ring-2 has-[[data-slot][aria-invalid=true]]:ring-2 has-[textarea]:rounded-md has-[>[data-align=block-end]]:h-auto has-[>[data-align=block-end]]:flex-col has-[>[data-align=block-start]]:h-auto has-[>[data-align=block-start]]:flex-col has-[>[data-align=block-end]]:[&>input]:pt-3 has-[>[data-align=block-start]]:[&>input]:pb-3 has-[>[data-align=inline-end]]:[&>input]:pr-1.5 has-[>[data-align=inline-start]]:[&>input]:pl-1.5 in-data-[slot=combobox-content]:focus-within:border-inherit in-data-[slot=combobox-content]:focus-within:ring-0 group/input-group relative flex w-full min-w-0 items-center outline-none has-[>textarea]:h-auto',
+ className,
)}
{...props}
/>
- )
+ );
}
const inputGroupAddonVariants = cva(
@@ -25,25 +25,27 @@ const inputGroupAddonVariants = cva(
{
variants: {
align: {
- "inline-start": "pl-2 has-[>button]:ml-[-0.275rem] has-[>kbd]:ml-[-0.275rem] order-first",
- "inline-end": "pr-2 has-[>button]:mr-[-0.275rem] has-[>kbd]:mr-[-0.275rem] order-last",
- "block-start":
- "px-2 pt-2 group-has-[>input]/input-group:pt-2 [.border-b]:pb-2 order-first w-full justify-start",
- "block-end":
- "px-2 pb-2 group-has-[>input]/input-group:pb-2 [.border-t]:pt-2 order-last w-full justify-start",
+ 'inline-start':
+ 'pl-2 has-[>button]:ml-[-0.275rem] has-[>kbd]:ml-[-0.275rem] order-first',
+ 'inline-end':
+ 'pr-2 has-[>button]:mr-[-0.275rem] has-[>kbd]:mr-[-0.275rem] order-last',
+ 'block-start':
+ 'px-2 pt-2 group-has-[>input]/input-group:pt-2 [.border-b]:pb-2 order-first w-full justify-start',
+ 'block-end':
+ 'px-2 pb-2 group-has-[>input]/input-group:pb-2 [.border-t]:pt-2 order-last w-full justify-start',
},
},
defaultVariants: {
- align: "inline-start",
+ align: 'inline-start',
},
- }
-)
+ },
+);
function InputGroupAddon({
className,
- align = "inline-start",
+ align = 'inline-start',
...props
-}: React.ComponentProps<"div"> & VariantProps
) {
+}: React.ComponentProps<'div'> & VariantProps) {
return (
{
- if ((e.target as HTMLElement).closest("button")) {
- return
+ if ((e.target as HTMLElement).closest('button')) {
+ return;
}
- e.currentTarget.parentElement?.querySelector("input")?.focus()
+ e.currentTarget.parentElement?.querySelector('input')?.focus();
}}
{...props}
/>
- )
+ );
}
const inputGroupButtonVariants = cva(
- "gap-2 rounded-md text-xs/relaxed shadow-none flex items-center",
+ 'gap-2 rounded-md text-xs/relaxed shadow-none flex items-center',
{
variants: {
size: {
xs: "h-5 gap-1 rounded-[calc(var(--radius-sm)-2px)] px-1 [&>svg:not([class*='size-'])]:size-3",
- sm: "",
- "icon-xs": "size-6 p-0 has-[>svg]:p-0",
- "icon-sm": "size-8 p-0 has-[>svg]:p-0",
+ sm: '',
+ 'icon-xs': 'size-6 p-0 has-[>svg]:p-0',
+ 'icon-sm': 'size-8 p-0 has-[>svg]:p-0',
},
},
defaultVariants: {
- size: "xs",
+ size: 'xs',
},
- }
-)
+ },
+);
function InputGroupButton({
className,
- type = "button",
- variant = "ghost",
- size = "xs",
+ type = 'button',
+ variant = 'ghost',
+ size = 'xs',
...props
-}: Omit
, "size"> &
+}: Omit, 'size'> &
VariantProps) {
return (
- )
+ );
}
-function InputGroupText({ className, ...props }: React.ComponentProps<"span">) {
+function InputGroupText({ className, ...props }: React.ComponentProps<'span'>) {
return (
- )
+ );
}
function InputGroupInput({
className,
...props
-}: React.ComponentProps<"input">) {
+}: React.ComponentProps<'input'>) {
return (
- )
+ );
}
function InputGroupTextarea({
className,
...props
-}: React.ComponentProps<"textarea">) {
+}: React.ComponentProps<'textarea'>) {
return (
- )
+ );
}
export {
InputGroup,
InputGroupAddon,
InputGroupButton,
- InputGroupText,
InputGroupInput,
+ InputGroupText,
InputGroupTextarea,
-}
+};
diff --git a/src/components/ui/input.tsx b/src/components/ui/input.tsx
index fd4a86e..4f4c1ff 100644
--- a/src/components/ui/input.tsx
+++ b/src/components/ui/input.tsx
@@ -1,8 +1,8 @@
-import * as React from "react"
+import * as React from 'react';
-import { cn } from "@/lib/utils"
+import { cn } from '@lib/utils';
-function Input({ className, type, ...props }: React.ComponentProps<"input">) {
+function Input({ className, type, ...props }: React.ComponentProps<'input'>) {
return (
) {
);
}
-export { Input }
+export { Input };
diff --git a/src/components/ui/item.tsx b/src/components/ui/item.tsx
new file mode 100644
index 0000000..b48762a
--- /dev/null
+++ b/src/components/ui/item.tsx
@@ -0,0 +1,196 @@
+import { cva, type VariantProps } from 'class-variance-authority';
+import { Slot } from 'radix-ui';
+import * as React from 'react';
+
+import { Separator } from '@/components/ui/separator';
+import { cn } from '@lib/utils';
+
+function ItemGroup({ className, ...props }: React.ComponentProps<'div'>) {
+ return (
+
+ );
+}
+
+function ItemSeparator({
+ className,
+ ...props
+}: React.ComponentProps) {
+ return (
+
+ );
+}
+
+const itemVariants = cva(
+ '[a]:hover:bg-muted rounded-md border text-xs/relaxed w-full group/item focus-visible:border-ring focus-visible:ring-ring/50 flex items-center flex-wrap outline-none transition-colors duration-100 focus-visible:ring-[3px] [a]:transition-colors',
+ {
+ variants: {
+ variant: {
+ default: 'border-transparent',
+ outline: 'border-border',
+ muted: 'bg-muted/50 border-transparent',
+ },
+ size: {
+ default: 'gap-2.5 px-3 py-2.5',
+ sm: 'gap-2.5 px-3 py-2.5',
+ xs: 'gap-2.5 px-2.5 py-2 [[data-slot=dropdown-menu-content]_&]:p-0',
+ },
+ },
+ defaultVariants: {
+ variant: 'default',
+ size: 'default',
+ },
+ },
+);
+
+function Item({
+ className,
+ variant = 'default',
+ size = 'default',
+ asChild = false,
+ ...props
+}: React.ComponentProps<'div'> &
+ VariantProps & { asChild?: boolean }) {
+ const Comp = asChild ? Slot.Root : 'div';
+ return (
+
+ );
+}
+
+const itemMediaVariants = cva(
+ 'gap-2 group-has-[[data-slot=item-description]]/item:translate-y-0.5 group-has-[[data-slot=item-description]]/item:self-start flex shrink-0 items-center justify-center [&_svg]:pointer-events-none',
+ {
+ variants: {
+ variant: {
+ default: 'bg-transparent',
+ icon: "[&_svg:not([class*='size-'])]:size-4",
+ image:
+ 'size-8 overflow-hidden rounded-sm group-data-[size=sm]/item:size-8 group-data-[size=xs]/item:size-6 [&_img]:size-full [&_img]:object-cover',
+ },
+ },
+ defaultVariants: {
+ variant: 'default',
+ },
+ },
+);
+
+function ItemMedia({
+ className,
+ variant = 'default',
+ ...props
+}: React.ComponentProps<'div'> & VariantProps) {
+ return (
+
+ );
+}
+
+function ItemContent({ className, ...props }: React.ComponentProps<'div'>) {
+ return (
+
+ );
+}
+
+function ItemTitle({ className, ...props }: React.ComponentProps<'div'>) {
+ return (
+
+ );
+}
+
+function ItemDescription({ className, ...props }: React.ComponentProps<'p'>) {
+ return (
+ a:hover]:text-primary line-clamp-2 font-normal [&>a]:underline [&>a]:underline-offset-4',
+ className,
+ )}
+ {...props}
+ />
+ );
+}
+
+function ItemActions({ className, ...props }: React.ComponentProps<'div'>) {
+ return (
+
+ );
+}
+
+function ItemHeader({ className, ...props }: React.ComponentProps<'div'>) {
+ return (
+
+ );
+}
+
+function ItemFooter({ className, ...props }: React.ComponentProps<'div'>) {
+ return (
+
+ );
+}
+
+export {
+ Item,
+ ItemActions,
+ ItemContent,
+ ItemDescription,
+ ItemFooter,
+ ItemGroup,
+ ItemHeader,
+ ItemMedia,
+ ItemSeparator,
+ ItemTitle,
+};
diff --git a/src/components/ui/label.tsx b/src/components/ui/label.tsx
index 64dbf64..536674f 100644
--- a/src/components/ui/label.tsx
+++ b/src/components/ui/label.tsx
@@ -1,7 +1,7 @@
-import * as React from "react"
-import { Label as LabelPrimitive } from "radix-ui"
+import { Label as LabelPrimitive } from 'radix-ui';
+import * as React from 'react';
-import { cn } from "@/lib/utils"
+import { cn } from '@lib/utils';
function Label({
className,
@@ -11,12 +11,12 @@ function Label({
- )
+ );
}
-export { Label }
+export { Label };
diff --git a/src/components/ui/scroll-area.tsx b/src/components/ui/scroll-area.tsx
new file mode 100644
index 0000000..605ef01
--- /dev/null
+++ b/src/components/ui/scroll-area.tsx
@@ -0,0 +1,53 @@
+import * as React from "react"
+import { ScrollArea as ScrollAreaPrimitive } from "radix-ui"
+
+import { cn } from "@/lib/utils"
+
+function ScrollArea({
+ className,
+ children,
+ ...props
+}: React.ComponentProps) {
+ return (
+
+
+ {children}
+
+
+
+
+ )
+}
+
+function ScrollBar({
+ className,
+ orientation = "vertical",
+ ...props
+}: React.ComponentProps) {
+ return (
+
+
+
+ )
+}
+
+export { ScrollArea, ScrollBar }
diff --git a/src/components/ui/search-input.tsx b/src/components/ui/search-input.tsx
index ba6a540..c580b31 100644
--- a/src/components/ui/search-input.tsx
+++ b/src/components/ui/search-input.tsx
@@ -14,7 +14,7 @@ const SearchInput = ({ keywords, setKeyword, onChange }: SearchInputProps) => {
};
return (
-
+
+ `${u.name} - ${u.email}`;
+
+type SelectUserProps = {
+ value: string;
+ onValueChange: (userId: string) => void;
+ values: SelectUserItem[];
+ placeholder?: string;
+ /** Khi truyền cùng onKeywordChange: tìm kiếm theo API (keyword gửi lên server) */
+ keyword?: string;
+ onKeywordChange?: (value: string) => void;
+ searchPlaceholder?: string;
+ name?: string;
+ id?: string;
+ 'aria-invalid'?: boolean;
+ disabled?: boolean;
+ className?: string;
+ selectKey?: 'id' | 'email';
+};
+
+export function SelectUser({
+ value,
+ onValueChange,
+ values,
+ placeholder,
+ keyword,
+ onKeywordChange,
+ searchPlaceholder = 'Tìm theo tên hoặc email...',
+ name,
+ id,
+ 'aria-invalid': ariaInvalid,
+ disabled = false,
+ className,
+ selectKey = 'id',
+}: SelectUserProps) {
+ const [open, setOpen] = useState(false);
+ const [localQuery, setLocalQuery] = useState('');
+ const wrapperRef = useRef(null);
+ const searchInputRef = useRef(null);
+
+ const useServerSearch = keyword !== undefined && onKeywordChange != null;
+ const searchValue = useServerSearch ? keyword : localQuery;
+ const setSearchValue = useServerSearch ? onKeywordChange! : setLocalQuery;
+
+ const selectedUser =
+ value != null && value !== ''
+ ? values.find((u) => u[selectKey] === value)
+ : null;
+ const displayValue = selectedUser ? userLabel(selectedUser) : '';
+
+ const filtered = useServerSearch
+ ? values
+ : (() => {
+ const q = localQuery.trim().toLowerCase();
+ return q === ''
+ ? values
+ : values.filter(
+ (u) =>
+ u.name.toLowerCase().includes(q) ||
+ u.email.toLowerCase().includes(q),
+ );
+ })();
+
+ const close = useCallback(() => {
+ setOpen(false);
+ if (!useServerSearch) setLocalQuery('');
+ }, [useServerSearch]);
+
+ useEffect(() => {
+ if (!open) return;
+ searchInputRef.current?.focus();
+ }, [open]);
+
+ useEffect(() => {
+ if (!open) return;
+ const onMouseDown = (e: MouseEvent) => {
+ if (
+ wrapperRef.current &&
+ !wrapperRef.current.contains(e.target as Node)
+ ) {
+ close();
+ }
+ };
+ document.addEventListener('mousedown', onMouseDown);
+ return () => document.removeEventListener('mousedown', onMouseDown);
+ }, [open, close]);
+
+ const handleSelect = (userId: string) => {
+ onValueChange(userId);
+ close();
+ };
+
+ const controlId = id ?? name;
+ const listboxId = controlId ? `${controlId}-listbox` : undefined;
+
+ return (
+
+ {name != null && (
+
+ )}
+
!disabled && setOpen((o) => !o)}
+ >
+
+ {displayValue || (
+ {placeholder}
+ )}
+
+
+
+ {open && (
+
+
+
+ setSearchValue(e.target.value)}
+ onKeyDown={(e) => e.stopPropagation()}
+ className="min-w-0 flex-1 bg-transparent py-1 text-xs outline-none placeholder:text-muted-foreground"
+ />
+
+
+ {filtered.length === 0 ? (
+
+ Không có kết quả
+
+ ) : (
+ filtered.map((u) => (
+
+ ))
+ )}
+
+
+ )}
+
+ );
+}
diff --git a/src/components/ui/select.tsx b/src/components/ui/select.tsx
index 7641172..cafbe78 100644
--- a/src/components/ui/select.tsx
+++ b/src/components/ui/select.tsx
@@ -1,7 +1,7 @@
import { Select as SelectPrimitive } from 'radix-ui';
import * as React from 'react';
-import { cn } from '@/lib/utils';
+import { cn } from '@lib/utils';
import { CaretDownIcon, CaretUpIcon, CheckIcon } from '@phosphor-icons/react';
function Select({
diff --git a/src/components/ui/separator.tsx b/src/components/ui/separator.tsx
index 19e4c9a..c39dd9c 100644
--- a/src/components/ui/separator.tsx
+++ b/src/components/ui/separator.tsx
@@ -1,11 +1,11 @@
-import * as React from "react"
-import { Separator as SeparatorPrimitive } from "radix-ui"
+import { Separator as SeparatorPrimitive } from 'radix-ui';
+import * as React from 'react';
-import { cn } from "@/lib/utils"
+import { cn } from '@lib/utils';
function Separator({
className,
- orientation = "horizontal",
+ orientation = 'horizontal',
decorative = true,
...props
}: React.ComponentProps) {
@@ -15,12 +15,12 @@ function Separator({
decorative={decorative}
orientation={orientation}
className={cn(
- "bg-border shrink-0 data-[orientation=horizontal]:h-px data-[orientation=horizontal]:w-full data-[orientation=vertical]:w-px data-[orientation=vertical]:self-stretch",
- className
+ 'bg-border shrink-0 data-[orientation=horizontal]:h-px data-[orientation=horizontal]:w-full data-[orientation=vertical]:w-px data-[orientation=vertical]:self-stretch',
+ className,
)}
{...props}
/>
- )
+ );
}
-export { Separator }
+export { Separator };
diff --git a/src/components/ui/sheet.tsx b/src/components/ui/sheet.tsx
index c9624cb..2b5b81f 100644
--- a/src/components/ui/sheet.tsx
+++ b/src/components/ui/sheet.tsx
@@ -1,30 +1,30 @@
-import * as React from "react"
-import { Dialog as SheetPrimitive } from "radix-ui"
+import { Dialog as SheetPrimitive } from 'radix-ui';
+import * as React from 'react';
-import { cn } from "@/lib/utils"
-import { Button } from "@/components/ui/button"
-import { XIcon } from "@phosphor-icons/react"
+import { Button } from '@/components/ui/button';
+import { cn } from '@lib/utils';
+import { XIcon } from '@phosphor-icons/react';
function Sheet({ ...props }: React.ComponentProps) {
- return
+ return ;
}
function SheetTrigger({
...props
}: React.ComponentProps) {
- return
+ return ;
}
function SheetClose({
...props
}: React.ComponentProps) {
- return
+ return ;
}
function SheetPortal({
...props
}: React.ComponentProps) {
- return
+ return ;
}
function SheetOverlay({
@@ -34,21 +34,24 @@ function SheetOverlay({
return (
- )
+ );
}
function SheetContent({
className,
children,
- side = "right",
+ side = 'right',
showCloseButton = true,
...props
}: React.ComponentProps & {
- side?: "top" | "right" | "bottom" | "left"
- showCloseButton?: boolean
+ side?: 'top' | 'right' | 'bottom' | 'left';
+ showCloseButton?: boolean;
}) {
return (
@@ -56,42 +59,48 @@ function SheetContent({
{children}
{showCloseButton && (
-
)}
- )
+ );
}
-function SheetHeader({ className, ...props }: React.ComponentProps<"div">) {
+function SheetHeader({ className, ...props }: React.ComponentProps<'div'>) {
return (
- )
+ );
}
-function SheetFooter({ className, ...props }: React.ComponentProps<"div">) {
+function SheetFooter({ className, ...props }: React.ComponentProps<'div'>) {
return (
- )
+ );
}
function SheetTitle({
@@ -101,10 +110,10 @@ function SheetTitle({
return (
- )
+ );
}
function SheetDescription({
@@ -114,19 +123,19 @@ function SheetDescription({
return (
- )
+ );
}
export {
Sheet,
- SheetTrigger,
SheetClose,
SheetContent,
- SheetHeader,
- SheetFooter,
- SheetTitle,
SheetDescription,
-}
+ SheetFooter,
+ SheetHeader,
+ SheetTitle,
+ SheetTrigger,
+};
diff --git a/src/components/ui/sidebar.tsx b/src/components/ui/sidebar.tsx
index 2eb4aa8..2163d48 100644
--- a/src/components/ui/sidebar.tsx
+++ b/src/components/ui/sidebar.tsx
@@ -20,8 +20,8 @@ import {
TooltipContent,
TooltipTrigger,
} from '@/components/ui/tooltip';
-import { useIsMobile } from '@/hooks/use-mobile';
-import { cn } from '@/lib/utils';
+import { useIsMobile } from '@hooks/use-mobile';
+import { cn } from '@lib/utils';
import { SidebarIcon } from '@phosphor-icons/react';
const SIDEBAR_COOKIE_NAME = 'sidebar_state';
diff --git a/src/components/ui/skeleton.tsx b/src/components/ui/skeleton.tsx
index 41bcbf9..e9986b6 100644
--- a/src/components/ui/skeleton.tsx
+++ b/src/components/ui/skeleton.tsx
@@ -1,13 +1,13 @@
-import { cn } from "@/lib/utils"
+import { cn } from '@lib/utils';
-function Skeleton({ className, ...props }: React.ComponentProps<"div">) {
+function Skeleton({ className, ...props }: React.ComponentProps<'div'>) {
return (
- )
+ );
}
-export { Skeleton }
+export { Skeleton };
diff --git a/src/components/ui/spinner.tsx b/src/components/ui/spinner.tsx
new file mode 100644
index 0000000..dab07e1
--- /dev/null
+++ b/src/components/ui/spinner.tsx
@@ -0,0 +1,10 @@
+import { cn } from "@/lib/utils"
+import { SpinnerIcon } from "@phosphor-icons/react"
+
+function Spinner({ className, ...props }: React.ComponentProps<"svg">) {
+ return (
+
+ )
+}
+
+export { Spinner }
diff --git a/src/components/ui/table.tsx b/src/components/ui/table.tsx
index 4886601..c47f69a 100644
--- a/src/components/ui/table.tsx
+++ b/src/components/ui/table.tsx
@@ -1,99 +1,114 @@
import * as React from "react"
-import { cn } from "@/lib/utils"
+import { cn } from '@lib/utils';
-function Table({ className, ...props }: React.ComponentProps<"table">) {
+function Table({ className, ...props }: React.ComponentProps<'table'>) {
return (
-
+
- )
+ );
}
-function TableHeader({ className, ...props }: React.ComponentProps<"thead">) {
+function TableHeader({ className, ...props }: React.ComponentProps<'thead'>) {
return (
- )
+ );
}
-function TableBody({ className, ...props }: React.ComponentProps<"tbody">) {
+function TableBody({ className, ...props }: React.ComponentProps<'tbody'>) {
return (
- )
+ );
}
-function TableFooter({ className, ...props }: React.ComponentProps<"tfoot">) {
+function TableFooter({ className, ...props }: React.ComponentProps<'tfoot'>) {
return (
tr]:last:border-b-0", className)}
+ className={cn(
+ 'bg-muted/50 border-t font-medium [&>tr]:last:border-b-0',
+ className,
+ )}
{...props}
/>
- )
+ );
}
-function TableRow({ className, ...props }: React.ComponentProps<"tr">) {
+function TableRow({ className, ...props }: React.ComponentProps<'tr'>) {
return (
- )
+ );
}
-function TableHead({ className, ...props }: React.ComponentProps<"th">) {
+function TableHead({ className, ...props }: React.ComponentProps<'th'>) {
return (
|
- )
+ );
}
-function TableCell({ className, ...props }: React.ComponentProps<"td">) {
+function TableCell({ className, ...props }: React.ComponentProps<'td'>) {
return (
|
- )
+ );
}
function TableCaption({
className,
...props
-}: React.ComponentProps<"caption">) {
+}: React.ComponentProps<'caption'>) {
return (
- )
+ );
}
export {
Table,
- TableHeader,
TableBody,
+ TableCaption,
+ TableCell,
TableFooter,
TableHead,
+ TableHeader,
TableRow,
- TableCell,
- TableCaption,
-}
+};
diff --git a/src/components/ui/textarea.tsx b/src/components/ui/textarea.tsx
index d8c3757..eab2139 100644
--- a/src/components/ui/textarea.tsx
+++ b/src/components/ui/textarea.tsx
@@ -1,6 +1,6 @@
import * as React from "react"
-import { cn } from "@/lib/utils"
+import { cn } from '@lib/utils';
function Textarea({ className, ...props }: React.ComponentProps<"textarea">) {
return (
diff --git a/src/components/ui/tooltip.tsx b/src/components/ui/tooltip.tsx
index a165749..5197800 100644
--- a/src/components/ui/tooltip.tsx
+++ b/src/components/ui/tooltip.tsx
@@ -3,7 +3,7 @@
import { Tooltip as TooltipPrimitive } from 'radix-ui'
import * as React from 'react'
-import { cn } from "@/lib/utils"
+import { cn } from '@lib/utils';
function TooltipProvider({
delayDuration = 0,
diff --git a/src/components/user/add-new-user-dialog.tsx b/src/components/user/add-new-user-dialog.tsx
index 9ff36f0..c42a642 100644
--- a/src/components/user/add-new-user-dialog.tsx
+++ b/src/components/user/add-new-user-dialog.tsx
@@ -1,9 +1,9 @@
-import usePreventAutoFocus from '@/hooks/use-prevent-auto-focus';
-import { m } from '@/paraglide/messages';
+import AdminCreateUserForm from '@form/user/admin-create-user-form';
+import useHasPermission from '@hooks/use-has-permission';
+import usePreventAutoFocus from '@hooks/use-prevent-auto-focus';
+import { m } from '@paraglide/messages';
import { PlusIcon } from '@phosphor-icons/react';
-import { useState } from 'react';
-import AdminCreateUserForm from '../form/admin-create-user-form';
-import { Button } from '../ui/button';
+import { Button } from '@ui/button';
import {
Dialog,
DialogContent,
@@ -11,38 +11,46 @@ import {
DialogHeader,
DialogTitle,
DialogTrigger,
-} from '../ui/dialog';
+} from '@ui/dialog';
+import { useState } from 'react';
const AddNewUserButton = () => {
+ const { hasPermission, isLoading } = useHasPermission('user', 'create');
const [_open, _setOpen] = useState(false);
const prevent = usePreventAutoFocus();
- return (
-
- );
+
+
+ e.preventDefault()}
+ >
+
+
+
+ {m.nav_add_new()}
+
+
+ {m.nav_add_new()}
+
+
+
+
+
+ );
+ }
+
+ return null;
};
export default AddNewUserButton;
diff --git a/src/components/user/ban-user-confirm-dialog.tsx b/src/components/user/ban-user-confirm-dialog.tsx
index 44aecd6..7c42265 100644
--- a/src/components/user/ban-user-confirm-dialog.tsx
+++ b/src/components/user/ban-user-confirm-dialog.tsx
@@ -1,14 +1,10 @@
-import usePreventAutoFocus from '@/hooks/use-prevent-auto-focus';
-import { m } from '@/paraglide/messages';
-import { usersQueries } from '@/service/queries';
-import { banUser } from '@/service/user.api';
-import { ReturnError } from '@/types/common';
+import usePreventAutoFocus from '@hooks/use-prevent-auto-focus';
+import { m } from '@paraglide/messages';
import { ShieldWarningIcon } from '@phosphor-icons/react';
+import { usersQueries } from '@service/queries';
+import { banUser } from '@service/user.api';
import { useMutation, useQueryClient } from '@tanstack/react-query';
-import { UserWithRole } from 'better-auth/plugins';
-import { toast } from 'sonner';
-import DisplayBreakLineMessage from '../DisplayBreakLineMessage';
-import { Button } from '../ui/button';
+import { Button } from '@ui/button';
import {
Dialog,
DialogClose,
@@ -17,7 +13,18 @@ import {
DialogFooter,
DialogHeader,
DialogTitle,
-} from '../ui/dialog';
+} from '@ui/dialog';
+import { UserWithRole } from 'better-auth/plugins';
+import { toast } from 'sonner';
+import { Spinner } from '../ui/spinner';
+import {
+ Table,
+ TableBody,
+ TableCell,
+ TableHead,
+ TableHeader,
+ TableRow,
+} from '../ui/table';
import { useBanContext } from './ban-user-dialog';
type BanConfirmProps = {
@@ -29,7 +36,7 @@ const BanUserConfirm = ({ data }: BanConfirmProps) => {
const queryClient = useQueryClient();
const prevent = usePreventAutoFocus();
- const { mutate: banUserMutation } = useMutation({
+ const { mutate: banUserMutation, isPending } = useMutation({
mutationFn: banUser,
onSuccess: () => {
queryClient.refetchQueries({
@@ -42,7 +49,10 @@ const BanUserConfirm = ({ data }: BanConfirmProps) => {
},
onError: (error: ReturnError) => {
console.error(error);
- toast.error(m.backend_message({ code: error.code }), {
+ const code = error.code as Parameters<
+ typeof m.backend_message
+ >[0]['code'];
+ toast.error(m.backend_message({ code }), {
richColors: true,
});
},
@@ -58,6 +68,7 @@ const BanUserConfirm = ({ data }: BanConfirmProps) => {
showCloseButton={false}
{...prevent}
onPointerDownOutside={(e) => e.preventDefault()}
+ onEscapeKeyDown={(e) => e.preventDefault()}
>
@@ -70,23 +81,62 @@ const BanUserConfirm = ({ data }: BanConfirmProps) => {
{m.users_page_ui_dialog_alert_ban_title()}
-
- {m.users_page_ui_dialog_alert_description({
- name: data.name,
- email: data.email,
- })}
- {m.users_page_ui_dialog_alert_description_2({
- reason: submitData.banReason,
- exp: m.exp_time({ time: submitData.banExp }),
- })}
-
+
+
+
+
+
+ {m.users_page_ui_dialog_alert_description_title()}
+
+
+
+
+
+
+ {m.users_page_ui_table_header_name()}:
+
+ {data.name}
+
+
+
+ {m.users_page_ui_table_header_email()}:
+
+ {data.email}
+
+
+
+ {m.users_page_ui_form_ban_reason()}:
+
+ {submitData.banReason}
+
+
+
+ {m.users_page_ui_form_ban_exp()}:
+
+
+ {m.exp_time({
+ time: submitData.banExp.toString() as Parameters<
+ typeof m.exp_time
+ >[0]['time'],
+ })}
+
+
+
+
+
{m.ui_cancel_btn()}
-
+
+ {isPending && }
{m.ui_confirm_btn()}
diff --git a/src/components/user/ban-user-dialog.tsx b/src/components/user/ban-user-dialog.tsx
index bde71b7..b399452 100644
--- a/src/components/user/ban-user-dialog.tsx
+++ b/src/components/user/ban-user-dialog.tsx
@@ -1,10 +1,10 @@
-import usePreventAutoFocus from '@/hooks/use-prevent-auto-focus';
-import { m } from '@/paraglide/messages';
+import BanUserForm from '@form/user/admin-ban-user-form';
+import useHasPermission from '@hooks/use-has-permission';
+import usePreventAutoFocus from '@hooks/use-prevent-auto-focus';
+import { m } from '@paraglide/messages';
import { LockIcon } from '@phosphor-icons/react';
-import { UserWithRole } from 'better-auth/plugins';
-import { createContext, useContext, useState } from 'react';
-import BanUserForm from '../form/admin-ban-user-form';
-import { Button } from '../ui/button';
+import { useRouteContext } from '@tanstack/react-router';
+import { Button } from '@ui/button';
import {
Dialog,
DialogContent,
@@ -12,9 +12,11 @@ import {
DialogHeader,
DialogTitle,
DialogTrigger,
-} from '../ui/dialog';
-import { Label } from '../ui/label';
-import { Tooltip, TooltipContent, TooltipTrigger } from '../ui/tooltip';
+} from '@ui/dialog';
+import { Label } from '@ui/label';
+import { Tooltip, TooltipContent, TooltipTrigger } from '@ui/tooltip';
+import { UserWithRole } from 'better-auth/plugins';
+import { createContext, useContext, useState } from 'react';
import BanUserConfirm from './ban-user-confirm-dialog';
type ChangeUserStatusProps = {
@@ -39,6 +41,9 @@ type BanContextProps = {
const BanContext = createContext(null);
const BanUserAction = ({ data }: ChangeUserStatusProps) => {
+ const { session } = useRouteContext({ from: '__root__' });
+ const isCurrentUser = session?.user.id === data.id;
+ const { hasPermission, isLoading } = useHasPermission('user', 'ban');
const [_open, _setOpen] = useState(false);
const [_openConfirm, _setOpenConfirm] = useState(false);
const [_confirmData, _setConfirmData] = useState({
@@ -48,56 +53,61 @@ const BanUserAction = ({ data }: ChangeUserStatusProps) => {
});
const prevent = usePreventAutoFocus();
- return (
-
-
-
-
-
-
+ if (isCurrentUser || isLoading) return null;
+
+ if (hasPermission) {
+ return (
+
+
+
+
+
+
+
+ {m.ui_ban_btn()}
+
+
+
+
+
+
+
+ e.preventDefault()}
+ >
+
+
- {m.ui_ban_btn()}
-
-
-
-
-
-
-
- e.preventDefault()}
- >
-
-
-
- {m.ui_ban_btn()}
-
-
- {m.ui_change_role_btn()}
-
-
-
-
-
-
-
- );
+ {m.ui_ban_btn()}
+
+
+ {m.ui_change_role_btn()}
+
+
+
+
+
+
+
+ );
+ }
+ return null;
};
export default BanUserAction;
diff --git a/src/components/user/change-role-dialog.tsx b/src/components/user/change-role-dialog.tsx
index 374fa11..f1a9d79 100644
--- a/src/components/user/change-role-dialog.tsx
+++ b/src/components/user/change-role-dialog.tsx
@@ -1,10 +1,9 @@
-import usePreventAutoFocus from '@/hooks/use-prevent-auto-focus';
-import { m } from '@/paraglide/messages';
+import AdminSetUserRoleForm from '@form/user/admin-set-user-role-form';
+import useHasPermission from '@hooks/use-has-permission';
+import usePreventAutoFocus from '@hooks/use-prevent-auto-focus';
+import { m } from '@paraglide/messages';
import { UserGearIcon } from '@phosphor-icons/react';
-import { UserWithRole } from 'better-auth/plugins';
-import { useState } from 'react';
-import AdminSetUserRoleForm from '../form/admin-set-user-role-form';
-import { Button } from '../ui/button';
+import { Button } from '@ui/button';
import {
Dialog,
DialogContent,
@@ -12,9 +11,11 @@ import {
DialogHeader,
DialogTitle,
DialogTrigger,
-} from '../ui/dialog';
-import { Label } from '../ui/label';
-import { Tooltip, TooltipContent, TooltipTrigger } from '../ui/tooltip';
+} from '@ui/dialog';
+import { Label } from '@ui/label';
+import { Tooltip, TooltipContent, TooltipTrigger } from '@ui/tooltip';
+import { UserWithRole } from 'better-auth/plugins';
+import { useState } from 'react';
type SetRoleProps = {
data: UserWithRole;
@@ -23,45 +24,50 @@ type SetRoleProps = {
const ChangeRoleAction = ({ data }: SetRoleProps) => {
const [_open, _setOpen] = useState(false);
const prevent = usePreventAutoFocus();
+ const { hasPermission, isLoading } = useHasPermission('user', 'set-role');
- return (
-
-
-
-
-
+ if (isLoading) return null;
+
+ if (hasPermission) {
+ return (
+
+
+
+
+
+
+ {m.ui_change_role_btn()}
+
+
+
+
+
+
+
+ e.preventDefault()}
+ >
+
+
- {m.ui_change_role_btn()}
-
-
-
-
-
-
-
- e.preventDefault()}
- >
-
-
-
- {m.ui_change_role_btn()}
-
-
- {m.ui_change_role_btn()}
-
-
-
-
-
- );
+ {m.ui_change_role_btn()}
+
+
+ {m.ui_change_role_btn()}
+
+
+
+
+
+ );
+ }
};
export default ChangeRoleAction;
diff --git a/src/components/user/edit-user-dialog.tsx b/src/components/user/edit-user-dialog.tsx
index b44abd3..85ce569 100644
--- a/src/components/user/edit-user-dialog.tsx
+++ b/src/components/user/edit-user-dialog.tsx
@@ -1,10 +1,9 @@
-import usePreventAutoFocus from '@/hooks/use-prevent-auto-focus';
-import { m } from '@/paraglide/messages';
+import AdminUpdateUserInfoForm from '@form/user/admin-update-user-info-form';
+import useHasPermission from '@hooks/use-has-permission';
+import usePreventAutoFocus from '@hooks/use-prevent-auto-focus';
+import { m } from '@paraglide/messages';
import { PenIcon } from '@phosphor-icons/react';
-import { UserWithRole } from 'better-auth/plugins';
-import { useState } from 'react';
-import AdminUpdateUserInfoForm from '../form/admin-update-user-info-form';
-import { Button } from '../ui/button';
+import { Button } from '@ui/button';
import {
Dialog,
DialogContent,
@@ -12,9 +11,11 @@ import {
DialogHeader,
DialogTitle,
DialogTrigger,
-} from '../ui/dialog';
-import { Label } from '../ui/label';
-import { Tooltip, TooltipContent, TooltipTrigger } from '../ui/tooltip';
+} from '@ui/dialog';
+import { Label } from '@ui/label';
+import { Tooltip, TooltipContent, TooltipTrigger } from '@ui/tooltip';
+import { UserWithRole } from 'better-auth/plugins';
+import { useState } from 'react';
type EditUserProps = {
data: UserWithRole;
@@ -23,44 +24,51 @@ type EditUserProps = {
const EditUserAction = ({ data }: EditUserProps) => {
const [_open, _setOpen] = useState(false);
const prevent = usePreventAutoFocus();
+ const { hasPermission, isLoading } = useHasPermission('user', 'update');
- return (
-
-
-
-
-
-
- {m.ui_edit_user_btn()}
-
-
-
-
-
-
-
- e.preventDefault()}
- >
-
-
- {m.ui_edit_user_btn()}
-
-
- {m.ui_edit_user_btn()}
-
-
-
-
-
- );
+ if (isLoading) return null;
+
+ if (hasPermission) {
+ return (
+
+
+
+
+
+
+ {m.ui_edit_user_btn()}
+
+
+
+
+
+
+
+ e.preventDefault()}
+ >
+
+
+ {m.ui_edit_user_btn()}
+
+
+ {m.ui_edit_user_btn()}
+
+
+
+
+
+ );
+ }
+
+ return null;
};
export default EditUserAction;
diff --git a/src/components/user/set-password-dialog.tsx b/src/components/user/set-password-dialog.tsx
index d4c618e..dc7469e 100644
--- a/src/components/user/set-password-dialog.tsx
+++ b/src/components/user/set-password-dialog.tsx
@@ -1,10 +1,9 @@
-import usePreventAutoFocus from '@/hooks/use-prevent-auto-focus';
-import { m } from '@/paraglide/messages';
+import AdminSetPasswordForm from '@form/user/admin-set-password-form';
+import useHasPermission from '@hooks/use-has-permission';
+import usePreventAutoFocus from '@hooks/use-prevent-auto-focus';
+import { m } from '@paraglide/messages';
import { KeyIcon } from '@phosphor-icons/react';
-import { UserWithRole } from 'better-auth/plugins';
-import { useState } from 'react';
-import AdminSetPasswordForm from '../form/admin-set-password-form';
-import { Button } from '../ui/button';
+import { Button } from '@ui/button';
import {
Dialog,
DialogContent,
@@ -12,9 +11,11 @@ import {
DialogHeader,
DialogTitle,
DialogTrigger,
-} from '../ui/dialog';
-import { Label } from '../ui/label';
-import { Tooltip, TooltipContent, TooltipTrigger } from '../ui/tooltip';
+} from '@ui/dialog';
+import { Label } from '@ui/label';
+import { Tooltip, TooltipContent, TooltipTrigger } from '@ui/tooltip';
+import { UserWithRole } from 'better-auth/plugins';
+import { useState } from 'react';
type UpdatePasswordProps = {
data: UserWithRole;
@@ -23,45 +24,50 @@ type UpdatePasswordProps = {
const SetPasswordAction = ({ data }: UpdatePasswordProps) => {
const [_open, _setOpen] = useState(false);
const prevent = usePreventAutoFocus();
+ const { hasPermission, isLoading } = useHasPermission('user', 'set-password');
- return (
-
-
-
-
-
-
- {m.ui_update_password_btn()}
-
-
-
-
-
-
-
- e.preventDefault()}
- >
-
-
-
- {m.ui_update_password_btn()}
-
-
- {m.ui_update_password_btn()}
-
-
-
-
-
- );
+ if (isLoading) return null;
+
+ if (hasPermission) {
+ return (
+
+
+
+
+
+
+ {m.ui_update_password_btn()}
+
+
+
+
+
+
+
+ e.preventDefault()}
+ >
+
+
+
+ {m.ui_update_password_btn()}
+
+
+ {m.ui_update_password_btn()}
+
+
+
+
+
+ );
+ }
};
export default SetPasswordAction;
diff --git a/src/components/user/unban-user-dialog.tsx b/src/components/user/unban-user-dialog.tsx
index 1ce57b4..3a50b80 100644
--- a/src/components/user/unban-user-dialog.tsx
+++ b/src/components/user/unban-user-dialog.tsx
@@ -1,15 +1,12 @@
-import usePreventAutoFocus from '@/hooks/use-prevent-auto-focus';
-import { m } from '@/paraglide/messages';
-import { usersQueries } from '@/service/queries';
-import { unbanUser } from '@/service/user.api';
-import { ReturnError } from '@/types/common';
+import useHasPermission from '@hooks/use-has-permission';
+import usePreventAutoFocus from '@hooks/use-prevent-auto-focus';
+import { m } from '@paraglide/messages';
import { LockOpenIcon, ShieldWarningIcon } from '@phosphor-icons/react';
+import { usersQueries } from '@service/queries';
+import { unbanUser } from '@service/user.api';
import { useMutation, useQueryClient } from '@tanstack/react-query';
-import { UserWithRole } from 'better-auth/plugins';
-import { useState } from 'react';
-import { toast } from 'sonner';
-import DisplayBreakLineMessage from '../DisplayBreakLineMessage';
-import { Button } from '../ui/button';
+import { useRouteContext } from '@tanstack/react-router';
+import { Button } from '@ui/button';
import {
Dialog,
DialogClose,
@@ -19,21 +16,36 @@ import {
DialogHeader,
DialogTitle,
DialogTrigger,
-} from '../ui/dialog';
-import { Label } from '../ui/label';
-import { Tooltip, TooltipContent, TooltipTrigger } from '../ui/tooltip';
+} from '@ui/dialog';
+import { Label } from '@ui/label';
+import { Tooltip, TooltipContent, TooltipTrigger } from '@ui/tooltip';
+import { UserWithRole } from 'better-auth/plugins';
+import { useState } from 'react';
+import { toast } from 'sonner';
+import { Spinner } from '../ui/spinner';
+import {
+ Table,
+ TableBody,
+ TableCell,
+ TableHead,
+ TableHeader,
+ TableRow,
+} from '../ui/table';
type UnbanUserProps = {
data: UserWithRole;
};
const UnbanUserAction = ({ data }: UnbanUserProps) => {
+ const { session } = useRouteContext({ from: '__root__' });
+ const isCurrentUser = session?.user.id === data.id;
+ const { hasPermission, isLoading } = useHasPermission('user', 'ban');
const queryClient = useQueryClient();
const [_open, _setOpen] = useState(false);
const prevent = usePreventAutoFocus();
- const { mutate: unbanMutation } = useMutation({
+ const { mutate: unbanMutation, isPending } = useMutation({
mutationFn: unbanUser,
onSuccess: () => {
queryClient.refetchQueries({
@@ -49,7 +61,10 @@ const UnbanUserAction = ({ data }: UnbanUserProps) => {
},
onError: (error: ReturnError) => {
console.error(error);
- toast.error(m.backend_message({ code: error.code }), {
+ const code = error.code as Parameters<
+ typeof m.backend_message
+ >[0]['code'];
+ toast.error(m.backend_message({ code }), {
richColors: true,
});
},
@@ -59,61 +74,96 @@ const UnbanUserAction = ({ data }: UnbanUserProps) => {
unbanMutation({ data: { id: data.id } });
};
- return (
-
-
-
-
+ if (isCurrentUser || isLoading) return null;
+
+ if (hasPermission) {
+ return (
+
+
+
+
+
+
+ {m.ui_unban_btn()}
+
+
+
+
+
+
+
+ e.preventDefault()}
+ onEscapeKeyDown={(e) => e.preventDefault()}
+ >
+
+
+
+
+
+ {m.users_page_ui_dialog_alert_title()}
+
+
+ {m.users_page_ui_dialog_alert_title()}
+
+
+
+
+
+
+
+ {m.users_page_ui_dialog_alert_description_title()}
+
+
+
+
+
+
+ {m.users_page_ui_table_header_name()}:
+
+ {data.name}
+
+
+
+ {m.users_page_ui_table_header_email()}:
+
+ {data.email}
+
+
+
+
+
+
+
+ {m.ui_cancel_btn()}
+
+
-
- {m.ui_unban_btn()}
+ {isPending && }
+ {m.ui_confirm_btn()}
-
-
-
-
-
-
- e.preventDefault()}
- >
-
-
-
-
-
- {m.users_page_ui_dialog_alert_title()}
-
-
- {m.users_page_ui_dialog_alert_title()}
-
-
-
- {m.users_page_ui_dialog_alert_description({
- name: data.name,
- email: data.email,
- })}
-
-
-
-
- {m.ui_cancel_btn()}
-
-
-
- {m.ui_confirm_btn()}
-
-
-
-
- );
+
+
+
+ );
+ }
+
+ return null;
};
export default UnbanUserAction;
diff --git a/src/components/user/user-column.tsx b/src/components/user/user-column.tsx
index 9e796c4..72f64d8 100644
--- a/src/components/user/user-column.tsx
+++ b/src/components/user/user-column.tsx
@@ -1,7 +1,7 @@
-import { m } from '@/paraglide/messages';
-import { formatters } from '@/utils/formatters';
+import { m } from '@paraglide/messages';
import { CheckCircleIcon, XCircleIcon } from '@phosphor-icons/react';
import { ColumnDef } from '@tanstack/react-table';
+import { formatters } from '@utils/formatters';
import { UserWithRole } from 'better-auth/plugins';
import RoleBadge from '../avatar/role-badge';
import BanUserAction from './ban-user-dialog';
diff --git a/src/generated/prisma/browser.ts b/src/generated/prisma/browser.ts
index 3cf601e..ea251ed 100644
--- a/src/generated/prisma/browser.ts
+++ b/src/generated/prisma/browser.ts
@@ -62,3 +62,8 @@ export type Setting = Prisma.SettingModel
*
*/
export type Audit = Prisma.AuditModel
+/**
+ * Model Notification
+ *
+ */
+export type Notification = Prisma.NotificationModel
diff --git a/src/generated/prisma/client.ts b/src/generated/prisma/client.ts
index fe60bf9..afc81bd 100644
--- a/src/generated/prisma/client.ts
+++ b/src/generated/prisma/client.ts
@@ -84,3 +84,8 @@ export type Setting = Prisma.SettingModel
*
*/
export type Audit = Prisma.AuditModel
+/**
+ * Model Notification
+ *
+ */
+export type Notification = Prisma.NotificationModel
diff --git a/src/generated/prisma/internal/class.ts b/src/generated/prisma/internal/class.ts
index e9cc59f..33d3480 100644
--- a/src/generated/prisma/internal/class.ts
+++ b/src/generated/prisma/internal/class.ts
@@ -17,10 +17,10 @@ import type * as Prisma from "./prismaNamespace.ts"
const config: runtime.GetPrismaClientConfig = {
"previewFeatures": [],
- "clientVersion": "7.2.0",
- "engineVersion": "0c8ef2ce45c83248ab3df073180d5eda9e8be7a3",
+ "clientVersion": "7.3.0",
+ "engineVersion": "9d6ad21cbbceab97458517b147a6a09ff43aa735",
"activeProvider": "postgresql",
- "inlineSchema": "generator client {\n provider = \"prisma-client\"\n output = \"../src/generated/prisma\"\n}\n\ndatasource db {\n provider = \"postgresql\"\n}\n\nmodel User {\n id String @id @default(uuid())\n name String\n email String\n emailVerified Boolean @default(false)\n image String?\n createdAt DateTime @default(now())\n updatedAt DateTime @updatedAt\n sessions Session[]\n accounts Account[]\n audit Audit[]\n\n role String?\n banned Boolean? @default(false)\n banReason String?\n banExpires DateTime?\n\n members Member[]\n invitations Invitation[]\n\n @@unique([email])\n @@map(\"user\")\n}\n\nmodel Session {\n id String @id @default(uuid())\n expiresAt DateTime\n token String\n createdAt DateTime @default(now())\n updatedAt DateTime @updatedAt\n ipAddress String?\n userAgent String?\n userId String\n user User @relation(fields: [userId], references: [id], onDelete: Cascade)\n\n impersonatedBy String?\n\n activeOrganizationId String?\n\n @@unique([token])\n @@index([userId])\n @@map(\"session\")\n}\n\nmodel Account {\n id String @id @default(uuid())\n accountId String\n providerId String\n userId String\n user User @relation(fields: [userId], references: [id], onDelete: Cascade)\n accessToken String?\n refreshToken String?\n idToken String?\n accessTokenExpiresAt DateTime?\n refreshTokenExpiresAt DateTime?\n scope String?\n password String?\n createdAt DateTime @default(now())\n updatedAt DateTime @updatedAt\n\n @@index([userId])\n @@map(\"account\")\n}\n\nmodel Verification {\n id String @id @default(uuid())\n identifier String\n value String\n expiresAt DateTime\n createdAt DateTime @default(now())\n updatedAt DateTime @updatedAt\n\n @@index([identifier])\n @@map(\"verification\")\n}\n\nmodel Organization {\n id String @id @default(uuid())\n name String\n slug String\n logo String?\n createdAt DateTime\n metadata String?\n members Member[]\n invitations Invitation[]\n\n color String? @default(\"#000000\")\n\n @@unique([slug])\n @@map(\"organization\")\n}\n\nmodel Member {\n id String @id @default(uuid())\n organizationId String\n organization Organization @relation(fields: [organizationId], references: [id], onDelete: Cascade)\n userId String\n user User @relation(fields: [userId], references: [id], onDelete: Cascade)\n role String @default(\"member\")\n createdAt DateTime\n\n @@index([organizationId])\n @@index([userId])\n @@map(\"member\")\n}\n\nmodel Invitation {\n id String @id @default(uuid())\n organizationId String\n organization Organization @relation(fields: [organizationId], references: [id], onDelete: Cascade)\n email String\n role String?\n status String @default(\"pending\")\n expiresAt DateTime\n createdAt DateTime @default(now())\n inviterId String\n user User @relation(fields: [inviterId], references: [id], onDelete: Cascade)\n\n @@index([organizationId])\n @@index([email])\n @@map(\"invitation\")\n}\n\nmodel Setting {\n id String @id @default(uuid())\n key String @unique\n value String\n description String\n relation String @default(\"admin\")\n\n createdAt DateTime @default(now())\n updatedAt DateTime @updatedAt\n\n @@map(\"setting\")\n}\n\nmodel Audit {\n id String @id @default(uuid())\n userId String\n action String\n tableName String\n recordId String\n oldValue String?\n newValue String?\n createdAt DateTime @default(now())\n\n user User @relation(fields: [userId], references: [id], onDelete: Cascade)\n\n @@map(\"audit\")\n}\n",
+ "inlineSchema": "generator client {\n provider = \"prisma-client\"\n output = \"../src/generated/prisma\"\n}\n\ndatasource db {\n provider = \"postgresql\"\n}\n\nmodel User {\n id String @id @default(uuid())\n name String\n email String\n emailVerified Boolean @default(false)\n image String?\n createdAt DateTime @default(now()) @db.Timestamptz\n updatedAt DateTime @updatedAt @db.Timestamptz\n sessions Session[]\n accounts Account[]\n audit Audit[]\n notification Notification[]\n\n role String?\n banned Boolean? @default(false)\n banReason String?\n banExpires DateTime?\n\n members Member[]\n invitations Invitation[]\n\n @@unique([email])\n @@map(\"user\")\n}\n\nmodel Session {\n id String @id @default(uuid())\n expiresAt DateTime @db.Timestamptz\n token String\n createdAt DateTime @default(now()) @db.Timestamptz\n updatedAt DateTime @updatedAt @db.Timestamptz\n ipAddress String?\n userAgent String?\n userId String\n user User @relation(fields: [userId], references: [id], onDelete: Cascade)\n\n impersonatedBy String?\n\n activeOrganizationId String?\n\n @@unique([token])\n @@index([userId])\n @@map(\"session\")\n}\n\nmodel Account {\n id String @id @default(uuid())\n accountId String\n providerId String\n userId String\n user User @relation(fields: [userId], references: [id], onDelete: Cascade)\n accessToken String?\n refreshToken String?\n idToken String?\n accessTokenExpiresAt DateTime?\n refreshTokenExpiresAt DateTime?\n scope String?\n password String?\n createdAt DateTime @default(now()) @db.Timestamptz\n updatedAt DateTime @updatedAt @db.Timestamptz\n\n @@index([userId])\n @@map(\"account\")\n}\n\nmodel Verification {\n id String @id @default(uuid())\n identifier String\n value String\n expiresAt DateTime\n createdAt DateTime @default(now()) @db.Timestamptz\n updatedAt DateTime @updatedAt @db.Timestamptz\n\n @@index([identifier])\n @@map(\"verification\")\n}\n\nmodel Organization {\n id String @id @default(uuid())\n name String\n slug String\n logo String?\n createdAt DateTime @db.Timestamptz\n metadata String?\n members Member[]\n invitations Invitation[]\n\n color String? @default(\"#000000\")\n\n @@unique([slug])\n @@map(\"organization\")\n}\n\nmodel Member {\n id String @id @default(uuid())\n organizationId String\n organization Organization @relation(fields: [organizationId], references: [id], onDelete: Cascade)\n userId String\n user User @relation(fields: [userId], references: [id], onDelete: Cascade)\n role String @default(\"member\")\n createdAt DateTime @db.Timestamptz\n\n @@index([organizationId])\n @@index([userId])\n @@map(\"member\")\n}\n\nmodel Invitation {\n id String @id @default(uuid())\n organizationId String\n organization Organization @relation(fields: [organizationId], references: [id], onDelete: Cascade)\n email String\n role String?\n status String @default(\"pending\")\n expiresAt DateTime @db.Timestamptz\n createdAt DateTime @default(now()) @db.Timestamptz\n inviterId String\n user User @relation(fields: [inviterId], references: [id], onDelete: Cascade)\n\n @@index([organizationId])\n @@index([email])\n @@map(\"invitation\")\n}\n\nmodel Setting {\n id String @id @default(uuid())\n key String @unique\n value String\n description String\n relation String @default(\"admin\")\n\n createdAt DateTime @default(now()) @db.Timestamptz\n updatedAt DateTime @updatedAt @db.Timestamptz\n\n @@map(\"setting\")\n}\n\nmodel Audit {\n id String @id @default(uuid())\n userId String\n action String\n tableName String\n recordId String\n oldValue String?\n newValue String?\n createdAt DateTime @default(now()) @db.Timestamptz\n\n user User @relation(fields: [userId], references: [id], onDelete: Cascade)\n\n @@map(\"audit\")\n}\n\nmodel Notification {\n id String @id @default(uuid())\n userId String\n\n title String\n message String\n type String @default(\"system\")\n\n link String?\n metadata String?\n\n createdAt DateTime @default(now()) @db.Timestamptz\n readAt DateTime? @db.Timestamptz\n\n user User @relation(fields: [userId], references: [id], onDelete: Cascade)\n\n @@index([userId, readAt])\n @@index([readAt])\n @@map(\"notification\")\n}\n",
"runtimeDataModel": {
"models": {},
"enums": {},
@@ -28,7 +28,7 @@ const config: runtime.GetPrismaClientConfig = {
}
}
-config.runtimeDataModel = JSON.parse("{\"models\":{\"User\":{\"fields\":[{\"name\":\"id\",\"kind\":\"scalar\",\"type\":\"String\"},{\"name\":\"name\",\"kind\":\"scalar\",\"type\":\"String\"},{\"name\":\"email\",\"kind\":\"scalar\",\"type\":\"String\"},{\"name\":\"emailVerified\",\"kind\":\"scalar\",\"type\":\"Boolean\"},{\"name\":\"image\",\"kind\":\"scalar\",\"type\":\"String\"},{\"name\":\"createdAt\",\"kind\":\"scalar\",\"type\":\"DateTime\"},{\"name\":\"updatedAt\",\"kind\":\"scalar\",\"type\":\"DateTime\"},{\"name\":\"sessions\",\"kind\":\"object\",\"type\":\"Session\",\"relationName\":\"SessionToUser\"},{\"name\":\"accounts\",\"kind\":\"object\",\"type\":\"Account\",\"relationName\":\"AccountToUser\"},{\"name\":\"audit\",\"kind\":\"object\",\"type\":\"Audit\",\"relationName\":\"AuditToUser\"},{\"name\":\"role\",\"kind\":\"scalar\",\"type\":\"String\"},{\"name\":\"banned\",\"kind\":\"scalar\",\"type\":\"Boolean\"},{\"name\":\"banReason\",\"kind\":\"scalar\",\"type\":\"String\"},{\"name\":\"banExpires\",\"kind\":\"scalar\",\"type\":\"DateTime\"},{\"name\":\"members\",\"kind\":\"object\",\"type\":\"Member\",\"relationName\":\"MemberToUser\"},{\"name\":\"invitations\",\"kind\":\"object\",\"type\":\"Invitation\",\"relationName\":\"InvitationToUser\"}],\"dbName\":\"user\"},\"Session\":{\"fields\":[{\"name\":\"id\",\"kind\":\"scalar\",\"type\":\"String\"},{\"name\":\"expiresAt\",\"kind\":\"scalar\",\"type\":\"DateTime\"},{\"name\":\"token\",\"kind\":\"scalar\",\"type\":\"String\"},{\"name\":\"createdAt\",\"kind\":\"scalar\",\"type\":\"DateTime\"},{\"name\":\"updatedAt\",\"kind\":\"scalar\",\"type\":\"DateTime\"},{\"name\":\"ipAddress\",\"kind\":\"scalar\",\"type\":\"String\"},{\"name\":\"userAgent\",\"kind\":\"scalar\",\"type\":\"String\"},{\"name\":\"userId\",\"kind\":\"scalar\",\"type\":\"String\"},{\"name\":\"user\",\"kind\":\"object\",\"type\":\"User\",\"relationName\":\"SessionToUser\"},{\"name\":\"impersonatedBy\",\"kind\":\"scalar\",\"type\":\"String\"},{\"name\":\"activeOrganizationId\",\"kind\":\"scalar\",\"type\":\"String\"}],\"dbName\":\"session\"},\"Account\":{\"fields\":[{\"name\":\"id\",\"kind\":\"scalar\",\"type\":\"String\"},{\"name\":\"accountId\",\"kind\":\"scalar\",\"type\":\"String\"},{\"name\":\"providerId\",\"kind\":\"scalar\",\"type\":\"String\"},{\"name\":\"userId\",\"kind\":\"scalar\",\"type\":\"String\"},{\"name\":\"user\",\"kind\":\"object\",\"type\":\"User\",\"relationName\":\"AccountToUser\"},{\"name\":\"accessToken\",\"kind\":\"scalar\",\"type\":\"String\"},{\"name\":\"refreshToken\",\"kind\":\"scalar\",\"type\":\"String\"},{\"name\":\"idToken\",\"kind\":\"scalar\",\"type\":\"String\"},{\"name\":\"accessTokenExpiresAt\",\"kind\":\"scalar\",\"type\":\"DateTime\"},{\"name\":\"refreshTokenExpiresAt\",\"kind\":\"scalar\",\"type\":\"DateTime\"},{\"name\":\"scope\",\"kind\":\"scalar\",\"type\":\"String\"},{\"name\":\"password\",\"kind\":\"scalar\",\"type\":\"String\"},{\"name\":\"createdAt\",\"kind\":\"scalar\",\"type\":\"DateTime\"},{\"name\":\"updatedAt\",\"kind\":\"scalar\",\"type\":\"DateTime\"}],\"dbName\":\"account\"},\"Verification\":{\"fields\":[{\"name\":\"id\",\"kind\":\"scalar\",\"type\":\"String\"},{\"name\":\"identifier\",\"kind\":\"scalar\",\"type\":\"String\"},{\"name\":\"value\",\"kind\":\"scalar\",\"type\":\"String\"},{\"name\":\"expiresAt\",\"kind\":\"scalar\",\"type\":\"DateTime\"},{\"name\":\"createdAt\",\"kind\":\"scalar\",\"type\":\"DateTime\"},{\"name\":\"updatedAt\",\"kind\":\"scalar\",\"type\":\"DateTime\"}],\"dbName\":\"verification\"},\"Organization\":{\"fields\":[{\"name\":\"id\",\"kind\":\"scalar\",\"type\":\"String\"},{\"name\":\"name\",\"kind\":\"scalar\",\"type\":\"String\"},{\"name\":\"slug\",\"kind\":\"scalar\",\"type\":\"String\"},{\"name\":\"logo\",\"kind\":\"scalar\",\"type\":\"String\"},{\"name\":\"createdAt\",\"kind\":\"scalar\",\"type\":\"DateTime\"},{\"name\":\"metadata\",\"kind\":\"scalar\",\"type\":\"String\"},{\"name\":\"members\",\"kind\":\"object\",\"type\":\"Member\",\"relationName\":\"MemberToOrganization\"},{\"name\":\"invitations\",\"kind\":\"object\",\"type\":\"Invitation\",\"relationName\":\"InvitationToOrganization\"},{\"name\":\"color\",\"kind\":\"scalar\",\"type\":\"String\"}],\"dbName\":\"organization\"},\"Member\":{\"fields\":[{\"name\":\"id\",\"kind\":\"scalar\",\"type\":\"String\"},{\"name\":\"organizationId\",\"kind\":\"scalar\",\"type\":\"String\"},{\"name\":\"organization\",\"kind\":\"object\",\"type\":\"Organization\",\"relationName\":\"MemberToOrganization\"},{\"name\":\"userId\",\"kind\":\"scalar\",\"type\":\"String\"},{\"name\":\"user\",\"kind\":\"object\",\"type\":\"User\",\"relationName\":\"MemberToUser\"},{\"name\":\"role\",\"kind\":\"scalar\",\"type\":\"String\"},{\"name\":\"createdAt\",\"kind\":\"scalar\",\"type\":\"DateTime\"}],\"dbName\":\"member\"},\"Invitation\":{\"fields\":[{\"name\":\"id\",\"kind\":\"scalar\",\"type\":\"String\"},{\"name\":\"organizationId\",\"kind\":\"scalar\",\"type\":\"String\"},{\"name\":\"organization\",\"kind\":\"object\",\"type\":\"Organization\",\"relationName\":\"InvitationToOrganization\"},{\"name\":\"email\",\"kind\":\"scalar\",\"type\":\"String\"},{\"name\":\"role\",\"kind\":\"scalar\",\"type\":\"String\"},{\"name\":\"status\",\"kind\":\"scalar\",\"type\":\"String\"},{\"name\":\"expiresAt\",\"kind\":\"scalar\",\"type\":\"DateTime\"},{\"name\":\"createdAt\",\"kind\":\"scalar\",\"type\":\"DateTime\"},{\"name\":\"inviterId\",\"kind\":\"scalar\",\"type\":\"String\"},{\"name\":\"user\",\"kind\":\"object\",\"type\":\"User\",\"relationName\":\"InvitationToUser\"}],\"dbName\":\"invitation\"},\"Setting\":{\"fields\":[{\"name\":\"id\",\"kind\":\"scalar\",\"type\":\"String\"},{\"name\":\"key\",\"kind\":\"scalar\",\"type\":\"String\"},{\"name\":\"value\",\"kind\":\"scalar\",\"type\":\"String\"},{\"name\":\"description\",\"kind\":\"scalar\",\"type\":\"String\"},{\"name\":\"relation\",\"kind\":\"scalar\",\"type\":\"String\"},{\"name\":\"createdAt\",\"kind\":\"scalar\",\"type\":\"DateTime\"},{\"name\":\"updatedAt\",\"kind\":\"scalar\",\"type\":\"DateTime\"}],\"dbName\":\"setting\"},\"Audit\":{\"fields\":[{\"name\":\"id\",\"kind\":\"scalar\",\"type\":\"String\"},{\"name\":\"userId\",\"kind\":\"scalar\",\"type\":\"String\"},{\"name\":\"action\",\"kind\":\"scalar\",\"type\":\"String\"},{\"name\":\"tableName\",\"kind\":\"scalar\",\"type\":\"String\"},{\"name\":\"recordId\",\"kind\":\"scalar\",\"type\":\"String\"},{\"name\":\"oldValue\",\"kind\":\"scalar\",\"type\":\"String\"},{\"name\":\"newValue\",\"kind\":\"scalar\",\"type\":\"String\"},{\"name\":\"createdAt\",\"kind\":\"scalar\",\"type\":\"DateTime\"},{\"name\":\"user\",\"kind\":\"object\",\"type\":\"User\",\"relationName\":\"AuditToUser\"}],\"dbName\":\"audit\"}},\"enums\":{},\"types\":{}}")
+config.runtimeDataModel = JSON.parse("{\"models\":{\"User\":{\"fields\":[{\"name\":\"id\",\"kind\":\"scalar\",\"type\":\"String\"},{\"name\":\"name\",\"kind\":\"scalar\",\"type\":\"String\"},{\"name\":\"email\",\"kind\":\"scalar\",\"type\":\"String\"},{\"name\":\"emailVerified\",\"kind\":\"scalar\",\"type\":\"Boolean\"},{\"name\":\"image\",\"kind\":\"scalar\",\"type\":\"String\"},{\"name\":\"createdAt\",\"kind\":\"scalar\",\"type\":\"DateTime\"},{\"name\":\"updatedAt\",\"kind\":\"scalar\",\"type\":\"DateTime\"},{\"name\":\"sessions\",\"kind\":\"object\",\"type\":\"Session\",\"relationName\":\"SessionToUser\"},{\"name\":\"accounts\",\"kind\":\"object\",\"type\":\"Account\",\"relationName\":\"AccountToUser\"},{\"name\":\"audit\",\"kind\":\"object\",\"type\":\"Audit\",\"relationName\":\"AuditToUser\"},{\"name\":\"notification\",\"kind\":\"object\",\"type\":\"Notification\",\"relationName\":\"NotificationToUser\"},{\"name\":\"role\",\"kind\":\"scalar\",\"type\":\"String\"},{\"name\":\"banned\",\"kind\":\"scalar\",\"type\":\"Boolean\"},{\"name\":\"banReason\",\"kind\":\"scalar\",\"type\":\"String\"},{\"name\":\"banExpires\",\"kind\":\"scalar\",\"type\":\"DateTime\"},{\"name\":\"members\",\"kind\":\"object\",\"type\":\"Member\",\"relationName\":\"MemberToUser\"},{\"name\":\"invitations\",\"kind\":\"object\",\"type\":\"Invitation\",\"relationName\":\"InvitationToUser\"}],\"dbName\":\"user\"},\"Session\":{\"fields\":[{\"name\":\"id\",\"kind\":\"scalar\",\"type\":\"String\"},{\"name\":\"expiresAt\",\"kind\":\"scalar\",\"type\":\"DateTime\"},{\"name\":\"token\",\"kind\":\"scalar\",\"type\":\"String\"},{\"name\":\"createdAt\",\"kind\":\"scalar\",\"type\":\"DateTime\"},{\"name\":\"updatedAt\",\"kind\":\"scalar\",\"type\":\"DateTime\"},{\"name\":\"ipAddress\",\"kind\":\"scalar\",\"type\":\"String\"},{\"name\":\"userAgent\",\"kind\":\"scalar\",\"type\":\"String\"},{\"name\":\"userId\",\"kind\":\"scalar\",\"type\":\"String\"},{\"name\":\"user\",\"kind\":\"object\",\"type\":\"User\",\"relationName\":\"SessionToUser\"},{\"name\":\"impersonatedBy\",\"kind\":\"scalar\",\"type\":\"String\"},{\"name\":\"activeOrganizationId\",\"kind\":\"scalar\",\"type\":\"String\"}],\"dbName\":\"session\"},\"Account\":{\"fields\":[{\"name\":\"id\",\"kind\":\"scalar\",\"type\":\"String\"},{\"name\":\"accountId\",\"kind\":\"scalar\",\"type\":\"String\"},{\"name\":\"providerId\",\"kind\":\"scalar\",\"type\":\"String\"},{\"name\":\"userId\",\"kind\":\"scalar\",\"type\":\"String\"},{\"name\":\"user\",\"kind\":\"object\",\"type\":\"User\",\"relationName\":\"AccountToUser\"},{\"name\":\"accessToken\",\"kind\":\"scalar\",\"type\":\"String\"},{\"name\":\"refreshToken\",\"kind\":\"scalar\",\"type\":\"String\"},{\"name\":\"idToken\",\"kind\":\"scalar\",\"type\":\"String\"},{\"name\":\"accessTokenExpiresAt\",\"kind\":\"scalar\",\"type\":\"DateTime\"},{\"name\":\"refreshTokenExpiresAt\",\"kind\":\"scalar\",\"type\":\"DateTime\"},{\"name\":\"scope\",\"kind\":\"scalar\",\"type\":\"String\"},{\"name\":\"password\",\"kind\":\"scalar\",\"type\":\"String\"},{\"name\":\"createdAt\",\"kind\":\"scalar\",\"type\":\"DateTime\"},{\"name\":\"updatedAt\",\"kind\":\"scalar\",\"type\":\"DateTime\"}],\"dbName\":\"account\"},\"Verification\":{\"fields\":[{\"name\":\"id\",\"kind\":\"scalar\",\"type\":\"String\"},{\"name\":\"identifier\",\"kind\":\"scalar\",\"type\":\"String\"},{\"name\":\"value\",\"kind\":\"scalar\",\"type\":\"String\"},{\"name\":\"expiresAt\",\"kind\":\"scalar\",\"type\":\"DateTime\"},{\"name\":\"createdAt\",\"kind\":\"scalar\",\"type\":\"DateTime\"},{\"name\":\"updatedAt\",\"kind\":\"scalar\",\"type\":\"DateTime\"}],\"dbName\":\"verification\"},\"Organization\":{\"fields\":[{\"name\":\"id\",\"kind\":\"scalar\",\"type\":\"String\"},{\"name\":\"name\",\"kind\":\"scalar\",\"type\":\"String\"},{\"name\":\"slug\",\"kind\":\"scalar\",\"type\":\"String\"},{\"name\":\"logo\",\"kind\":\"scalar\",\"type\":\"String\"},{\"name\":\"createdAt\",\"kind\":\"scalar\",\"type\":\"DateTime\"},{\"name\":\"metadata\",\"kind\":\"scalar\",\"type\":\"String\"},{\"name\":\"members\",\"kind\":\"object\",\"type\":\"Member\",\"relationName\":\"MemberToOrganization\"},{\"name\":\"invitations\",\"kind\":\"object\",\"type\":\"Invitation\",\"relationName\":\"InvitationToOrganization\"},{\"name\":\"color\",\"kind\":\"scalar\",\"type\":\"String\"}],\"dbName\":\"organization\"},\"Member\":{\"fields\":[{\"name\":\"id\",\"kind\":\"scalar\",\"type\":\"String\"},{\"name\":\"organizationId\",\"kind\":\"scalar\",\"type\":\"String\"},{\"name\":\"organization\",\"kind\":\"object\",\"type\":\"Organization\",\"relationName\":\"MemberToOrganization\"},{\"name\":\"userId\",\"kind\":\"scalar\",\"type\":\"String\"},{\"name\":\"user\",\"kind\":\"object\",\"type\":\"User\",\"relationName\":\"MemberToUser\"},{\"name\":\"role\",\"kind\":\"scalar\",\"type\":\"String\"},{\"name\":\"createdAt\",\"kind\":\"scalar\",\"type\":\"DateTime\"}],\"dbName\":\"member\"},\"Invitation\":{\"fields\":[{\"name\":\"id\",\"kind\":\"scalar\",\"type\":\"String\"},{\"name\":\"organizationId\",\"kind\":\"scalar\",\"type\":\"String\"},{\"name\":\"organization\",\"kind\":\"object\",\"type\":\"Organization\",\"relationName\":\"InvitationToOrganization\"},{\"name\":\"email\",\"kind\":\"scalar\",\"type\":\"String\"},{\"name\":\"role\",\"kind\":\"scalar\",\"type\":\"String\"},{\"name\":\"status\",\"kind\":\"scalar\",\"type\":\"String\"},{\"name\":\"expiresAt\",\"kind\":\"scalar\",\"type\":\"DateTime\"},{\"name\":\"createdAt\",\"kind\":\"scalar\",\"type\":\"DateTime\"},{\"name\":\"inviterId\",\"kind\":\"scalar\",\"type\":\"String\"},{\"name\":\"user\",\"kind\":\"object\",\"type\":\"User\",\"relationName\":\"InvitationToUser\"}],\"dbName\":\"invitation\"},\"Setting\":{\"fields\":[{\"name\":\"id\",\"kind\":\"scalar\",\"type\":\"String\"},{\"name\":\"key\",\"kind\":\"scalar\",\"type\":\"String\"},{\"name\":\"value\",\"kind\":\"scalar\",\"type\":\"String\"},{\"name\":\"description\",\"kind\":\"scalar\",\"type\":\"String\"},{\"name\":\"relation\",\"kind\":\"scalar\",\"type\":\"String\"},{\"name\":\"createdAt\",\"kind\":\"scalar\",\"type\":\"DateTime\"},{\"name\":\"updatedAt\",\"kind\":\"scalar\",\"type\":\"DateTime\"}],\"dbName\":\"setting\"},\"Audit\":{\"fields\":[{\"name\":\"id\",\"kind\":\"scalar\",\"type\":\"String\"},{\"name\":\"userId\",\"kind\":\"scalar\",\"type\":\"String\"},{\"name\":\"action\",\"kind\":\"scalar\",\"type\":\"String\"},{\"name\":\"tableName\",\"kind\":\"scalar\",\"type\":\"String\"},{\"name\":\"recordId\",\"kind\":\"scalar\",\"type\":\"String\"},{\"name\":\"oldValue\",\"kind\":\"scalar\",\"type\":\"String\"},{\"name\":\"newValue\",\"kind\":\"scalar\",\"type\":\"String\"},{\"name\":\"createdAt\",\"kind\":\"scalar\",\"type\":\"DateTime\"},{\"name\":\"user\",\"kind\":\"object\",\"type\":\"User\",\"relationName\":\"AuditToUser\"}],\"dbName\":\"audit\"},\"Notification\":{\"fields\":[{\"name\":\"id\",\"kind\":\"scalar\",\"type\":\"String\"},{\"name\":\"userId\",\"kind\":\"scalar\",\"type\":\"String\"},{\"name\":\"title\",\"kind\":\"scalar\",\"type\":\"String\"},{\"name\":\"message\",\"kind\":\"scalar\",\"type\":\"String\"},{\"name\":\"type\",\"kind\":\"scalar\",\"type\":\"String\"},{\"name\":\"link\",\"kind\":\"scalar\",\"type\":\"String\"},{\"name\":\"metadata\",\"kind\":\"scalar\",\"type\":\"String\"},{\"name\":\"createdAt\",\"kind\":\"scalar\",\"type\":\"DateTime\"},{\"name\":\"readAt\",\"kind\":\"scalar\",\"type\":\"DateTime\"},{\"name\":\"user\",\"kind\":\"object\",\"type\":\"User\",\"relationName\":\"NotificationToUser\"}],\"dbName\":\"notification\"}},\"enums\":{},\"types\":{}}")
async function decodeBase64AsWasm(wasmBase64: string): Promise {
const { Buffer } = await import('node:buffer')
@@ -37,12 +37,14 @@ async function decodeBase64AsWasm(wasmBase64: string): Promise await import("@prisma/client/runtime/query_compiler_bg.postgresql.mjs"),
+ getRuntime: async () => await import("@prisma/client/runtime/query_compiler_fast_bg.postgresql.mjs"),
getQueryCompilerWasmModule: async () => {
- const { wasm } = await import("@prisma/client/runtime/query_compiler_bg.postgresql.wasm-base64.mjs")
+ const { wasm } = await import("@prisma/client/runtime/query_compiler_fast_bg.postgresql.wasm-base64.mjs")
return await decodeBase64AsWasm(wasm)
- }
+ },
+
+ importName: "./query_compiler_fast_bg.js"
}
@@ -263,6 +265,16 @@ export interface PrismaClient<
* ```
*/
get audit(): Prisma.AuditDelegate;
+
+ /**
+ * `prisma.notification`: Exposes CRUD operations for the **Notification** model.
+ * Example usage:
+ * ```ts
+ * // Fetch zero or more Notifications
+ * const notifications = await prisma.notification.findMany()
+ * ```
+ */
+ get notification(): Prisma.NotificationDelegate;
}
export function getPrismaClientClass(): PrismaClientConstructor {
diff --git a/src/generated/prisma/internal/prismaNamespace.ts b/src/generated/prisma/internal/prismaNamespace.ts
index 75881c6..7afbfe4 100644
--- a/src/generated/prisma/internal/prismaNamespace.ts
+++ b/src/generated/prisma/internal/prismaNamespace.ts
@@ -80,12 +80,12 @@ export type PrismaVersion = {
}
/**
- * Prisma Client JS version: 7.2.0
- * Query Engine version: 0c8ef2ce45c83248ab3df073180d5eda9e8be7a3
+ * Prisma Client JS version: 7.3.0
+ * Query Engine version: 9d6ad21cbbceab97458517b147a6a09ff43aa735
*/
export const prismaVersion: PrismaVersion = {
- client: "7.2.0",
- engine: "0c8ef2ce45c83248ab3df073180d5eda9e8be7a3"
+ client: "7.3.0",
+ engine: "9d6ad21cbbceab97458517b147a6a09ff43aa735"
}
/**
@@ -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
+ fields: Prisma.NotificationFieldRefs
+ operations: {
+ findUnique: {
+ args: Prisma.NotificationFindUniqueArgs
+ result: runtime.Types.Utils.PayloadToResult | null
+ }
+ findUniqueOrThrow: {
+ args: Prisma.NotificationFindUniqueOrThrowArgs
+ result: runtime.Types.Utils.PayloadToResult
+ }
+ findFirst: {
+ args: Prisma.NotificationFindFirstArgs
+ result: runtime.Types.Utils.PayloadToResult | null
+ }
+ findFirstOrThrow: {
+ args: Prisma.NotificationFindFirstOrThrowArgs
+ result: runtime.Types.Utils.PayloadToResult
+ }
+ findMany: {
+ args: Prisma.NotificationFindManyArgs
+ result: runtime.Types.Utils.PayloadToResult[]
+ }
+ create: {
+ args: Prisma.NotificationCreateArgs
+ result: runtime.Types.Utils.PayloadToResult
+ }
+ createMany: {
+ args: Prisma.NotificationCreateManyArgs
+ result: BatchPayload
+ }
+ createManyAndReturn: {
+ args: Prisma.NotificationCreateManyAndReturnArgs
+ result: runtime.Types.Utils.PayloadToResult[]
+ }
+ delete: {
+ args: Prisma.NotificationDeleteArgs
+ result: runtime.Types.Utils.PayloadToResult
+ }
+ update: {
+ args: Prisma.NotificationUpdateArgs
+ result: runtime.Types.Utils.PayloadToResult
+ }
+ deleteMany: {
+ args: Prisma.NotificationDeleteManyArgs
+ result: BatchPayload
+ }
+ updateMany: {
+ args: Prisma.NotificationUpdateManyArgs
+ result: BatchPayload
+ }
+ updateManyAndReturn: {
+ args: Prisma.NotificationUpdateManyAndReturnArgs
+ result: runtime.Types.Utils.PayloadToResult[]
+ }
+ upsert: {
+ args: Prisma.NotificationUpsertArgs
+ result: runtime.Types.Utils.PayloadToResult
+ }
+ aggregate: {
+ args: Prisma.NotificationAggregateArgs
+ result: runtime.Types.Utils.Optional
+ }
+ groupBy: {
+ args: Prisma.NotificationGroupByArgs
+ result: runtime.Types.Utils.Optional[]
+ }
+ count: {
+ args: Prisma.NotificationCountArgs
+ result: runtime.Types.Utils.Optional | 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 */
diff --git a/src/generated/prisma/internal/prismaNamespaceBrowser.ts b/src/generated/prisma/internal/prismaNamespaceBrowser.ts
index c765336..e1dc876 100644
--- a/src/generated/prisma/internal/prismaNamespaceBrowser.ts
+++ b/src/generated/prisma/internal/prismaNamespaceBrowser.ts
@@ -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]
@@ -68,12 +69,12 @@ export type ModelName = (typeof ModelName)[keyof typeof ModelName]
* Enums
*/
-export const TransactionIsolationLevel = {
+export const TransactionIsolationLevel = runtime.makeStrictEnum({
ReadUncommitted: 'ReadUncommitted',
ReadCommitted: 'ReadCommitted',
RepeatableRead: 'RepeatableRead',
Serializable: 'Serializable'
-} as const
+} as const)
export type TransactionIsolationLevel = (typeof TransactionIsolationLevel)[keyof typeof TransactionIsolationLevel]
@@ -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'
diff --git a/src/generated/prisma/models.ts b/src/generated/prisma/models.ts
index d7fa0ad..1bdae43 100644
--- a/src/generated/prisma/models.ts
+++ b/src/generated/prisma/models.ts
@@ -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'
\ No newline at end of file
diff --git a/src/generated/prisma/models/Notification.ts b/src/generated/prisma/models/Notification.ts
new file mode 100644
index 0000000..d19fe34
--- /dev/null
+++ b/src/generated/prisma/models/Notification.ts
@@ -0,0 +1,1480 @@
+
+/* !!! This is code generated by Prisma. Do not edit directly. !!! */
+/* eslint-disable */
+// biome-ignore-all lint: generated file
+// @ts-nocheck
+/*
+ * This file exports the `Notification` model and its related types.
+ *
+ * 🟢 You can import this file directly.
+ */
+import type * as runtime from "@prisma/client/runtime/client"
+import type * as $Enums from "../enums.ts"
+import type * as Prisma from "../internal/prismaNamespace.ts"
+
+/**
+ * Model Notification
+ *
+ */
+export type NotificationModel = runtime.Types.Result.DefaultSelection
+
+export type AggregateNotification = {
+ _count: NotificationCountAggregateOutputType | null
+ _min: NotificationMinAggregateOutputType | null
+ _max: NotificationMaxAggregateOutputType | null
+}
+
+export type NotificationMinAggregateOutputType = {
+ id: string | null
+ userId: string | null
+ title: string | null
+ message: string | null
+ type: string | null
+ link: string | null
+ metadata: string | null
+ createdAt: Date | null
+ readAt: Date | null
+}
+
+export type NotificationMaxAggregateOutputType = {
+ id: string | null
+ userId: string | null
+ title: string | null
+ message: string | null
+ type: string | null
+ link: string | null
+ metadata: string | null
+ createdAt: Date | null
+ readAt: Date | null
+}
+
+export type NotificationCountAggregateOutputType = {
+ id: number
+ userId: number
+ title: number
+ message: number
+ type: number
+ link: number
+ metadata: number
+ createdAt: number
+ readAt: number
+ _all: number
+}
+
+
+export type NotificationMinAggregateInputType = {
+ id?: true
+ userId?: true
+ title?: true
+ message?: true
+ type?: true
+ link?: true
+ metadata?: true
+ createdAt?: true
+ readAt?: true
+}
+
+export type NotificationMaxAggregateInputType = {
+ id?: true
+ userId?: true
+ title?: true
+ message?: true
+ type?: true
+ link?: true
+ metadata?: true
+ createdAt?: true
+ readAt?: true
+}
+
+export type NotificationCountAggregateInputType = {
+ id?: true
+ userId?: true
+ title?: true
+ message?: true
+ type?: true
+ link?: true
+ metadata?: true
+ createdAt?: true
+ readAt?: true
+ _all?: true
+}
+
+export type NotificationAggregateArgs = {
+ /**
+ * Filter which Notification to aggregate.
+ */
+ where?: Prisma.NotificationWhereInput
+ /**
+ * {@link https://www.prisma.io/docs/concepts/components/prisma-client/sorting Sorting Docs}
+ *
+ * Determine the order of Notifications to fetch.
+ */
+ orderBy?: Prisma.NotificationOrderByWithRelationInput | Prisma.NotificationOrderByWithRelationInput[]
+ /**
+ * {@link https://www.prisma.io/docs/concepts/components/prisma-client/pagination#cursor-based-pagination Cursor Docs}
+ *
+ * Sets the start position
+ */
+ cursor?: Prisma.NotificationWhereUniqueInput
+ /**
+ * {@link https://www.prisma.io/docs/concepts/components/prisma-client/pagination Pagination Docs}
+ *
+ * Take `±n` Notifications from the position of the cursor.
+ */
+ take?: number
+ /**
+ * {@link https://www.prisma.io/docs/concepts/components/prisma-client/pagination Pagination Docs}
+ *
+ * Skip the first `n` Notifications.
+ */
+ skip?: number
+ /**
+ * {@link https://www.prisma.io/docs/concepts/components/prisma-client/aggregations Aggregation Docs}
+ *
+ * Count returned Notifications
+ **/
+ _count?: true | NotificationCountAggregateInputType
+ /**
+ * {@link https://www.prisma.io/docs/concepts/components/prisma-client/aggregations Aggregation Docs}
+ *
+ * Select which fields to find the minimum value
+ **/
+ _min?: NotificationMinAggregateInputType
+ /**
+ * {@link https://www.prisma.io/docs/concepts/components/prisma-client/aggregations Aggregation Docs}
+ *
+ * Select which fields to find the maximum value
+ **/
+ _max?: NotificationMaxAggregateInputType
+}
+
+export type GetNotificationAggregateType = {
+ [P in keyof T & keyof AggregateNotification]: P extends '_count' | 'count'
+ ? T[P] extends true
+ ? number
+ : Prisma.GetScalarType
+ : Prisma.GetScalarType
+}
+
+
+
+
+export type NotificationGroupByArgs = {
+ where?: Prisma.NotificationWhereInput
+ orderBy?: Prisma.NotificationOrderByWithAggregationInput | Prisma.NotificationOrderByWithAggregationInput[]
+ by: Prisma.NotificationScalarFieldEnum[] | Prisma.NotificationScalarFieldEnum
+ having?: Prisma.NotificationScalarWhereWithAggregatesInput
+ take?: number
+ skip?: number
+ _count?: NotificationCountAggregateInputType | true
+ _min?: NotificationMinAggregateInputType
+ _max?: NotificationMaxAggregateInputType
+}
+
+export type NotificationGroupByOutputType = {
+ id: string
+ userId: string
+ title: string
+ message: string
+ type: string
+ link: string | null
+ metadata: string | null
+ createdAt: Date
+ readAt: Date | null
+ _count: NotificationCountAggregateOutputType | null
+ _min: NotificationMinAggregateOutputType | null
+ _max: NotificationMaxAggregateOutputType | null
+}
+
+type GetNotificationGroupByPayload = Prisma.PrismaPromise<
+ Array<
+ Prisma.PickEnumerable &
+ {
+ [P in ((keyof T) & (keyof NotificationGroupByOutputType))]: P extends '_count'
+ ? T[P] extends boolean
+ ? number
+ : Prisma.GetScalarType
+ : Prisma.GetScalarType
+ }
+ >
+ >
+
+
+
+export type NotificationWhereInput = {
+ AND?: Prisma.NotificationWhereInput | Prisma.NotificationWhereInput[]
+ OR?: Prisma.NotificationWhereInput[]
+ NOT?: Prisma.NotificationWhereInput | Prisma.NotificationWhereInput[]
+ id?: Prisma.StringFilter<"Notification"> | string
+ userId?: Prisma.StringFilter<"Notification"> | string
+ title?: Prisma.StringFilter<"Notification"> | string
+ message?: Prisma.StringFilter<"Notification"> | string
+ type?: Prisma.StringFilter<"Notification"> | string
+ link?: Prisma.StringNullableFilter<"Notification"> | string | null
+ metadata?: Prisma.StringNullableFilter<"Notification"> | string | null
+ createdAt?: Prisma.DateTimeFilter<"Notification"> | Date | string
+ readAt?: Prisma.DateTimeNullableFilter<"Notification"> | Date | string | null
+ user?: Prisma.XOR
+}
+
+export type NotificationOrderByWithRelationInput = {
+ id?: Prisma.SortOrder
+ userId?: Prisma.SortOrder
+ title?: Prisma.SortOrder
+ message?: Prisma.SortOrder
+ type?: Prisma.SortOrder
+ link?: Prisma.SortOrderInput | Prisma.SortOrder
+ metadata?: Prisma.SortOrderInput | Prisma.SortOrder
+ createdAt?: Prisma.SortOrder
+ readAt?: Prisma.SortOrderInput | Prisma.SortOrder
+ user?: Prisma.UserOrderByWithRelationInput
+}
+
+export type NotificationWhereUniqueInput = Prisma.AtLeast<{
+ id?: string
+ AND?: Prisma.NotificationWhereInput | Prisma.NotificationWhereInput[]
+ OR?: Prisma.NotificationWhereInput[]
+ NOT?: Prisma.NotificationWhereInput | Prisma.NotificationWhereInput[]
+ userId?: Prisma.StringFilter<"Notification"> | string
+ title?: Prisma.StringFilter<"Notification"> | string
+ message?: Prisma.StringFilter<"Notification"> | string
+ type?: Prisma.StringFilter<"Notification"> | string
+ link?: Prisma.StringNullableFilter<"Notification"> | string | null
+ metadata?: Prisma.StringNullableFilter<"Notification"> | string | null
+ createdAt?: Prisma.DateTimeFilter<"Notification"> | Date | string
+ readAt?: Prisma.DateTimeNullableFilter<"Notification"> | Date | string | null
+ user?: Prisma.XOR
+}, "id">
+
+export type NotificationOrderByWithAggregationInput = {
+ id?: Prisma.SortOrder
+ userId?: Prisma.SortOrder
+ title?: Prisma.SortOrder
+ message?: Prisma.SortOrder
+ type?: Prisma.SortOrder
+ link?: Prisma.SortOrderInput | Prisma.SortOrder
+ metadata?: Prisma.SortOrderInput | Prisma.SortOrder
+ createdAt?: Prisma.SortOrder
+ readAt?: Prisma.SortOrderInput | Prisma.SortOrder
+ _count?: Prisma.NotificationCountOrderByAggregateInput
+ _max?: Prisma.NotificationMaxOrderByAggregateInput
+ _min?: Prisma.NotificationMinOrderByAggregateInput
+}
+
+export type NotificationScalarWhereWithAggregatesInput = {
+ AND?: Prisma.NotificationScalarWhereWithAggregatesInput | Prisma.NotificationScalarWhereWithAggregatesInput[]
+ OR?: Prisma.NotificationScalarWhereWithAggregatesInput[]
+ NOT?: Prisma.NotificationScalarWhereWithAggregatesInput | Prisma.NotificationScalarWhereWithAggregatesInput[]
+ id?: Prisma.StringWithAggregatesFilter<"Notification"> | string
+ userId?: Prisma.StringWithAggregatesFilter<"Notification"> | string
+ title?: Prisma.StringWithAggregatesFilter<"Notification"> | string
+ message?: Prisma.StringWithAggregatesFilter<"Notification"> | string
+ type?: Prisma.StringWithAggregatesFilter<"Notification"> | string
+ link?: Prisma.StringNullableWithAggregatesFilter<"Notification"> | string | null
+ metadata?: Prisma.StringNullableWithAggregatesFilter<"Notification"> | string | null
+ createdAt?: Prisma.DateTimeWithAggregatesFilter<"Notification"> | Date | string
+ readAt?: Prisma.DateTimeNullableWithAggregatesFilter<"Notification"> | Date | string | null
+}
+
+export type NotificationCreateInput = {
+ id?: string
+ title: string
+ message: string
+ type?: string
+ link?: string | null
+ metadata?: string | null
+ createdAt?: Date | string
+ readAt?: Date | string | null
+ user: Prisma.UserCreateNestedOneWithoutNotificationInput
+}
+
+export type NotificationUncheckedCreateInput = {
+ id?: string
+ userId: string
+ title: string
+ message: string
+ type?: string
+ link?: string | null
+ metadata?: string | null
+ createdAt?: Date | string
+ readAt?: Date | string | null
+}
+
+export type NotificationUpdateInput = {
+ id?: Prisma.StringFieldUpdateOperationsInput | string
+ title?: Prisma.StringFieldUpdateOperationsInput | string
+ message?: Prisma.StringFieldUpdateOperationsInput | string
+ type?: Prisma.StringFieldUpdateOperationsInput | string
+ link?: Prisma.NullableStringFieldUpdateOperationsInput | string | null
+ metadata?: Prisma.NullableStringFieldUpdateOperationsInput | string | null
+ createdAt?: Prisma.DateTimeFieldUpdateOperationsInput | Date | string
+ readAt?: Prisma.NullableDateTimeFieldUpdateOperationsInput | Date | string | null
+ user?: Prisma.UserUpdateOneRequiredWithoutNotificationNestedInput
+}
+
+export type NotificationUncheckedUpdateInput = {
+ id?: Prisma.StringFieldUpdateOperationsInput | string
+ userId?: Prisma.StringFieldUpdateOperationsInput | string
+ title?: Prisma.StringFieldUpdateOperationsInput | string
+ message?: Prisma.StringFieldUpdateOperationsInput | string
+ type?: Prisma.StringFieldUpdateOperationsInput | string
+ link?: Prisma.NullableStringFieldUpdateOperationsInput | string | null
+ metadata?: Prisma.NullableStringFieldUpdateOperationsInput | string | null
+ createdAt?: Prisma.DateTimeFieldUpdateOperationsInput | Date | string
+ readAt?: Prisma.NullableDateTimeFieldUpdateOperationsInput | Date | string | null
+}
+
+export type NotificationCreateManyInput = {
+ id?: string
+ userId: string
+ title: string
+ message: string
+ type?: string
+ link?: string | null
+ metadata?: string | null
+ createdAt?: Date | string
+ readAt?: Date | string | null
+}
+
+export type NotificationUpdateManyMutationInput = {
+ id?: Prisma.StringFieldUpdateOperationsInput | string
+ title?: Prisma.StringFieldUpdateOperationsInput | string
+ message?: Prisma.StringFieldUpdateOperationsInput | string
+ type?: Prisma.StringFieldUpdateOperationsInput | string
+ link?: Prisma.NullableStringFieldUpdateOperationsInput | string | null
+ metadata?: Prisma.NullableStringFieldUpdateOperationsInput | string | null
+ createdAt?: Prisma.DateTimeFieldUpdateOperationsInput | Date | string
+ readAt?: Prisma.NullableDateTimeFieldUpdateOperationsInput | Date | string | null
+}
+
+export type NotificationUncheckedUpdateManyInput = {
+ id?: Prisma.StringFieldUpdateOperationsInput | string
+ userId?: Prisma.StringFieldUpdateOperationsInput | string
+ title?: Prisma.StringFieldUpdateOperationsInput | string
+ message?: Prisma.StringFieldUpdateOperationsInput | string
+ type?: Prisma.StringFieldUpdateOperationsInput | string
+ link?: Prisma.NullableStringFieldUpdateOperationsInput | string | null
+ metadata?: Prisma.NullableStringFieldUpdateOperationsInput | string | null
+ createdAt?: Prisma.DateTimeFieldUpdateOperationsInput | Date | string
+ readAt?: Prisma.NullableDateTimeFieldUpdateOperationsInput | Date | string | null
+}
+
+export type NotificationListRelationFilter = {
+ every?: Prisma.NotificationWhereInput
+ some?: Prisma.NotificationWhereInput
+ none?: Prisma.NotificationWhereInput
+}
+
+export type NotificationOrderByRelationAggregateInput = {
+ _count?: Prisma.SortOrder
+}
+
+export type NotificationCountOrderByAggregateInput = {
+ id?: Prisma.SortOrder
+ userId?: Prisma.SortOrder
+ title?: Prisma.SortOrder
+ message?: Prisma.SortOrder
+ type?: Prisma.SortOrder
+ link?: Prisma.SortOrder
+ metadata?: Prisma.SortOrder
+ createdAt?: Prisma.SortOrder
+ readAt?: Prisma.SortOrder
+}
+
+export type NotificationMaxOrderByAggregateInput = {
+ id?: Prisma.SortOrder
+ userId?: Prisma.SortOrder
+ title?: Prisma.SortOrder
+ message?: Prisma.SortOrder
+ type?: Prisma.SortOrder
+ link?: Prisma.SortOrder
+ metadata?: Prisma.SortOrder
+ createdAt?: Prisma.SortOrder
+ readAt?: Prisma.SortOrder
+}
+
+export type NotificationMinOrderByAggregateInput = {
+ id?: Prisma.SortOrder
+ userId?: Prisma.SortOrder
+ title?: Prisma.SortOrder
+ message?: Prisma.SortOrder
+ type?: Prisma.SortOrder
+ link?: Prisma.SortOrder
+ metadata?: Prisma.SortOrder
+ createdAt?: Prisma.SortOrder
+ readAt?: Prisma.SortOrder
+}
+
+export type NotificationCreateNestedManyWithoutUserInput = {
+ create?: Prisma.XOR | Prisma.NotificationCreateWithoutUserInput[] | Prisma.NotificationUncheckedCreateWithoutUserInput[]
+ connectOrCreate?: Prisma.NotificationCreateOrConnectWithoutUserInput | Prisma.NotificationCreateOrConnectWithoutUserInput[]
+ createMany?: Prisma.NotificationCreateManyUserInputEnvelope
+ connect?: Prisma.NotificationWhereUniqueInput | Prisma.NotificationWhereUniqueInput[]
+}
+
+export type NotificationUncheckedCreateNestedManyWithoutUserInput = {
+ create?: Prisma.XOR | Prisma.NotificationCreateWithoutUserInput[] | Prisma.NotificationUncheckedCreateWithoutUserInput[]
+ connectOrCreate?: Prisma.NotificationCreateOrConnectWithoutUserInput | Prisma.NotificationCreateOrConnectWithoutUserInput[]
+ createMany?: Prisma.NotificationCreateManyUserInputEnvelope
+ connect?: Prisma.NotificationWhereUniqueInput | Prisma.NotificationWhereUniqueInput[]
+}
+
+export type NotificationUpdateManyWithoutUserNestedInput = {
+ create?: Prisma.XOR | Prisma.NotificationCreateWithoutUserInput[] | Prisma.NotificationUncheckedCreateWithoutUserInput[]
+ connectOrCreate?: Prisma.NotificationCreateOrConnectWithoutUserInput | Prisma.NotificationCreateOrConnectWithoutUserInput[]
+ upsert?: Prisma.NotificationUpsertWithWhereUniqueWithoutUserInput | Prisma.NotificationUpsertWithWhereUniqueWithoutUserInput[]
+ createMany?: Prisma.NotificationCreateManyUserInputEnvelope
+ set?: Prisma.NotificationWhereUniqueInput | Prisma.NotificationWhereUniqueInput[]
+ disconnect?: Prisma.NotificationWhereUniqueInput | Prisma.NotificationWhereUniqueInput[]
+ delete?: Prisma.NotificationWhereUniqueInput | Prisma.NotificationWhereUniqueInput[]
+ connect?: Prisma.NotificationWhereUniqueInput | Prisma.NotificationWhereUniqueInput[]
+ update?: Prisma.NotificationUpdateWithWhereUniqueWithoutUserInput | Prisma.NotificationUpdateWithWhereUniqueWithoutUserInput[]
+ updateMany?: Prisma.NotificationUpdateManyWithWhereWithoutUserInput | Prisma.NotificationUpdateManyWithWhereWithoutUserInput[]
+ deleteMany?: Prisma.NotificationScalarWhereInput | Prisma.NotificationScalarWhereInput[]
+}
+
+export type NotificationUncheckedUpdateManyWithoutUserNestedInput = {
+ create?: Prisma.XOR | Prisma.NotificationCreateWithoutUserInput[] | Prisma.NotificationUncheckedCreateWithoutUserInput[]
+ connectOrCreate?: Prisma.NotificationCreateOrConnectWithoutUserInput | Prisma.NotificationCreateOrConnectWithoutUserInput[]
+ upsert?: Prisma.NotificationUpsertWithWhereUniqueWithoutUserInput | Prisma.NotificationUpsertWithWhereUniqueWithoutUserInput[]
+ createMany?: Prisma.NotificationCreateManyUserInputEnvelope
+ set?: Prisma.NotificationWhereUniqueInput | Prisma.NotificationWhereUniqueInput[]
+ disconnect?: Prisma.NotificationWhereUniqueInput | Prisma.NotificationWhereUniqueInput[]
+ delete?: Prisma.NotificationWhereUniqueInput | Prisma.NotificationWhereUniqueInput[]
+ connect?: Prisma.NotificationWhereUniqueInput | Prisma.NotificationWhereUniqueInput[]
+ update?: Prisma.NotificationUpdateWithWhereUniqueWithoutUserInput | Prisma.NotificationUpdateWithWhereUniqueWithoutUserInput[]
+ updateMany?: Prisma.NotificationUpdateManyWithWhereWithoutUserInput | Prisma.NotificationUpdateManyWithWhereWithoutUserInput[]
+ deleteMany?: Prisma.NotificationScalarWhereInput | Prisma.NotificationScalarWhereInput[]
+}
+
+export type NotificationCreateWithoutUserInput = {
+ id?: string
+ title: string
+ message: string
+ type?: string
+ link?: string | null
+ metadata?: string | null
+ createdAt?: Date | string
+ readAt?: Date | string | null
+}
+
+export type NotificationUncheckedCreateWithoutUserInput = {
+ id?: string
+ title: string
+ message: string
+ type?: string
+ link?: string | null
+ metadata?: string | null
+ createdAt?: Date | string
+ readAt?: Date | string | null
+}
+
+export type NotificationCreateOrConnectWithoutUserInput = {
+ where: Prisma.NotificationWhereUniqueInput
+ create: Prisma.XOR
+}
+
+export type NotificationCreateManyUserInputEnvelope = {
+ data: Prisma.NotificationCreateManyUserInput | Prisma.NotificationCreateManyUserInput[]
+ skipDuplicates?: boolean
+}
+
+export type NotificationUpsertWithWhereUniqueWithoutUserInput = {
+ where: Prisma.NotificationWhereUniqueInput
+ update: Prisma.XOR
+ create: Prisma.XOR
+}
+
+export type NotificationUpdateWithWhereUniqueWithoutUserInput = {
+ where: Prisma.NotificationWhereUniqueInput
+ data: Prisma.XOR
+}
+
+export type NotificationUpdateManyWithWhereWithoutUserInput = {
+ where: Prisma.NotificationScalarWhereInput
+ data: Prisma.XOR
+}
+
+export type NotificationScalarWhereInput = {
+ AND?: Prisma.NotificationScalarWhereInput | Prisma.NotificationScalarWhereInput[]
+ OR?: Prisma.NotificationScalarWhereInput[]
+ NOT?: Prisma.NotificationScalarWhereInput | Prisma.NotificationScalarWhereInput[]
+ id?: Prisma.StringFilter<"Notification"> | string
+ userId?: Prisma.StringFilter<"Notification"> | string
+ title?: Prisma.StringFilter<"Notification"> | string
+ message?: Prisma.StringFilter<"Notification"> | string
+ type?: Prisma.StringFilter<"Notification"> | string
+ link?: Prisma.StringNullableFilter<"Notification"> | string | null
+ metadata?: Prisma.StringNullableFilter<"Notification"> | string | null
+ createdAt?: Prisma.DateTimeFilter<"Notification"> | Date | string
+ readAt?: Prisma.DateTimeNullableFilter<"Notification"> | Date | string | null
+}
+
+export type NotificationCreateManyUserInput = {
+ id?: string
+ title: string
+ message: string
+ type?: string
+ link?: string | null
+ metadata?: string | null
+ createdAt?: Date | string
+ readAt?: Date | string | null
+}
+
+export type NotificationUpdateWithoutUserInput = {
+ id?: Prisma.StringFieldUpdateOperationsInput | string
+ title?: Prisma.StringFieldUpdateOperationsInput | string
+ message?: Prisma.StringFieldUpdateOperationsInput | string
+ type?: Prisma.StringFieldUpdateOperationsInput | string
+ link?: Prisma.NullableStringFieldUpdateOperationsInput | string | null
+ metadata?: Prisma.NullableStringFieldUpdateOperationsInput | string | null
+ createdAt?: Prisma.DateTimeFieldUpdateOperationsInput | Date | string
+ readAt?: Prisma.NullableDateTimeFieldUpdateOperationsInput | Date | string | null
+}
+
+export type NotificationUncheckedUpdateWithoutUserInput = {
+ id?: Prisma.StringFieldUpdateOperationsInput | string
+ title?: Prisma.StringFieldUpdateOperationsInput | string
+ message?: Prisma.StringFieldUpdateOperationsInput | string
+ type?: Prisma.StringFieldUpdateOperationsInput | string
+ link?: Prisma.NullableStringFieldUpdateOperationsInput | string | null
+ metadata?: Prisma.NullableStringFieldUpdateOperationsInput | string | null
+ createdAt?: Prisma.DateTimeFieldUpdateOperationsInput | Date | string
+ readAt?: Prisma.NullableDateTimeFieldUpdateOperationsInput | Date | string | null
+}
+
+export type NotificationUncheckedUpdateManyWithoutUserInput = {
+ id?: Prisma.StringFieldUpdateOperationsInput | string
+ title?: Prisma.StringFieldUpdateOperationsInput | string
+ message?: Prisma.StringFieldUpdateOperationsInput | string
+ type?: Prisma.StringFieldUpdateOperationsInput | string
+ link?: Prisma.NullableStringFieldUpdateOperationsInput | string | null
+ metadata?: Prisma.NullableStringFieldUpdateOperationsInput | string | null
+ createdAt?: Prisma.DateTimeFieldUpdateOperationsInput | Date | string
+ readAt?: Prisma.NullableDateTimeFieldUpdateOperationsInput | Date | string | null
+}
+
+
+
+export type NotificationSelect = runtime.Types.Extensions.GetSelect<{
+ id?: boolean
+ userId?: boolean
+ title?: boolean
+ message?: boolean
+ type?: boolean
+ link?: boolean
+ metadata?: boolean
+ createdAt?: boolean
+ readAt?: boolean
+ user?: boolean | Prisma.UserDefaultArgs
+}, ExtArgs["result"]["notification"]>
+
+export type NotificationSelectCreateManyAndReturn = runtime.Types.Extensions.GetSelect<{
+ id?: boolean
+ userId?: boolean
+ title?: boolean
+ message?: boolean
+ type?: boolean
+ link?: boolean
+ metadata?: boolean
+ createdAt?: boolean
+ readAt?: boolean
+ user?: boolean | Prisma.UserDefaultArgs
+}, ExtArgs["result"]["notification"]>
+
+export type NotificationSelectUpdateManyAndReturn = runtime.Types.Extensions.GetSelect<{
+ id?: boolean
+ userId?: boolean
+ title?: boolean
+ message?: boolean
+ type?: boolean
+ link?: boolean
+ metadata?: boolean
+ createdAt?: boolean
+ readAt?: boolean
+ user?: boolean | Prisma.UserDefaultArgs
+}, ExtArgs["result"]["notification"]>
+
+export type NotificationSelectScalar = {
+ id?: boolean
+ userId?: boolean
+ title?: boolean
+ message?: boolean
+ type?: boolean
+ link?: boolean
+ metadata?: boolean
+ createdAt?: boolean
+ readAt?: boolean
+}
+
+export type NotificationOmit = runtime.Types.Extensions.GetOmit<"id" | "userId" | "title" | "message" | "type" | "link" | "metadata" | "createdAt" | "readAt", ExtArgs["result"]["notification"]>
+export type NotificationInclude = {
+ user?: boolean | Prisma.UserDefaultArgs
+}
+export type NotificationIncludeCreateManyAndReturn = {
+ user?: boolean | Prisma.UserDefaultArgs
+}
+export type NotificationIncludeUpdateManyAndReturn = {
+ user?: boolean | Prisma.UserDefaultArgs
+}
+
+export type $NotificationPayload = {
+ name: "Notification"
+ objects: {
+ user: Prisma.$UserPayload
+ }
+ scalars: runtime.Types.Extensions.GetPayloadResult<{
+ id: string
+ userId: string
+ title: string
+ message: string
+ type: string
+ link: string | null
+ metadata: string | null
+ createdAt: Date
+ readAt: Date | null
+ }, ExtArgs["result"]["notification"]>
+ composites: {}
+}
+
+export type NotificationGetPayload = runtime.Types.Result.GetResult
+
+export type NotificationCountArgs =
+ Omit & {
+ select?: NotificationCountAggregateInputType | true
+ }
+
+export interface NotificationDelegate {
+ [K: symbol]: { types: Prisma.TypeMap['model']['Notification'], meta: { name: 'Notification' } }
+ /**
+ * Find zero or one Notification that matches the filter.
+ * @param {NotificationFindUniqueArgs} args - Arguments to find a Notification
+ * @example
+ * // Get one Notification
+ * const notification = await prisma.notification.findUnique({
+ * where: {
+ * // ... provide filter here
+ * }
+ * })
+ */
+ findUnique(args: Prisma.SelectSubset>): Prisma.Prisma__NotificationClient, T, "findUnique", GlobalOmitOptions> | null, null, ExtArgs, GlobalOmitOptions>
+
+ /**
+ * Find one Notification that matches the filter or throw an error with `error.code='P2025'`
+ * if no matches were found.
+ * @param {NotificationFindUniqueOrThrowArgs} args - Arguments to find a Notification
+ * @example
+ * // Get one Notification
+ * const notification = await prisma.notification.findUniqueOrThrow({
+ * where: {
+ * // ... provide filter here
+ * }
+ * })
+ */
+ findUniqueOrThrow(args: Prisma.SelectSubset>): Prisma.Prisma__NotificationClient, T, "findUniqueOrThrow", GlobalOmitOptions>, never, ExtArgs, GlobalOmitOptions>
+
+ /**
+ * Find the first Notification that matches the filter.
+ * Note, that providing `undefined` is treated as the value not being there.
+ * Read more here: https://pris.ly/d/null-undefined
+ * @param {NotificationFindFirstArgs} args - Arguments to find a Notification
+ * @example
+ * // Get one Notification
+ * const notification = await prisma.notification.findFirst({
+ * where: {
+ * // ... provide filter here
+ * }
+ * })
+ */
+ findFirst(args?: Prisma.SelectSubset>): Prisma.Prisma__NotificationClient, T, "findFirst", GlobalOmitOptions> | null, null, ExtArgs, GlobalOmitOptions>
+
+ /**
+ * Find the first Notification that matches the filter or
+ * throw `PrismaKnownClientError` with `P2025` code if no matches were found.
+ * Note, that providing `undefined` is treated as the value not being there.
+ * Read more here: https://pris.ly/d/null-undefined
+ * @param {NotificationFindFirstOrThrowArgs} args - Arguments to find a Notification
+ * @example
+ * // Get one Notification
+ * const notification = await prisma.notification.findFirstOrThrow({
+ * where: {
+ * // ... provide filter here
+ * }
+ * })
+ */
+ findFirstOrThrow(args?: Prisma.SelectSubset>): Prisma.Prisma__NotificationClient, T, "findFirstOrThrow", GlobalOmitOptions>, never, ExtArgs, GlobalOmitOptions>
+
+ /**
+ * Find zero or more Notifications that matches the filter.
+ * Note, that providing `undefined` is treated as the value not being there.
+ * Read more here: https://pris.ly/d/null-undefined
+ * @param {NotificationFindManyArgs} args - Arguments to filter and select certain fields only.
+ * @example
+ * // Get all Notifications
+ * const notifications = await prisma.notification.findMany()
+ *
+ * // Get first 10 Notifications
+ * const notifications = await prisma.notification.findMany({ take: 10 })
+ *
+ * // Only select the `id`
+ * const notificationWithIdOnly = await prisma.notification.findMany({ select: { id: true } })
+ *
+ */
+ findMany(args?: Prisma.SelectSubset>): Prisma.PrismaPromise, T, "findMany", GlobalOmitOptions>>
+
+ /**
+ * Create a Notification.
+ * @param {NotificationCreateArgs} args - Arguments to create a Notification.
+ * @example
+ * // Create one Notification
+ * const Notification = await prisma.notification.create({
+ * data: {
+ * // ... data to create a Notification
+ * }
+ * })
+ *
+ */
+ create(args: Prisma.SelectSubset>): Prisma.Prisma__NotificationClient, T, "create", GlobalOmitOptions>, never, ExtArgs, GlobalOmitOptions>
+
+ /**
+ * Create many Notifications.
+ * @param {NotificationCreateManyArgs} args - Arguments to create many Notifications.
+ * @example
+ * // Create many Notifications
+ * const notification = await prisma.notification.createMany({
+ * data: [
+ * // ... provide data here
+ * ]
+ * })
+ *
+ */
+ createMany(args?: Prisma.SelectSubset>): Prisma.PrismaPromise
+
+ /**
+ * Create many Notifications and returns the data saved in the database.
+ * @param {NotificationCreateManyAndReturnArgs} args - Arguments to create many Notifications.
+ * @example
+ * // Create many Notifications
+ * const notification = await prisma.notification.createManyAndReturn({
+ * data: [
+ * // ... provide data here
+ * ]
+ * })
+ *
+ * // Create many Notifications and only return the `id`
+ * const notificationWithIdOnly = await prisma.notification.createManyAndReturn({
+ * select: { id: true },
+ * data: [
+ * // ... provide data here
+ * ]
+ * })
+ * Note, that providing `undefined` is treated as the value not being there.
+ * Read more here: https://pris.ly/d/null-undefined
+ *
+ */
+ createManyAndReturn(args?: Prisma.SelectSubset>): Prisma.PrismaPromise, T, "createManyAndReturn", GlobalOmitOptions>>
+
+ /**
+ * Delete a Notification.
+ * @param {NotificationDeleteArgs} args - Arguments to delete one Notification.
+ * @example
+ * // Delete one Notification
+ * const Notification = await prisma.notification.delete({
+ * where: {
+ * // ... filter to delete one Notification
+ * }
+ * })
+ *
+ */
+ delete(args: Prisma.SelectSubset>): Prisma.Prisma__NotificationClient, T, "delete", GlobalOmitOptions>, never, ExtArgs, GlobalOmitOptions>
+
+ /**
+ * Update one Notification.
+ * @param {NotificationUpdateArgs} args - Arguments to update one Notification.
+ * @example
+ * // Update one Notification
+ * const notification = await prisma.notification.update({
+ * where: {
+ * // ... provide filter here
+ * },
+ * data: {
+ * // ... provide data here
+ * }
+ * })
+ *
+ */
+ update(args: Prisma.SelectSubset>): Prisma.Prisma__NotificationClient, T, "update", GlobalOmitOptions>, never, ExtArgs, GlobalOmitOptions>
+
+ /**
+ * Delete zero or more Notifications.
+ * @param {NotificationDeleteManyArgs} args - Arguments to filter Notifications to delete.
+ * @example
+ * // Delete a few Notifications
+ * const { count } = await prisma.notification.deleteMany({
+ * where: {
+ * // ... provide filter here
+ * }
+ * })
+ *
+ */
+ deleteMany(args?: Prisma.SelectSubset>): Prisma.PrismaPromise
+
+ /**
+ * Update zero or more Notifications.
+ * Note, that providing `undefined` is treated as the value not being there.
+ * Read more here: https://pris.ly/d/null-undefined
+ * @param {NotificationUpdateManyArgs} args - Arguments to update one or more rows.
+ * @example
+ * // Update many Notifications
+ * const notification = await prisma.notification.updateMany({
+ * where: {
+ * // ... provide filter here
+ * },
+ * data: {
+ * // ... provide data here
+ * }
+ * })
+ *
+ */
+ updateMany(args: Prisma.SelectSubset>): Prisma.PrismaPromise
+
+ /**
+ * Update zero or more Notifications and returns the data updated in the database.
+ * @param {NotificationUpdateManyAndReturnArgs} args - Arguments to update many Notifications.
+ * @example
+ * // Update many Notifications
+ * const notification = await prisma.notification.updateManyAndReturn({
+ * where: {
+ * // ... provide filter here
+ * },
+ * data: [
+ * // ... provide data here
+ * ]
+ * })
+ *
+ * // Update zero or more Notifications and only return the `id`
+ * const notificationWithIdOnly = await prisma.notification.updateManyAndReturn({
+ * select: { id: true },
+ * where: {
+ * // ... provide filter here
+ * },
+ * data: [
+ * // ... provide data here
+ * ]
+ * })
+ * Note, that providing `undefined` is treated as the value not being there.
+ * Read more here: https://pris.ly/d/null-undefined
+ *
+ */
+ updateManyAndReturn(args: Prisma.SelectSubset>): Prisma.PrismaPromise, T, "updateManyAndReturn", GlobalOmitOptions>>
+
+ /**
+ * Create or update one Notification.
+ * @param {NotificationUpsertArgs} args - Arguments to update or create a Notification.
+ * @example
+ * // Update or create a Notification
+ * const notification = await prisma.notification.upsert({
+ * create: {
+ * // ... data to create a Notification
+ * },
+ * update: {
+ * // ... in case it already exists, update
+ * },
+ * where: {
+ * // ... the filter for the Notification we want to update
+ * }
+ * })
+ */
+ upsert(args: Prisma.SelectSubset>): Prisma.Prisma__NotificationClient, T, "upsert", GlobalOmitOptions>, never, ExtArgs, GlobalOmitOptions>
+
+
+ /**
+ * Count the number of Notifications.
+ * Note, that providing `undefined` is treated as the value not being there.
+ * Read more here: https://pris.ly/d/null-undefined
+ * @param {NotificationCountArgs} args - Arguments to filter Notifications to count.
+ * @example
+ * // Count the number of Notifications
+ * const count = await prisma.notification.count({
+ * where: {
+ * // ... the filter for the Notifications we want to count
+ * }
+ * })
+ **/
+ count(
+ args?: Prisma.Subset,
+ ): Prisma.PrismaPromise<
+ T extends runtime.Types.Utils.Record<'select', any>
+ ? T['select'] extends true
+ ? number
+ : Prisma.GetScalarType
+ : number
+ >
+
+ /**
+ * Allows you to perform aggregations operations on a Notification.
+ * Note, that providing `undefined` is treated as the value not being there.
+ * Read more here: https://pris.ly/d/null-undefined
+ * @param {NotificationAggregateArgs} args - Select which aggregations you would like to apply and on what fields.
+ * @example
+ * // Ordered by age ascending
+ * // Where email contains prisma.io
+ * // Limited to the 10 users
+ * const aggregations = await prisma.user.aggregate({
+ * _avg: {
+ * age: true,
+ * },
+ * where: {
+ * email: {
+ * contains: "prisma.io",
+ * },
+ * },
+ * orderBy: {
+ * age: "asc",
+ * },
+ * take: 10,
+ * })
+ **/
+ aggregate(args: Prisma.Subset): Prisma.PrismaPromise>
+
+ /**
+ * Group by Notification.
+ * Note, that providing `undefined` is treated as the value not being there.
+ * Read more here: https://pris.ly/d/null-undefined
+ * @param {NotificationGroupByArgs} args - Group by arguments.
+ * @example
+ * // Group by city, order by createdAt, get count
+ * const result = await prisma.user.groupBy({
+ * by: ['city', 'createdAt'],
+ * orderBy: {
+ * createdAt: true
+ * },
+ * _count: {
+ * _all: true
+ * },
+ * })
+ *
+ **/
+ groupBy<
+ T extends NotificationGroupByArgs,
+ HasSelectOrTake extends Prisma.Or<
+ Prisma.Extends<'skip', Prisma.Keys>,
+ Prisma.Extends<'take', Prisma.Keys>
+ >,
+ OrderByArg extends Prisma.True extends HasSelectOrTake
+ ? { orderBy: NotificationGroupByArgs['orderBy'] }
+ : { orderBy?: NotificationGroupByArgs['orderBy'] },
+ OrderFields extends Prisma.ExcludeUnderscoreKeys>>,
+ ByFields extends Prisma.MaybeTupleToUnion,
+ ByValid extends Prisma.Has,
+ HavingFields extends Prisma.GetHavingFields,
+ HavingValid extends Prisma.Has,
+ ByEmpty extends T['by'] extends never[] ? Prisma.True : Prisma.False,
+ InputErrors extends ByEmpty extends Prisma.True
+ ? `Error: "by" must not be empty.`
+ : HavingValid extends Prisma.False
+ ? {
+ [P in HavingFields]: P extends ByFields
+ ? never
+ : P extends string
+ ? `Error: Field "${P}" used in "having" needs to be provided in "by".`
+ : [
+ Error,
+ 'Field ',
+ P,
+ ` in "having" needs to be provided in "by"`,
+ ]
+ }[HavingFields]
+ : 'take' extends Prisma.Keys
+ ? 'orderBy' extends Prisma.Keys
+ ? ByValid extends Prisma.True
+ ? {}
+ : {
+ [P in OrderFields]: P extends ByFields
+ ? never
+ : `Error: Field "${P}" in "orderBy" needs to be provided in "by"`
+ }[OrderFields]
+ : 'Error: If you provide "take", you also need to provide "orderBy"'
+ : 'skip' extends Prisma.Keys
+ ? 'orderBy' extends Prisma.Keys
+ ? ByValid extends Prisma.True
+ ? {}
+ : {
+ [P in OrderFields]: P extends ByFields
+ ? never
+ : `Error: Field "${P}" in "orderBy" needs to be provided in "by"`
+ }[OrderFields]
+ : 'Error: If you provide "skip", you also need to provide "orderBy"'
+ : ByValid extends Prisma.True
+ ? {}
+ : {
+ [P in OrderFields]: P extends ByFields
+ ? never
+ : `Error: Field "${P}" in "orderBy" needs to be provided in "by"`
+ }[OrderFields]
+ >(args: Prisma.SubsetIntersection & InputErrors): {} extends InputErrors ? GetNotificationGroupByPayload : Prisma.PrismaPromise
+/**
+ * Fields of the Notification model
+ */
+readonly fields: NotificationFieldRefs;
+}
+
+/**
+ * The delegate class that acts as a "Promise-like" for Notification.
+ * Why is this prefixed with `Prisma__`?
+ * Because we want to prevent naming conflicts as mentioned in
+ * https://github.com/prisma/prisma-client-js/issues/707
+ */
+export interface Prisma__NotificationClient extends Prisma.PrismaPromise {
+ readonly [Symbol.toStringTag]: "PrismaPromise"
+ user = {}>(args?: Prisma.Subset>): Prisma.Prisma__UserClient, T, "findUniqueOrThrow", GlobalOmitOptions> | Null, Null, ExtArgs, GlobalOmitOptions>
+ /**
+ * Attaches callbacks for the resolution and/or rejection of the Promise.
+ * @param onfulfilled The callback to execute when the Promise is resolved.
+ * @param onrejected The callback to execute when the Promise is rejected.
+ * @returns A Promise for the completion of which ever callback is executed.
+ */
+ then(onfulfilled?: ((value: T) => TResult1 | PromiseLike) | undefined | null, onrejected?: ((reason: any) => TResult2 | PromiseLike) | undefined | null): runtime.Types.Utils.JsPromise
+ /**
+ * Attaches a callback for only the rejection of the Promise.
+ * @param onrejected The callback to execute when the Promise is rejected.
+ * @returns A Promise for the completion of the callback.
+ */
+ catch(onrejected?: ((reason: any) => TResult | PromiseLike) | undefined | null): runtime.Types.Utils.JsPromise
+ /**
+ * Attaches a callback that is invoked when the Promise is settled (fulfilled or rejected). The
+ * resolved value cannot be modified from the callback.
+ * @param onfinally The callback to execute when the Promise is settled (fulfilled or rejected).
+ * @returns A Promise for the completion of the callback.
+ */
+ finally(onfinally?: (() => void) | undefined | null): runtime.Types.Utils.JsPromise
+}
+
+
+
+
+/**
+ * Fields of the Notification model
+ */
+export interface NotificationFieldRefs {
+ readonly id: Prisma.FieldRef<"Notification", 'String'>
+ readonly userId: Prisma.FieldRef<"Notification", 'String'>
+ readonly title: Prisma.FieldRef<"Notification", 'String'>
+ readonly message: Prisma.FieldRef<"Notification", 'String'>
+ readonly type: Prisma.FieldRef<"Notification", 'String'>
+ readonly link: Prisma.FieldRef<"Notification", 'String'>
+ readonly metadata: Prisma.FieldRef<"Notification", 'String'>
+ readonly createdAt: Prisma.FieldRef<"Notification", 'DateTime'>
+ readonly readAt: Prisma.FieldRef<"Notification", 'DateTime'>
+}
+
+
+// Custom InputTypes
+/**
+ * Notification findUnique
+ */
+export type NotificationFindUniqueArgs = {
+ /**
+ * Select specific fields to fetch from the Notification
+ */
+ select?: Prisma.NotificationSelect | null
+ /**
+ * Omit specific fields from the Notification
+ */
+ omit?: Prisma.NotificationOmit | null
+ /**
+ * Choose, which related nodes to fetch as well
+ */
+ include?: Prisma.NotificationInclude | null
+ /**
+ * Filter, which Notification to fetch.
+ */
+ where: Prisma.NotificationWhereUniqueInput
+}
+
+/**
+ * Notification findUniqueOrThrow
+ */
+export type NotificationFindUniqueOrThrowArgs = {
+ /**
+ * Select specific fields to fetch from the Notification
+ */
+ select?: Prisma.NotificationSelect | null
+ /**
+ * Omit specific fields from the Notification
+ */
+ omit?: Prisma.NotificationOmit | null
+ /**
+ * Choose, which related nodes to fetch as well
+ */
+ include?: Prisma.NotificationInclude | null
+ /**
+ * Filter, which Notification to fetch.
+ */
+ where: Prisma.NotificationWhereUniqueInput
+}
+
+/**
+ * Notification findFirst
+ */
+export type NotificationFindFirstArgs = {
+ /**
+ * Select specific fields to fetch from the Notification
+ */
+ select?: Prisma.NotificationSelect | null
+ /**
+ * Omit specific fields from the Notification
+ */
+ omit?: Prisma.NotificationOmit | null
+ /**
+ * Choose, which related nodes to fetch as well
+ */
+ include?: Prisma.NotificationInclude | null
+ /**
+ * Filter, which Notification to fetch.
+ */
+ where?: Prisma.NotificationWhereInput
+ /**
+ * {@link https://www.prisma.io/docs/concepts/components/prisma-client/sorting Sorting Docs}
+ *
+ * Determine the order of Notifications to fetch.
+ */
+ orderBy?: Prisma.NotificationOrderByWithRelationInput | Prisma.NotificationOrderByWithRelationInput[]
+ /**
+ * {@link https://www.prisma.io/docs/concepts/components/prisma-client/pagination#cursor-based-pagination Cursor Docs}
+ *
+ * Sets the position for searching for Notifications.
+ */
+ cursor?: Prisma.NotificationWhereUniqueInput
+ /**
+ * {@link https://www.prisma.io/docs/concepts/components/prisma-client/pagination Pagination Docs}
+ *
+ * Take `±n` Notifications from the position of the cursor.
+ */
+ take?: number
+ /**
+ * {@link https://www.prisma.io/docs/concepts/components/prisma-client/pagination Pagination Docs}
+ *
+ * Skip the first `n` Notifications.
+ */
+ skip?: number
+ /**
+ * {@link https://www.prisma.io/docs/concepts/components/prisma-client/distinct Distinct Docs}
+ *
+ * Filter by unique combinations of Notifications.
+ */
+ distinct?: Prisma.NotificationScalarFieldEnum | Prisma.NotificationScalarFieldEnum[]
+}
+
+/**
+ * Notification findFirstOrThrow
+ */
+export type NotificationFindFirstOrThrowArgs = {
+ /**
+ * Select specific fields to fetch from the Notification
+ */
+ select?: Prisma.NotificationSelect | null
+ /**
+ * Omit specific fields from the Notification
+ */
+ omit?: Prisma.NotificationOmit | null
+ /**
+ * Choose, which related nodes to fetch as well
+ */
+ include?: Prisma.NotificationInclude | null
+ /**
+ * Filter, which Notification to fetch.
+ */
+ where?: Prisma.NotificationWhereInput
+ /**
+ * {@link https://www.prisma.io/docs/concepts/components/prisma-client/sorting Sorting Docs}
+ *
+ * Determine the order of Notifications to fetch.
+ */
+ orderBy?: Prisma.NotificationOrderByWithRelationInput | Prisma.NotificationOrderByWithRelationInput[]
+ /**
+ * {@link https://www.prisma.io/docs/concepts/components/prisma-client/pagination#cursor-based-pagination Cursor Docs}
+ *
+ * Sets the position for searching for Notifications.
+ */
+ cursor?: Prisma.NotificationWhereUniqueInput
+ /**
+ * {@link https://www.prisma.io/docs/concepts/components/prisma-client/pagination Pagination Docs}
+ *
+ * Take `±n` Notifications from the position of the cursor.
+ */
+ take?: number
+ /**
+ * {@link https://www.prisma.io/docs/concepts/components/prisma-client/pagination Pagination Docs}
+ *
+ * Skip the first `n` Notifications.
+ */
+ skip?: number
+ /**
+ * {@link https://www.prisma.io/docs/concepts/components/prisma-client/distinct Distinct Docs}
+ *
+ * Filter by unique combinations of Notifications.
+ */
+ distinct?: Prisma.NotificationScalarFieldEnum | Prisma.NotificationScalarFieldEnum[]
+}
+
+/**
+ * Notification findMany
+ */
+export type NotificationFindManyArgs = {
+ /**
+ * Select specific fields to fetch from the Notification
+ */
+ select?: Prisma.NotificationSelect | null
+ /**
+ * Omit specific fields from the Notification
+ */
+ omit?: Prisma.NotificationOmit | null
+ /**
+ * Choose, which related nodes to fetch as well
+ */
+ include?: Prisma.NotificationInclude | null
+ /**
+ * Filter, which Notifications to fetch.
+ */
+ where?: Prisma.NotificationWhereInput
+ /**
+ * {@link https://www.prisma.io/docs/concepts/components/prisma-client/sorting Sorting Docs}
+ *
+ * Determine the order of Notifications to fetch.
+ */
+ orderBy?: Prisma.NotificationOrderByWithRelationInput | Prisma.NotificationOrderByWithRelationInput[]
+ /**
+ * {@link https://www.prisma.io/docs/concepts/components/prisma-client/pagination#cursor-based-pagination Cursor Docs}
+ *
+ * Sets the position for listing Notifications.
+ */
+ cursor?: Prisma.NotificationWhereUniqueInput
+ /**
+ * {@link https://www.prisma.io/docs/concepts/components/prisma-client/pagination Pagination Docs}
+ *
+ * Take `±n` Notifications from the position of the cursor.
+ */
+ take?: number
+ /**
+ * {@link https://www.prisma.io/docs/concepts/components/prisma-client/pagination Pagination Docs}
+ *
+ * Skip the first `n` Notifications.
+ */
+ skip?: number
+ distinct?: Prisma.NotificationScalarFieldEnum | Prisma.NotificationScalarFieldEnum[]
+}
+
+/**
+ * Notification create
+ */
+export type NotificationCreateArgs = {
+ /**
+ * Select specific fields to fetch from the Notification
+ */
+ select?: Prisma.NotificationSelect | null
+ /**
+ * Omit specific fields from the Notification
+ */
+ omit?: Prisma.NotificationOmit | null
+ /**
+ * Choose, which related nodes to fetch as well
+ */
+ include?: Prisma.NotificationInclude | null
+ /**
+ * The data needed to create a Notification.
+ */
+ data: Prisma.XOR
+}
+
+/**
+ * Notification createMany
+ */
+export type NotificationCreateManyArgs = {
+ /**
+ * The data used to create many Notifications.
+ */
+ data: Prisma.NotificationCreateManyInput | Prisma.NotificationCreateManyInput[]
+ skipDuplicates?: boolean
+}
+
+/**
+ * Notification createManyAndReturn
+ */
+export type NotificationCreateManyAndReturnArgs = {
+ /**
+ * Select specific fields to fetch from the Notification
+ */
+ select?: Prisma.NotificationSelectCreateManyAndReturn | null
+ /**
+ * Omit specific fields from the Notification
+ */
+ omit?: Prisma.NotificationOmit | null
+ /**
+ * The data used to create many Notifications.
+ */
+ data: Prisma.NotificationCreateManyInput | Prisma.NotificationCreateManyInput[]
+ skipDuplicates?: boolean
+ /**
+ * Choose, which related nodes to fetch as well
+ */
+ include?: Prisma.NotificationIncludeCreateManyAndReturn | null
+}
+
+/**
+ * Notification update
+ */
+export type NotificationUpdateArgs = {
+ /**
+ * Select specific fields to fetch from the Notification
+ */
+ select?: Prisma.NotificationSelect | null
+ /**
+ * Omit specific fields from the Notification
+ */
+ omit?: Prisma.NotificationOmit | null
+ /**
+ * Choose, which related nodes to fetch as well
+ */
+ include?: Prisma.NotificationInclude | null
+ /**
+ * The data needed to update a Notification.
+ */
+ data: Prisma.XOR
+ /**
+ * Choose, which Notification to update.
+ */
+ where: Prisma.NotificationWhereUniqueInput
+}
+
+/**
+ * Notification updateMany
+ */
+export type NotificationUpdateManyArgs = {
+ /**
+ * The data used to update Notifications.
+ */
+ data: Prisma.XOR