project setup

This commit is contained in:
Sone
2025-11-02 10:08:56 +01:00
commit 568838003e
174 changed files with 27100 additions and 0 deletions
+53
View File
@@ -0,0 +1,53 @@
import { createApi, fetchBaseQuery } from "@reduxjs/toolkit/query/react";
import Constants from "expo-constants";
import { Platform } from "react-native";
import { getToken } from "@/auth/token";
export const getApiUrl = () => {
const isDev = process.env.NODE_ENV === "development" || !process.env.NODE_ENV;
const prodUrl = "https://your-production-api.com/api";
if (!isDev) return prodUrl;
if (Platform.OS === "web") {
return process.env.EXPO_PUBLIC_BACKEND_URL;
}
try {
const debuggerHost =
Constants.expoConfig?.hostUri ||
Constants.manifest2?.extra?.expoGo?.debuggerHost;
if (debuggerHost) {
const hostIp = debuggerHost.split(":")[0];
return `http://${hostIp}:5000/api`;
}
} catch (error) {
console.error("Failed to get debuggerHost:", error);
}
if (process.env.EXPO_PUBLIC_API_URL) {
return process.env.EXPO_PUBLIC_API_URL;
}
return "http://192.168.1.X:5000/api";
};
export const baseApi = createApi({
reducerPath: "api",
baseQuery: fetchBaseQuery({
baseUrl: getApiUrl(),
prepareHeaders: (headers) => {
headers.set("accept", "application/json");
const token = getToken();
if (token) {
headers.set("authorization", `Bearer ${token}`);
}
return headers;
},
}),
refetchOnFocus: true,
refetchOnReconnect: true,
tagTypes: ["Users", "Todos", "UserLogs"],
endpoints: () => ({}),
});
+59
View File
@@ -0,0 +1,59 @@
import { baseApi } from "@/store/api/baseApi";
import { Comment } from "@/store/models/Comment.model";
export const commentsApi = baseApi.injectEndpoints({
overrideExisting: false,
endpoints: (builder) => ({
getTodoComments: builder.query<Comment[], string>({
query: (todoId) => ({ url: `/todos/${todoId}/comments` }),
providesTags: (_result, _error, todoId) => [
{ type: "Todos", id: `comments-${todoId}` },
],
}),
addComment: builder.mutation<Comment, { todoId: string; content: string }>({
query: ({ todoId, content }) => ({
url: `/todos/${todoId}/comments`,
method: "POST",
body: { content },
}),
invalidatesTags: (_result, _error, { todoId }) => [
{ type: "Todos", id: `comments-${todoId}` },
],
}),
updateComment: builder.mutation<
Comment,
{ todoId: string; commentId: string; content: string }
>({
query: ({ todoId, commentId, content }) => ({
url: `/todos/${todoId}/comments/${commentId}`,
method: "PUT",
body: { content },
}),
invalidatesTags: (_result, _error, { todoId }) => [
{ type: "Todos", id: `comments-${todoId}` },
],
}),
deleteComment: builder.mutation<
{ message: string },
{ todoId: string; commentId: string }
>({
query: ({ todoId, commentId }) => ({
url: `/todos/${todoId}/comments/${commentId}`,
method: "DELETE",
}),
invalidatesTags: (_result, _error, { todoId }) => [
{ type: "Todos", id: `comments-${todoId}` },
],
}),
}),
});
export const {
useGetTodoCommentsQuery,
useAddCommentMutation,
useUpdateCommentMutation,
useDeleteCommentMutation,
} = commentsApi;
+52
View File
@@ -0,0 +1,52 @@
import { baseApi } from "@/store/api/baseApi";
export type SearchType = "user" | "admin" | "todo";
export interface SearchResult {
id: string;
type: SearchType;
title: string;
subtitle?: string;
description?: string;
createdAt?: string;
score: number;
meta?: Record<string, any>;
}
export interface QuickSearchResponse {
results: SearchResult[];
}
export interface SearchResponse {
results: SearchResult[];
total: number;
page: number;
limit: number;
}
export const searchApi = baseApi.injectEndpoints({
overrideExisting: true,
endpoints: (builder) => ({
quickSearch: builder.query<
QuickSearchResponse,
{ q: string; limit?: number }
>({
query: ({ q, limit = 8 }) => ({
url: "/search/quick",
params: { q, limit },
}),
}),
globalSearch: builder.query<
SearchResponse,
{ q: string; page?: number; limit?: number }
>({
query: ({ q, page = 1, limit = 25 }) => ({
url: "/search",
params: { q, page, limit },
}),
}),
}),
});
export const { useLazyQuickSearchQuery, useGlobalSearchQuery } = searchApi;
+133
View File
@@ -0,0 +1,133 @@
import { baseApi } from "@/store/api/baseApi";
import { Todo } from "@/store/models/Todo.model";
import { TodoStatus } from "@/constants/todoStatusEnum";
import { PriorityStatus } from "@/constants/priorityStatusEnum";
import { TableSortDirection } from "@/constants/tableSortDirectionEnum";
interface CreateTodoPayload {
title: string;
task: string;
status: TodoStatus;
priority: PriorityStatus;
dueDate: Date;
createdById: string;
assigneeIds: string[];
}
export interface TodosQueryParams {
page?: number;
limit?: number;
search?: string;
statuses?: TodoStatus[];
priorities?: PriorityStatus[];
sortBy?: "title" | "dueDate" | "status" | "priority" | "createdAt";
sortDir?: TableSortDirection;
assigneeUserId?: string;
teamOfManagerId?: string;
}
export interface PaginatedTodos {
items: Todo[];
total: number;
page: number;
limit: number;
}
const normalizeSortDir = (dir?: TableSortDirection) => {
return dir
? dir === TableSortDirection.DESCENDING
? "desc"
: "asc"
: undefined;
};
const buildTodoParams = (params?: TodosQueryParams) => {
if (!params) return {};
const {
page,
limit,
search,
statuses,
priorities,
sortBy,
sortDir,
assigneeUserId,
teamOfManagerId,
} = params;
return {
...(page !== undefined && { page }),
...(limit !== undefined && { limit }),
...(search && { search }),
...(Array.isArray(statuses) &&
statuses.length && {
statuses: statuses.join(","),
}),
...(Array.isArray(priorities) &&
priorities.length && {
priorities: priorities.join(","),
}),
...(sortBy && { sortBy }),
...(sortDir && { sortDir: normalizeSortDir(sortDir) }),
...(assigneeUserId && { assigneeUserId }),
...(teamOfManagerId && { teamOfManagerId }),
};
};
export const todosApi = baseApi.injectEndpoints({
overrideExisting: false,
endpoints: (builder) => ({
getTodosByUser: builder.query<Todo[], string>({
query: (userId) => ({ url: `/todos/user/${userId}` }),
providesTags: (result) => [
"Todos",
...(result ?? []).map(({ id }) => ({ type: "Todos", id } as const)),
],
}),
getTodos: builder.query<PaginatedTodos, TodosQueryParams | undefined>({
query: (params) => ({ url: "/todos", params: buildTodoParams(params) }),
providesTags: (result, _error, _arg) => [
"Todos",
...(result?.items ?? []).map(
({ id }) => ({ type: "Todos", id } as const)
),
],
}),
createTodo: builder.mutation<Todo, CreateTodoPayload>({
query: (data) => ({
url: "/todos",
method: "POST",
body: data,
}),
invalidatesTags: ["Todos"],
}),
updateTodo: builder.mutation<Todo, Partial<Todo>>({
query: (todo) => ({
url: `/todos/${todo.id}`,
method: "PUT",
body: todo,
}),
invalidatesTags: ["Todos"],
}),
deleteTodo: builder.mutation<void, string>({
query: (id) => ({
url: `/todos/${id}`,
method: "DELETE",
}),
invalidatesTags: ["Todos"],
}),
}),
});
export const {
useGetTodosByUserQuery,
useGetTodosQuery,
useCreateTodoMutation,
useUpdateTodoMutation,
useDeleteTodoMutation,
} = todosApi;
+150
View File
@@ -0,0 +1,150 @@
import { baseApi } from "@/store/api/baseApi";
import { User } from "@/store/models/User.model";
import { RiskEnum } from "@/constants/riskEnum";
import { TableSortDirection } from "@/constants/tableSortDirectionEnum";
export type UsersQueryParams = {
page?: number;
limit?: number;
search?: string;
risks?: RiskEnum[];
sortBy?: "name" | "email" | "title" | "role" | "risk";
sortDir?: TableSortDirection;
roleType?: "user" | "admin";
};
export type PaginatedUsers = {
items: User[];
total: number;
page: number;
limit: number;
};
export type UserLoginLog = {
id: string;
name: string;
email: string;
lastLoginAt: string | Date;
activity: string;
};
const normalizeSortDir = (dir?: TableSortDirection) => {
return dir
? dir === TableSortDirection.DESCENDING
? "desc"
: "asc"
: undefined;
};
const buildUserParams = (params?: UsersQueryParams) => {
if (!params) return {};
const { page, limit, search, risks, sortBy, sortDir, roleType } = params;
return {
...(page !== undefined && { page }),
...(limit !== undefined && { limit }),
...(search && { search }),
...(risks?.length && { risks: risks.join(",") }),
...(sortBy && { sortBy }),
...(sortDir && { sortDir: normalizeSortDir(sortDir) }),
...(roleType && { roleType }),
};
};
const buildLogParams = (params?: {
page?: number;
limit?: number;
search?: string;
}) => {
if (!params) return {};
const { page, limit, search } = params;
return {
...(page !== undefined && { page }),
...(limit !== undefined && { limit }),
...(search && { search }),
};
};
export const usersApi = baseApi.injectEndpoints({
overrideExisting: true,
endpoints: (builder) => ({
getUserLogs: builder.query<
{ items: UserLoginLog[]; total: number; page: number; limit: number },
{ page?: number; limit?: number; search?: string } | undefined
>({
query: (params) => ({
url: "/users/logs",
params: buildLogParams(params),
}),
providesTags: ["UserLogs"],
}),
getUsers: builder.query<PaginatedUsers, UsersQueryParams | undefined>({
query: (params) => ({ url: "/users", params: buildUserParams(params) }),
providesTags: (result) => [
"Users",
...(result?.items ?? []).map(
({ id }) => ({ type: "Users", id } as const)
),
],
}),
getUserById: builder.query<User, string>({
query: (id) => `/users/${id}`,
providesTags: (_res, _err, id) => [{ type: "Users", id }],
}),
deleteUser: builder.mutation<void, string>({
query: (id) => ({ url: `/users/${id}`, method: "DELETE" }),
invalidatesTags: ["Users"],
}),
deleteUsers: builder.mutation<void, string[]>({
query: (ids) => ({ url: "/users", method: "DELETE", body: { ids } }),
invalidatesTags: ["Users"],
}),
lockUser: builder.mutation<void, string>({
query: (id) => ({ url: `/users/${id}/lock`, method: "PUT" }),
invalidatesTags: ["Users"],
}),
updateUser: builder.mutation<User, Partial<User>>({
query: (user) => ({
url: `/users/${user.id}`,
method: "PUT",
body: user,
}),
invalidatesTags: (_res, _err, { id }) => [
"Users",
{ type: "Users", id },
"Todos",
],
}),
createUser: builder.mutation<User, Partial<User>>({
query: (user) => ({ url: "/users", method: "POST", body: user }),
invalidatesTags: ["Users"],
}),
resendInvitation: builder.mutation<{ message: string }, string>({
query: (id) => ({ url: `/users/${id}/invite`, method: "POST" }),
invalidatesTags: (_res, _err, id) => ["Users", { type: "Users", id }],
}),
}),
});
export const {
useGetUserLogsQuery,
useGetUsersQuery,
useGetUserByIdQuery,
useDeleteUserMutation,
useDeleteUsersMutation,
useLockUserMutation,
useUpdateUserMutation,
useCreateUserMutation,
useResendInvitationMutation,
} = usersApi;
+6
View File
@@ -0,0 +1,6 @@
import { TypedUseSelectorHook, useDispatch, useSelector } from "react-redux";
import type { AppDispatch, RootState } from "./index";
export const useAppDispatch = () => useDispatch<AppDispatch>();
export const useAppSelector: TypedUseSelectorHook<RootState> = useSelector;
+19
View File
@@ -0,0 +1,19 @@
import { configureStore } from "@reduxjs/toolkit";
import { setupListeners } from "@reduxjs/toolkit/query";
import { baseApi } from "./api/baseApi";
import screenSizeReducer from "./screen/screenSizeSlice";
export const store = configureStore({
reducer: {
screenSize: screenSizeReducer,
[baseApi.reducerPath]: baseApi.reducer,
},
middleware: (getDefaultMiddleware) =>
getDefaultMiddleware().concat(baseApi.middleware),
});
setupListeners(store.dispatch);
export type RootState = ReturnType<typeof store.getState>;
export type AppDispatch = typeof store.dispatch;
+10
View File
@@ -0,0 +1,10 @@
import { User } from "@/store/models/User.model";
export interface Comment {
id: string;
content: string;
createdAt: string | Date;
todoId: string;
userId: string;
user: Pick<User, "id" | "firstName" | "lastName" | "imageUrl" | "role">;
}
+16
View File
@@ -0,0 +1,16 @@
import { TodoStatus } from "@/constants/todoStatusEnum";
import { PriorityStatus } from "@/constants/priorityStatusEnum";
import { User } from "@/store/models/User.model";
export interface Todo {
id: string;
title: string;
task: string;
status: TodoStatus;
priority: PriorityStatus;
dueDate: Date;
createdAt: Date;
assignees: User[];
createdBy: User;
createdById: string;
}
+25
View File
@@ -0,0 +1,25 @@
import { RoleEnum } from "@/constants/roleEnum";
import { Todo } from "@/store/models/Todo.model";
import { RiskEnum } from "@/constants/riskEnum";
import { SecurityType } from "@/constants/securityTypeEnum";
export interface User {
id: string;
firstName: string;
middleName: string;
lastName: string;
email: string;
password?: string;
todos?: Todo[];
riskStatus: RiskEnum;
imageUrl?: string;
title: string;
role: RoleEnum;
isLocked: boolean;
isActivated: boolean;
phoneNumber?: string;
department?: string;
managerId?: string | null;
manager?: User | null;
securityType: SecurityType;
}
+48
View File
@@ -0,0 +1,48 @@
import { createSlice, PayloadAction } from "@reduxjs/toolkit";
import { Dimensions } from "react-native";
import type { RootState } from "../index";
export interface ScreenSizeState {
width: number;
height: number;
isMobile: boolean;
sidebarCollapsed: boolean;
}
const initialDimensions = Dimensions.get("window");
const initialState: ScreenSizeState = {
width: initialDimensions.width,
height: initialDimensions.height,
isMobile: initialDimensions.width < 768,
sidebarCollapsed: false,
};
export const screenSizeSlice = createSlice({
name: "screenSize",
initialState,
reducers: {
setScreenSize: (
state,
action: PayloadAction<{ width: number; height: number }>
) => {
state.width = action.payload.width;
state.height = action.payload.height;
state.isMobile = action.payload.width < 768;
},
setSidebarCollapsed: (state, action: PayloadAction<boolean>) => {
state.sidebarCollapsed = action.payload;
},
},
});
export const { setScreenSize, setSidebarCollapsed } = screenSizeSlice.actions;
export const selectScreenSize = (state: RootState) => state.screenSize;
export const selectIsMobile = (state: RootState) => state.screenSize.isMobile;
export const selectWidth = (state: RootState) => state.screenSize.width;
export const selectHeight = (state: RootState) => state.screenSize.height;
export const selectSidebarCollapsed = (state: RootState) => state.screenSize.sidebarCollapsed;
export default screenSizeSlice.reducer;