);
diff --git a/src/routes/(app)/(auth)/kanri/settings.tsx b/src/routes/(app)/(auth)/kanri/settings.tsx
index 9854706..3c90a5d 100644
--- a/src/routes/(app)/(auth)/kanri/settings.tsx
+++ b/src/routes/(app)/(auth)/kanri/settings.tsx
@@ -1,5 +1,5 @@
import SettingsForm from '@/components/form/settings-form';
-import { m } from '@/paraglide/messages';
+import { m } from '@paraglide/messages';
import { createFileRoute } from '@tanstack/react-router';
export const Route = createFileRoute('/(app)/(auth)/kanri/settings')({
diff --git a/src/routes/(app)/(auth)/kanri/users.tsx b/src/routes/(app)/(auth)/kanri/users.tsx
index e1c5111..e6af5c8 100644
--- a/src/routes/(app)/(auth)/kanri/users.tsx
+++ b/src/routes/(app)/(auth)/kanri/users.tsx
@@ -1,13 +1,13 @@
import DataTable from '@/components/DataTable';
-import { Card, CardHeader, CardTitle } from '@/components/ui/card';
+import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card';
import SearchInput from '@/components/ui/search-input';
import { Skeleton } from '@/components/ui/skeleton';
import AddNewUserButton from '@/components/user/add-new-user-dialog';
import { userColumns } from '@/components/user/user-column';
-import useDebounced from '@/hooks/use-debounced';
-import { m } from '@/paraglide/messages';
-import { usersQueries } from '@/service/queries';
+import useDebounced from '@hooks/use-debounced';
+import { m } from '@paraglide/messages';
import { UsersIcon } from '@phosphor-icons/react';
+import { usersQueries } from '@service/queries';
import { useQuery } from '@tanstack/react-query';
import { createFileRoute } from '@tanstack/react-router';
import { useState } from 'react';
@@ -39,44 +39,43 @@ function RouteComponent() {
if (isLoading) {
return (
-
+
-
+
{m.users_page_ui_title()}
+
+
+ {data && (
+
+ )}
+
-
- {data && (
-
- )}
);
diff --git a/src/routes/(app)/(auth)/dashboard.tsx b/src/routes/(app)/(auth)/management/dashboard.tsx
similarity index 82%
rename from src/routes/(app)/(auth)/dashboard.tsx
rename to src/routes/(app)/(auth)/management/dashboard.tsx
index 82c8bd2..fe279ee 100644
--- a/src/routes/(app)/(auth)/dashboard.tsx
+++ b/src/routes/(app)/(auth)/management/dashboard.tsx
@@ -1,7 +1,7 @@
-import { m } from '@/paraglide/messages';
+import { m } from '@paraglide/messages';
import { createFileRoute } from '@tanstack/react-router';
-export const Route = createFileRoute('/(app)/(auth)/dashboard')({
+export const Route = createFileRoute('/(app)/(auth)/management/dashboard')({
component: RouteComponent,
staticData: { breadcrumb: () => m.nav_dashboard() },
});
diff --git a/src/routes/(app)/(auth)/management/houses.tsx b/src/routes/(app)/(auth)/management/houses.tsx
new file mode 100644
index 0000000..16c2521
--- /dev/null
+++ b/src/routes/(app)/(auth)/management/houses.tsx
@@ -0,0 +1,35 @@
+import CurrentUserActionGroup from '@/components/house/current-user-action-group';
+import CurrentUserHouseList from '@/components/house/current-user-house-list';
+import CurrentUserInvitationList from '@/components/house/current-user-invitation-list';
+import CurrentUserMemberList from '@/components/house/current-user-member-list';
+import { m } from '@/paraglide/messages';
+import { housesQueries } from '@/service/queries';
+import { authClient } from '@lib/auth-client';
+import { useQuery } from '@tanstack/react-query';
+import { createFileRoute } from '@tanstack/react-router';
+
+export const Route = createFileRoute('/(app)/(auth)/management/houses')({
+ component: RouteComponent,
+ staticData: { breadcrumb: () => m.nav_houses() },
+});
+
+function RouteComponent() {
+ const { data: houses } = useQuery(housesQueries.currentUser());
+ const { data: activeHouse } = authClient.useActiveOrganization();
+
+ if (!activeHouse || !houses) return null;
+
+ return (
+
+ );
+}
diff --git a/src/routes/(app)/(auth)/management/index.tsx b/src/routes/(app)/(auth)/management/index.tsx
new file mode 100644
index 0000000..e131b56
--- /dev/null
+++ b/src/routes/(app)/(auth)/management/index.tsx
@@ -0,0 +1,9 @@
+import { createFileRoute } from '@tanstack/react-router'
+
+export const Route = createFileRoute('/(app)/(auth)/management/')({
+ component: RouteComponent,
+})
+
+function RouteComponent() {
+ return
Hello "/(app)/(auth)/management/"!
+}
diff --git a/src/routes/(app)/(auth)/management/route.tsx b/src/routes/(app)/(auth)/management/route.tsx
new file mode 100644
index 0000000..be8cca7
--- /dev/null
+++ b/src/routes/(app)/(auth)/management/route.tsx
@@ -0,0 +1,6 @@
+import { m } from '@/paraglide/messages';
+import { createFileRoute } from '@tanstack/react-router';
+
+export const Route = createFileRoute('/(app)/(auth)/management')({
+ staticData: { breadcrumb: () => m.nav_label_management() },
+});
diff --git a/src/routes/(app)/index.tsx b/src/routes/(app)/index.tsx
index 2313409..cd0b10c 100644
--- a/src/routes/(app)/index.tsx
+++ b/src/routes/(app)/index.tsx
@@ -1,12 +1,27 @@
-import { m } from '@/paraglide/messages';
+import { m } from '@paraglide/messages';
import { createFileRoute } from '@tanstack/react-router';
+import { useState } from 'react';
export const Route = createFileRoute('/(app)/')({
component: App,
staticData: { breadcrumb: () => m.nav_home() },
});
+const testselect = [
+ {
+ value: '1',
+ label: 'Sam',
+ email: 'luu.dat.tham@gmail.com',
+ },
+ {
+ value: '2',
+ label: 'Raysam',
+ email: 'raysam024@gmail.com',
+ },
+];
+
function App() {
+ const [value, setValue] = useState
();
return (
diff --git a/src/routes/(app)/route.tsx b/src/routes/(app)/route.tsx
index b3ba338..f8fcaf7 100644
--- a/src/routes/(app)/route.tsx
+++ b/src/routes/(app)/route.tsx
@@ -2,8 +2,8 @@ import { AuthProvider } from '@/components/auth/auth-provider';
import Header from '@/components/Header';
import AppSidebar from '@/components/sidebar/app-sidebar';
import { SidebarInset, SidebarProvider } from '@/components/ui/sidebar';
-import { Locale, setLocale } from '@/paraglide/runtime';
-import { settingQueries } from '@/service/queries';
+import { Locale, setLocale } from '@paraglide/runtime';
+import { settingQueries } from '@service/queries';
import { useQuery } from '@tanstack/react-query';
import { createFileRoute, Outlet } from '@tanstack/react-router';
import { useEffect } from 'react';
@@ -23,7 +23,7 @@ function RouteComponent() {
}, [language]);
return (
-
+
diff --git a/src/routes/__root.tsx b/src/routes/__root.tsx
index f4ba918..0eae837 100644
--- a/src/routes/__root.tsx
+++ b/src/routes/__root.tsx
@@ -1,13 +1,13 @@
import NotFound from '@/components/NotFound';
import { Toaster } from '@/components/ui/sonner';
-import { getLocale } from '@/paraglide/runtime';
-import { sessionQueries } from '@/service/queries';
+import { getLocale } from '@paraglide/runtime';
import {
CheckIcon,
InfoIcon,
WarningIcon,
XCircleIcon,
} from '@phosphor-icons/react';
+import { sessionQueries } from '@service/queries';
import { TanStackDevtools } from '@tanstack/react-devtools';
import type { QueryClient } from '@tanstack/react-query';
import {
diff --git a/src/routes/api.auth.$.ts b/src/routes/api.auth.$.ts
index cd07ea6..a2e3ae5 100644
--- a/src/routes/api.auth.$.ts
+++ b/src/routes/api.auth.$.ts
@@ -1,15 +1,15 @@
-import { createFileRoute } from '@tanstack/react-router'
-import { auth } from '@/lib/auth'
+import { auth } from '@lib/auth';
+import { createFileRoute } from '@tanstack/react-router';
export const Route = createFileRoute('/api/auth/$')({
server: {
handlers: {
GET: async ({ request }: { request: Request }) => {
- return await auth.handler(request)
+ return await auth.handler(request);
},
POST: async ({ request }: { request: Request }) => {
- return await auth.handler(request)
+ return await auth.handler(request);
},
},
},
-})
+});
diff --git a/src/server.ts b/src/server.ts
index fc56df3..118c2e1 100644
--- a/src/server.ts
+++ b/src/server.ts
@@ -1,5 +1,5 @@
+import { paraglideMiddleware } from '@paraglide/server.js';
import handler from '@tanstack/react-start/server-entry';
-import { paraglideMiddleware } from './paraglide/server.js';
export default {
fetch(req: Request): Promise {
diff --git a/src/service/audit.api.ts b/src/service/audit.api.ts
index 6e00cbb..2f8090f 100644
--- a/src/service/audit.api.ts
+++ b/src/service/audit.api.ts
@@ -1,6 +1,7 @@
import { prisma } from '@/db';
import { AuditWhereInput } from '@/generated/prisma/models';
-import { authMiddleware } from '@/lib/middleware';
+import { parseError } from '@lib/errors';
+import { authMiddleware } from '@lib/middleware';
import { createServerFn } from '@tanstack/react-start';
import { auditListSchema } from './audit.schema';
@@ -62,7 +63,8 @@ export const getAllAudit = createServerFn({ method: 'GET' })
},
};
} catch (error) {
- console.log(error);
- throw error;
+ console.error(error);
+ const { message, code } = parseError(error);
+ throw { message, code };
}
});
diff --git a/src/utils/disk-storage.ts b/src/service/disk-storage.ts
similarity index 87%
rename from src/utils/disk-storage.ts
rename to src/service/disk-storage.ts
index 6ae31fc..5794f83 100644
--- a/src/utils/disk-storage.ts
+++ b/src/service/disk-storage.ts
@@ -1,3 +1,4 @@
+import { AppError } from '@/lib/errors';
import fs, { writeFile } from 'fs/promises';
import path from 'path';
@@ -19,8 +20,10 @@ export async function saveFile(key: string, file: Buffer | File) {
return key;
} catch (error) {
console.error(`Error saving file: ${key}`, error);
- throw new Error(
+ throw new AppError(
+ 'FILE_SAVE_ERROR',
`Failed to save file: ${error instanceof Error ? error.message : 'Unknown error'}`,
+ 500,
);
}
}
diff --git a/src/service/house.api.ts b/src/service/house.api.ts
new file mode 100644
index 0000000..a58ac4e
--- /dev/null
+++ b/src/service/house.api.ts
@@ -0,0 +1,311 @@
+import { prisma } from '@/db';
+import { OrganizationWhereInput } from '@/generated/prisma/models';
+import { DB_TABLE, LOG_ACTION } from '@/types/enum';
+import { auth } from '@lib/auth';
+import { parseError } from '@lib/errors';
+import { authMiddleware } from '@lib/middleware';
+import { createServerFn } from '@tanstack/react-start';
+import { getRequestHeaders } from '@tanstack/react-start/server';
+import {
+ baseHouse,
+ houseCreateBESchema,
+ houseEditBESchema,
+ houseListSchema,
+ invitationCreateBESchema,
+} from './house.schema';
+import { createAuditLog } from './repository';
+
+export const getAllHouse = createServerFn({ method: 'GET' })
+ .middleware([authMiddleware])
+ .inputValidator(houseListSchema)
+ .handler(async ({ data }) => {
+ try {
+ const { page, limit, keyword } = data;
+ const skip = (page - 1) * limit;
+
+ const where: OrganizationWhereInput = {
+ OR: [
+ {
+ name: {
+ contains: keyword,
+ mode: 'insensitive',
+ },
+ },
+ ],
+ };
+
+ const [list, total]: [HouseWithMembers[], number] = await Promise.all([
+ await prisma.organization.findMany({
+ where,
+ orderBy: { createdAt: 'desc' },
+ include: {
+ members: {
+ select: {
+ role: true,
+ user: {
+ select: {
+ id: true,
+ name: true,
+ email: true,
+ image: true,
+ },
+ },
+ },
+ },
+ },
+ take: limit,
+ skip,
+ }),
+ await prisma.organization.count({ where }),
+ ]);
+
+ const totalPage = Math.ceil(+total / limit);
+
+ return {
+ result: list,
+ pagination: {
+ currentPage: page,
+ totalPage,
+ totalItem: total,
+ },
+ };
+ } catch (error) {
+ console.error(error);
+ const { message, code } = parseError(error);
+ throw { message, code };
+ }
+ });
+
+export const getCurrentUserHouses = createServerFn({ method: 'GET' })
+ .middleware([authMiddleware])
+ .handler(async ({ context: { user } }) => {
+ try {
+ const houses = await prisma.organization.findMany({
+ where: { members: { some: { userId: user.id } } },
+ orderBy: { createdAt: 'asc' },
+ include: {
+ _count: {
+ select: {
+ members: true,
+ },
+ },
+ members: {
+ select: {
+ role: true,
+ user: {
+ select: {
+ id: true,
+ name: true,
+ email: true,
+ image: true,
+ },
+ },
+ },
+ },
+ },
+ });
+
+ return houses;
+ } catch (error) {
+ console.error(error);
+ const { message, code } = parseError(error);
+ throw { message, code };
+ }
+ });
+
+export const createHouse = createServerFn({ method: 'POST' })
+ .middleware([authMiddleware])
+ .inputValidator(houseCreateBESchema)
+ .handler(async ({ data, context: { user } }) => {
+ try {
+ const result = await auth.api.createOrganization({
+ body: data,
+ });
+
+ if (!result) throw Error('Failed to create house');
+
+ await createAuditLog({
+ action: LOG_ACTION.CREATE,
+ tableName: DB_TABLE.ORGANIZATION,
+ recordId: result.id,
+ oldValue: '',
+ newValue: JSON.stringify(result),
+ userId: user.id,
+ });
+
+ return result;
+ } catch (error) {
+ console.error(error);
+ const { message, code } = parseError(error);
+ throw { message, code };
+ }
+ });
+
+export const updateHouse = createServerFn({ method: 'POST' })
+ .middleware([authMiddleware])
+ .inputValidator(houseEditBESchema)
+ .handler(async ({ data, context: { user } }) => {
+ try {
+ const currentHouse = await prisma.organization.findUnique({
+ where: { id: data.id },
+ });
+ if (!currentHouse) throw Error('House not found');
+
+ const { id, slug, name, color } = data;
+ const result = await prisma.organization.update({
+ where: { id },
+ data: {
+ name,
+ slug,
+ color,
+ },
+ });
+
+ await createAuditLog({
+ action: LOG_ACTION.UPDATE,
+ tableName: DB_TABLE.ORGANIZATION,
+ recordId: id,
+ oldValue: JSON.stringify(currentHouse),
+ newValue: JSON.stringify(result),
+ userId: user.id,
+ });
+
+ return result;
+ } catch (error) {
+ console.error(error);
+ const { message, code } = parseError(error);
+ throw { message, code };
+ }
+ });
+
+export const deleteHouse = createServerFn({ method: 'POST' })
+ .middleware([authMiddleware])
+ .inputValidator(baseHouse)
+ .handler(async ({ data, context: { user } }) => {
+ try {
+ const currentHouse = await prisma.organization.findUnique({
+ where: { id: data.id },
+ });
+ if (!currentHouse) throw Error('House not found');
+
+ const result = await Promise.all([
+ prisma.organization.delete({
+ where: { id: data.id },
+ }),
+ prisma.member.deleteMany({
+ where: { organizationId: data.id },
+ }),
+ prisma.invitation.deleteMany({
+ where: { organizationId: data.id },
+ }),
+ ]);
+
+ await createAuditLog({
+ action: LOG_ACTION.DELETE,
+ tableName: DB_TABLE.ORGANIZATION,
+ recordId: result[0]?.id,
+ oldValue: JSON.stringify(currentHouse),
+ newValue: '',
+ userId: user.id,
+ });
+
+ return result;
+ } catch (error) {
+ console.error(error);
+ const { message, code } = parseError(error);
+ throw { message, code };
+ }
+ });
+
+export const deleteUserHouse = createServerFn({ method: 'POST' })
+ .middleware([authMiddleware])
+ .inputValidator(baseHouse)
+ .handler(async ({ data, context: { user } }) => {
+ try {
+ const currentHouse = await prisma.organization.findUnique({
+ where: { id: data.id },
+ });
+ if (!currentHouse) throw Error('House not found');
+
+ const headers = getRequestHeaders();
+ const result = await auth.api.deleteOrganization({
+ body: {
+ organizationId: data.id, // required
+ },
+ headers,
+ });
+
+ if (result) {
+ await createAuditLog({
+ action: LOG_ACTION.DELETE,
+ tableName: DB_TABLE.ORGANIZATION,
+ recordId: result?.id,
+ oldValue: JSON.stringify(currentHouse),
+ newValue: '',
+ userId: user.id,
+ });
+ }
+
+ return result;
+ } catch (error) {
+ console.error(error);
+ const { message, code } = parseError(error);
+ throw { message, code };
+ }
+ });
+
+export const invitationMember = createServerFn({ method: 'POST' })
+ .middleware([authMiddleware])
+ .inputValidator(invitationCreateBESchema)
+ .handler(async ({ data, context: { user } }) => {
+ try {
+ const headers = getRequestHeaders();
+ const body = {
+ email: data.email,
+ role: data.role,
+ organizationId: data.houseId,
+ };
+
+ const result = await auth.api.createInvitation({
+ body,
+ headers,
+ });
+
+ if (result) {
+ await createAuditLog({
+ action: LOG_ACTION.CREATE,
+ tableName: DB_TABLE.INVITATION,
+ recordId: result.id,
+ oldValue: '',
+ newValue: JSON.stringify(body),
+ userId: user.id,
+ });
+ }
+
+ return result;
+ } catch (error) {
+ console.error(error);
+ const { message, code } = parseError(error);
+ throw { message, code };
+ }
+ });
+
+export const cancelInvitation = createServerFn({ method: 'POST' })
+ .middleware([authMiddleware])
+ .inputValidator(baseHouse)
+ .handler(async ({ data }) => {
+ try {
+ const headers = getRequestHeaders();
+ const result = await auth.api.cancelInvitation({
+ body: {
+ invitationId: data.id, // required
+ },
+ headers,
+ });
+ return result;
+ } catch (error) {
+ console.error(error);
+ const { message, code } = parseError(error);
+ throw { message, code };
+ }
+ });
diff --git a/src/service/house.schema.ts b/src/service/house.schema.ts
new file mode 100644
index 0000000..f98fd96
--- /dev/null
+++ b/src/service/house.schema.ts
@@ -0,0 +1,65 @@
+import { m } from '@paraglide/messages';
+import z from 'zod';
+
+export const baseHouse = z.object({
+ id: z.string().nonempty(m.houses_page_message_house_not_found()),
+});
+
+export const houseListSchema = z.object({
+ page: z.coerce.number().min(1).default(1),
+ limit: z.coerce.number().min(10).max(100).default(10),
+ keyword: z.string().optional(),
+});
+
+export const houseCreateSchema = z.object({
+ name: z
+ .string()
+ .nonempty(m.common_is_required({ field: m.houses_page_form_name() })),
+ userId: z
+ .string()
+ .nonempty(m.common_is_required({ field: m.houses_page_form_user() })),
+ color: z
+ .string()
+ .nonempty(m.common_is_required({ field: m.houses_page_form_color() })),
+});
+
+export const houseCreateBESchema = houseCreateSchema.extend({
+ slug: z.string().nonempty(m.common_is_required({ field: 'Slug' })),
+});
+
+export const houseCreateByUserBESchema = houseCreateBESchema.omit({
+ userId: true,
+});
+
+export const houseEditSchema = baseHouse.extend({
+ name: z
+ .string()
+ .nonempty(m.common_is_required({ field: m.houses_page_form_name() })),
+ color: z
+ .string()
+ .nonempty(m.common_is_required({ field: m.houses_page_form_color() })),
+});
+
+export const houseEditBESchema = houseEditSchema.extend({
+ slug: z.string().nonempty(m.common_is_required({ field: 'Slug' })),
+});
+
+export const RoleHouseEnum = z.enum(
+ ['owner', 'admin', 'member'],
+ m.users_page_message_role_select(),
+);
+
+const invitationCreateSchema = z.object({
+ email: z
+ .string()
+ .nonempty(m.common_is_required({ field: m.houses_page_form_user() })),
+ houseId: z.string().nonempty(m.houses_page_message_house_not_found()),
+});
+
+export const invitationCreateFESchema = invitationCreateSchema.extend({
+ role: z.string().nonempty(m.users_page_message_role_select()),
+});
+
+export const invitationCreateBESchema = invitationCreateSchema.extend({
+ role: RoleHouseEnum,
+});
diff --git a/src/service/profile.api.ts b/src/service/profile.api.ts
index d496c80..387a080 100644
--- a/src/service/profile.api.ts
+++ b/src/service/profile.api.ts
@@ -1,17 +1,92 @@
-import { authMiddleware } from '@/lib/middleware';
-import { saveFile } from '@/utils/disk-storage';
+import { prisma } from '@/db';
+import { auth } from '@/lib/auth';
+import { parseError } from '@/lib/errors';
+import { DB_TABLE, LOG_ACTION } from '@/types/enum';
+import { authMiddleware } from '@lib/middleware';
import { createServerFn } from '@tanstack/react-start';
+import { getRequestHeaders } from '@tanstack/react-start/server';
import z from 'zod';
+import { saveFile } from './disk-storage';
+import { createAuditLog } from './repository';
+import { changePasswordBESchema } from './user.schema';
-export const uploadProfileImage = createServerFn({ method: 'POST' })
+export const updateProfile = createServerFn({ method: 'POST' })
.middleware([authMiddleware])
.inputValidator(z.instanceof(FormData))
- .handler(async ({ data: formData }) => {
- const uuid = crypto.randomUUID();
- const file = formData.get('file') as File;
- if (!(file instanceof File)) throw new Error('File not found');
- const imageKey = `${uuid}.${file.type.split('/')[1]}`;
- const buffer = Buffer.from(await file.arrayBuffer());
- await saveFile(imageKey, buffer);
- return { imageKey };
+ .handler(async ({ data: formData, context: { user } }) => {
+ try {
+ let imageKey;
+ const file = formData.get('file') as File;
+ if (file) {
+ const uuid = crypto.randomUUID();
+ if (!(file instanceof File)) throw new Error('File not found');
+ const buffer = Buffer.from(await file.arrayBuffer());
+ imageKey = await saveFile(`${uuid}.${file.type.split('/')[1]}`, buffer);
+ }
+
+ const getOldUser = await prisma.user.findUnique({
+ where: { id: user.id },
+ });
+
+ const name = formData.get('name') as string;
+
+ const newUser = JSON.parse(JSON.stringify({ name, image: imageKey }));
+ const keys = Object.keys(newUser);
+ const oldUser = Object.fromEntries(
+ Object.entries(getOldUser || {}).filter(([key]) => keys.includes(key)),
+ );
+
+ const headers = getRequestHeaders();
+ const result = await auth.api.updateUser({
+ body: newUser,
+ headers,
+ });
+
+ await createAuditLog({
+ action: LOG_ACTION.UPDATE,
+ tableName: DB_TABLE.USER,
+ recordId: user.id,
+ oldValue: JSON.stringify(oldUser),
+ newValue: JSON.stringify(newUser),
+ userId: user.id,
+ });
+
+ return result;
+ } catch (error) {
+ console.error(error);
+ const { message, code } = parseError(error);
+ throw { message, code };
+ }
+ });
+
+export const changePassword = createServerFn({ method: 'POST' })
+ .middleware([authMiddleware])
+ .inputValidator(changePasswordBESchema)
+ .handler(async ({ data, context: { user } }) => {
+ try {
+ const headers = getRequestHeaders();
+ const result = await auth.api.changePassword({
+ body: {
+ newPassword: data.newPassword, // required
+ currentPassword: data.currentPassword, // required
+ revokeOtherSessions: true,
+ },
+ headers,
+ });
+
+ await createAuditLog({
+ action: LOG_ACTION.CHANGE_PASSWORD,
+ tableName: DB_TABLE.ACCOUNT,
+ recordId: user.id,
+ oldValue: 'Change Password',
+ newValue: 'Change Password',
+ userId: user.id,
+ });
+
+ return result;
+ } catch (error) {
+ // console.error(error);
+ const { message, code } = parseError(error);
+ throw { message, code };
+ }
});
diff --git a/src/service/profile.schema.ts b/src/service/profile.schema.ts
index 99d49c1..927788e 100644
--- a/src/service/profile.schema.ts
+++ b/src/service/profile.schema.ts
@@ -1,4 +1,4 @@
-import { m } from '@/paraglide/messages';
+import { m } from '@paraglide/messages';
import z from 'zod';
export const profileUpdateSchema = z.object({
diff --git a/src/service/queries.ts b/src/service/queries.ts
index c8bbadd..25ec7ea 100644
--- a/src/service/queries.ts
+++ b/src/service/queries.ts
@@ -1,12 +1,13 @@
-import { getSession } from '@/lib/auth/session';
+import { getSession } from '@lib/auth/session';
import { queryOptions } from '@tanstack/react-query';
import { getAllAudit } from './audit.api';
+import { getAllHouse, getCurrentUserHouses } from './house.api';
import {
getAdminSettings,
getCurrentUserLanguage,
getUserSettings,
} from './setting.api';
-import { getAllUser } from './user.api';
+import { getAllUser, getUserForSelect } from './user.api';
export const sessionQueries = {
all: ['auth'],
@@ -54,4 +55,23 @@ export const usersQueries = {
queryKey: [...usersQueries.all, 'list', params],
queryFn: () => getAllUser({ data: params }),
}),
+ select: (params: { keyword?: string }, noself: boolean = false) =>
+ queryOptions({
+ queryKey: [...usersQueries.all, 'select', params],
+ queryFn: () => getUserForSelect({ data: { ...params, noself } }),
+ }),
+};
+
+export const housesQueries = {
+ all: ['houses'],
+ list: (params: { page: number; limit: number; keyword?: string }) =>
+ queryOptions({
+ queryKey: [...housesQueries.all, 'list', params],
+ queryFn: () => getAllHouse({ data: params }),
+ }),
+ currentUser: () =>
+ queryOptions({
+ queryKey: [...housesQueries.all, 'currentUser'],
+ queryFn: () => getCurrentUserHouses(),
+ }),
};
diff --git a/src/service/repository.ts b/src/service/repository.ts
index 3981060..3f67e49 100644
--- a/src/service/repository.ts
+++ b/src/service/repository.ts
@@ -41,14 +41,23 @@ export async function getAllAdminSettings(valueOnly = false) {
}
export const createAuditLog = async (data: Omit) => {
- try {
- await prisma.audit.create({
- data: {
- ...data,
- },
- });
- } catch (error) {
- console.log(error);
- throw error;
- }
+ await prisma.audit.create({
+ data: {
+ ...data,
+ },
+ });
+};
+
+export const getInitialOrganization = async (userId: string) => {
+ const organization = await prisma.organization.findFirst({
+ where: {
+ members: {
+ some: {
+ userId,
+ },
+ },
+ },
+ });
+
+ return organization;
};
diff --git a/src/service/setting.api.ts b/src/service/setting.api.ts
index 7636dcc..8dba773 100644
--- a/src/service/setting.api.ts
+++ b/src/service/setting.api.ts
@@ -1,7 +1,8 @@
import { prisma } from '@/db';
-import { authMiddleware } from '@/lib/middleware';
-import { extractDiffObjects } from '@/utils/helper';
+import { parseError } from '@/lib/errors';
+import { authMiddleware } from '@lib/middleware';
import { createServerFn } from '@tanstack/react-start';
+import { extractDiffObjects } from '@utils/helper';
import { createAuditLog, getAllAdminSettings } from './repository';
import { settingSchema, userSettingSchema } from './setting.schema';
@@ -25,8 +26,9 @@ export const getCurrentUserLanguage = createServerFn({ method: 'GET' })
return value.language;
} catch (error) {
- console.log(error);
- throw error;
+ console.error(error);
+ const { message, code } = parseError(error);
+ throw { message, code };
}
});
@@ -71,8 +73,9 @@ export const updateAdminSettings = createServerFn({ method: 'POST' })
return { success: true };
} catch (error) {
- console.log(error);
- throw error;
+ console.error(error);
+ const { message, code } = parseError(error);
+ throw { message, code };
}
});
@@ -102,8 +105,9 @@ export const getUserSettings = createServerFn({ method: 'GET' })
value: JSON.parse(settings.value) as UserSetting,
};
} catch (error) {
- console.log(error);
- throw error;
+ console.error(error);
+ const { message, code } = parseError(error);
+ throw { message, code };
}
});
@@ -147,7 +151,8 @@ export const updateUserSettings = createServerFn({ method: 'POST' })
return { success: true };
} catch (error) {
- console.log(error);
- throw error;
+ console.error(error);
+ const { message, code } = parseError(error);
+ throw { message, code };
}
});
diff --git a/src/service/setting.schema.ts b/src/service/setting.schema.ts
index 75cfe4a..36c9121 100644
--- a/src/service/setting.schema.ts
+++ b/src/service/setting.schema.ts
@@ -1,4 +1,4 @@
-import { m } from '@/paraglide/messages';
+import { m } from '@paraglide/messages';
import z from 'zod';
export const settingSchema = z.object({
diff --git a/src/service/user.api.ts b/src/service/user.api.ts
index 2d9ca09..acae38d 100644
--- a/src/service/user.api.ts
+++ b/src/service/user.api.ts
@@ -1,52 +1,105 @@
import { prisma } from '@/db';
-import { auth } from '@/lib/auth';
-import { authMiddleware } from '@/lib/middleware';
import { DB_TABLE, LOG_ACTION } from '@/types/enum';
-import { parseError } from '@/utils/helper';
+import { auth } from '@lib/auth';
+import { parseError } from '@lib/errors';
+import { authMiddleware } from '@lib/middleware';
import { createServerFn } from '@tanstack/react-start';
import { getRequestHeaders } from '@tanstack/react-start/server';
import { createAuditLog } from './repository';
import {
baseUser,
userBanSchema,
- userCreateSchema,
+ userCreateBESchema,
+ userForSelectSchema,
userListSchema,
userSetPasswordSchema,
userUpdateInfoSchema,
- userUpdateRoleSchema,
+ userUpdateRoleBESchema,
} from './user.schema';
export const getAllUser = createServerFn({ method: 'GET' })
.middleware([authMiddleware])
.inputValidator(userListSchema)
.handler(async ({ data }) => {
- const headers = getRequestHeaders();
- const { page, limit, keyword } = data;
+ try {
+ const headers = getRequestHeaders();
+ const { page, limit, keyword } = data;
- const list = await auth.api.listUsers({
- query: {
- searchValue: keyword,
- searchField: 'name',
- searchOperator: 'contains',
- sortBy: 'createdAt',
- sortDirection: 'asc',
- limit,
- offset: (page - 1) * limit,
- },
- headers,
- });
+ const list = await auth.api.listUsers({
+ query: {
+ searchValue: keyword,
+ searchField: 'name',
+ searchOperator: 'contains',
+ sortBy: 'createdAt',
+ sortDirection: 'asc',
+ limit,
+ offset: (page - 1) * limit,
+ },
+ headers,
+ });
- const totalItem = list.total;
- const totalPage = Math.ceil(totalItem / limit);
+ const totalItem = list.total;
+ const totalPage = Math.ceil(totalItem / limit);
- return {
- result: list.users,
- pagination: {
- currentPage: page,
- totalPage,
- totalItem,
- },
- };
+ return {
+ result: list.users,
+ pagination: {
+ currentPage: page,
+ totalPage,
+ totalItem,
+ },
+ };
+ } catch (error) {
+ console.error(error);
+ const { message, code } = parseError(error);
+ throw { message, code };
+ }
+ });
+
+export const getUserForSelect = createServerFn({ method: 'GET' })
+ .middleware([authMiddleware])
+ .inputValidator(userForSelectSchema)
+ .handler(async ({ data, context: { user } }) => {
+ try {
+ const { keyword, noself } = data;
+
+ const result = await prisma.user.findMany({
+ where: {
+ OR: [
+ {
+ name: {
+ contains: keyword,
+ mode: 'insensitive',
+ },
+ },
+ {
+ email: {
+ contains: keyword,
+ mode: 'insensitive',
+ },
+ },
+ ],
+ AND: {
+ NOT: {
+ id: noself ? user.id : undefined,
+ },
+ },
+ },
+ select: {
+ id: true,
+ name: true,
+ email: true,
+ },
+ orderBy: { createdAt: 'desc' },
+ take: 5,
+ });
+
+ return result;
+ } catch (error) {
+ console.error(error);
+ const { message, code } = parseError(error);
+ throw { message, code };
+ }
});
export const setUserPassword = createServerFn({ method: 'POST' })
@@ -74,6 +127,7 @@ export const setUserPassword = createServerFn({ method: 'POST' })
return result;
} catch (error) {
+ console.error(error);
const { message, code } = parseError(error);
throw { message, code };
}
@@ -112,6 +166,7 @@ export const updateUserInformation = createServerFn({ method: 'POST' })
return result;
} catch (error) {
+ console.error(error);
const { message, code } = parseError(error);
throw { message, code };
}
@@ -119,7 +174,7 @@ export const updateUserInformation = createServerFn({ method: 'POST' })
export const setUserRole = createServerFn({ method: 'POST' })
.middleware([authMiddleware])
- .inputValidator(userUpdateRoleSchema)
+ .inputValidator(userUpdateRoleBESchema)
.handler(async ({ data, context: { user } }) => {
try {
const currentUser = await prisma.user.findUnique({
@@ -150,6 +205,7 @@ export const setUserRole = createServerFn({ method: 'POST' })
return result;
} catch (error) {
+ console.error(error);
const { message, code } = parseError(error);
throw { message, code };
}
@@ -185,6 +241,7 @@ export const banUser = createServerFn({ method: 'POST' })
return result;
} catch (error) {
+ console.error(error);
const { message, code } = parseError(error);
throw { message, code };
}
@@ -215,6 +272,7 @@ export const unbanUser = createServerFn({ method: 'POST' })
return result;
} catch (error) {
+ console.error(error);
const { message, code } = parseError(error);
throw { message, code };
}
@@ -222,7 +280,7 @@ export const unbanUser = createServerFn({ method: 'POST' })
export const createUser = createServerFn({ method: 'POST' })
.middleware([authMiddleware])
- .inputValidator(userCreateSchema)
+ .inputValidator(userCreateBESchema)
.handler(async ({ data, context: { user } }) => {
try {
const result = await auth.api.createUser({
diff --git a/src/service/user.schema.ts b/src/service/user.schema.ts
index 98c8db1..de3fc24 100644
--- a/src/service/user.schema.ts
+++ b/src/service/user.schema.ts
@@ -1,11 +1,11 @@
-import { m } from '@/paraglide/messages';
+import { m } from '@paraglide/messages';
import z from 'zod';
export const baseUser = z.object({
id: z.string().nonempty(m.users_page_message_user_not_found()),
});
-export const ChangePasswordFormSchema = z
+export const changePasswordFormSchema = z
.object({
currentPassword: z.string().nonempty(
m.common_is_required({
@@ -33,7 +33,20 @@ export const ChangePasswordFormSchema = z
}
});
-export type ChangePassword = z.infer;
+export const changePasswordBESchema = z.object({
+ currentPassword: z.string().nonempty(
+ m.common_is_required({
+ field: m.change_password_form_current_password(),
+ }),
+ ),
+ newPassword: z.string().nonempty(
+ m.common_is_required({
+ field: m.change_password_form_new_password(),
+ }),
+ ),
+});
+
+export type ChangePassword = z.infer;
export const userListSchema = z.object({
page: z.coerce.number().min(1).default(1),
@@ -41,6 +54,11 @@ export const userListSchema = z.object({
keyword: z.string().optional(),
});
+export const userForSelectSchema = z.object({
+ keyword: z.string().optional(),
+ noself: z.boolean().optional().default(false),
+});
+
export const userSetPasswordSchema = baseUser.extend({
password: z
.string()
@@ -65,6 +83,10 @@ export const RoleEnum = z.enum(
);
export const userUpdateRoleSchema = baseUser.extend({
+ role: z.string().nonempty(m.users_page_message_role_select()),
+});
+
+export const userUpdateRoleBESchema = baseUser.extend({
role: RoleEnum,
});
@@ -77,7 +99,7 @@ export const userBanSchema = baseUser.extend({
banExp: z.number().int().min(1, m.users_page_message_select_min_one_day()),
});
-export const userCreateSchema = z.object({
+const userCreateBaseSchema = z.object({
email: z
.string()
.nonempty(m.common_is_required({ field: m.login_page_form_email() }))
@@ -94,5 +116,12 @@ export const userCreateSchema = z.object({
field: m.profile_form_name(),
}),
),
+});
+
+export const userCreateFESchema = userCreateBaseSchema.extend({
+ role: z.string().nonempty(m.users_page_message_role_select()),
+});
+
+export const userCreateBESchema = userCreateBaseSchema.extend({
role: RoleEnum,
});
diff --git a/src/types/common.d.ts b/src/types/common.d.ts
index 894c5e1..e69de29 100644
--- a/src/types/common.d.ts
+++ b/src/types/common.d.ts
@@ -1,4 +0,0 @@
-export interface ReturnError extends Error {
- message: string;
- code: string;
-}
diff --git a/src/types/db.d.ts b/src/types/db.d.ts
index ef0beca..0e5b9d0 100644
--- a/src/types/db.d.ts
+++ b/src/types/db.d.ts
@@ -11,4 +11,33 @@ declare global {
};
};
}>;
+
+ type HouseWithMembers = Prisma.OrganizationGetPayload<{
+ include: {
+ members: {
+ select: {
+ role: true;
+ user: {
+ select: {
+ id: true;
+ name: true;
+ email: true;
+ image: true;
+ };
+ };
+ };
+ };
+ };
+ }>;
+
+ type HouseWithMembersCount = HouseWithMembers & {
+ _count: {
+ members: number;
+ };
+ };
+
+ type ReturnError = Error & {
+ code: string;
+ message: string;
+ };
}
diff --git a/src/types/enum.ts b/src/types/enum.ts
index 59fd043..690e506 100644
--- a/src/types/enum.ts
+++ b/src/types/enum.ts
@@ -26,3 +26,22 @@ export const DB_TABLE = {
} as const;
export type DB_TABLE = (typeof DB_TABLE)[keyof typeof DB_TABLE];
+
+export const ROLE_NAME = {
+ ADMIN: 'admin',
+ USER: 'user',
+ MEMBER: 'member',
+ OWNER: 'owner',
+} as const;
+
+export type ROLE_NAME = (typeof ROLE_NAME)[keyof typeof ROLE_NAME];
+
+export const INVITE_STATUS = {
+ PENDING: 'pending',
+ ACCEPT: 'accept',
+ REJECT: 'reject',
+ CANCELED: 'canceled',
+ EXPIRED: 'expired',
+} as const;
+
+export type INVITE_STATUS = (typeof INVITE_STATUS)[keyof typeof INVITE_STATUS];
diff --git a/src/utils/formatters.ts b/src/utils/formatters.ts
index daf05aa..19f2de6 100644
--- a/src/utils/formatters.ts
+++ b/src/utils/formatters.ts
@@ -1,4 +1,4 @@
-import { getLocale } from '@/paraglide/runtime';
+import { getLocale } from '@paraglide/runtime';
const locale = getLocale() === 'vi' ? 'vi-VN' : 'en-US';
diff --git a/src/utils/helper.ts b/src/utils/helper.ts
index 66a003a..5faffb3 100644
--- a/src/utils/helper.ts
+++ b/src/utils/helper.ts
@@ -26,18 +26,13 @@ export function extractDiffObjects(
);
}
-export function parseError(error: unknown) {
- if (typeof error === 'object' && error !== null && 'body' in error) {
- const e = error as any;
- return {
- message: e.body?.message ?? 'Unknown error',
- code: e.body?.code,
- };
- }
-
- if (error instanceof Error) {
- return { message: error.message };
- }
-
- return { message: String(error) };
+export function slugify(text: string) {
+ return text
+ .toLowerCase()
+ .normalize('NFD') // tách chữ và dấu
+ .replace(/[\u0300-\u036f]/g, '') // xóa dấu
+ .replace(/đ/g, 'd') // riêng chữ đ
+ .replace(/[^a-z0-9\s-]/g, '') // xóa ký tự đặc biệt
+ .trim()
+ .replace(/\s+/g, '-');
}
diff --git a/tsconfig.json b/tsconfig.json
index 4c8436b..5f5cfaf 100644
--- a/tsconfig.json
+++ b/tsconfig.json
@@ -31,6 +31,14 @@
"baseUrl": ".",
"paths": {
"@/*": ["./src/*"],
+ "@components/*": ["./src/components/*"],
+ "@ui/*": ["./src/components/ui/*"],
+ "@form/*": ["./src/components/form/*"],
+ "@hooks/*": ["./src/hooks/*"],
+ "@lib/*": ["./src/lib/*"],
+ "@paraglide/*": ["./src/paraglide/*"],
+ "@service/*": ["./src/service/*"],
+ "@utils/*": ["./src/utils/*"],
"@root/*": ["./*"]
}
}