project setup
This commit is contained in:
@@ -0,0 +1,377 @@
|
||||
import { useEffect, useMemo, useState, useDeferredValue } from "react";
|
||||
import { View, Text, ScrollView, GestureResponderEvent } from "react-native";
|
||||
|
||||
import { useAuthContext } from "@/auth/AuthContext";
|
||||
import { Todo } from "@/store/models/Todo.model";
|
||||
import { LoadingSpinner } from "@/components/ui/LoadingSpinner";
|
||||
import { NoData } from "@/components/ui/NoData";
|
||||
import {
|
||||
useCreateTodoMutation,
|
||||
useDeleteTodoMutation,
|
||||
useGetTodosQuery,
|
||||
useUpdateTodoMutation,
|
||||
} from "@/store/api/todosApi";
|
||||
import { TableSortDirection } from "@/constants/tableSortDirectionEnum";
|
||||
import { AdminTodosTableControls } from "@/components/features/todos/AdminTodosTableControls";
|
||||
import { TodoStatus } from "@/constants/todoStatusEnum";
|
||||
import { TablePagination } from "@/components/shared/TablePagination";
|
||||
import { useAppSelector } from "@/store/hooks";
|
||||
import { selectIsMobile } from "@/store/screen/screenSizeSlice";
|
||||
import { MobileAdminTodosTable } from "@/components/features/todos/MobileAdminTodosTable";
|
||||
import { DesktopAdminTodosTable } from "@/components/features/todos/DesktopAdminTodosTable";
|
||||
import { PriorityStatus } from "@/constants/priorityStatusEnum";
|
||||
import { TodoModal } from "@/components/features/todos/TodoModal";
|
||||
import { DeleteTodoModal } from "@/components/features/todos/DeleteTodoModal";
|
||||
import { OperationMode } from "@/constants/operationModeEnum";
|
||||
import { AppSnackbar } from "@/components/ui/AppSnackbar";
|
||||
|
||||
interface CreateTodoPayload {
|
||||
title: string;
|
||||
task: string;
|
||||
status: TodoStatus;
|
||||
priority: PriorityStatus;
|
||||
dueDate: Date;
|
||||
createdById: string;
|
||||
assigneeIds: string[];
|
||||
}
|
||||
|
||||
export const AdminTodosTable = ({ initialSearchQuery = "" }: { initialSearchQuery?: string }) => {
|
||||
const { user } = useAuthContext();
|
||||
const [page, setPage] = useState(0);
|
||||
const [itemsPerPage, setItemsPerPage] = useState(25);
|
||||
const [searchQuery, setSearchQuery] = useState("");
|
||||
const [selectedStatuses, setSelectedStatuses] = useState<TodoStatus[]>([]);
|
||||
const [selectedPriorities, setSelectedPriorities] = useState<
|
||||
PriorityStatus[]
|
||||
>([]);
|
||||
const [sortColumn, setSortColumn] = useState<string | null>(null);
|
||||
const [sortDirection, setSortDirection] = useState<TableSortDirection>(
|
||||
TableSortDirection.ASCENDING
|
||||
);
|
||||
|
||||
const deferredSearch = useDeferredValue(searchQuery);
|
||||
const [debouncedSearch, setDebouncedSearch] = useState("");
|
||||
useEffect(() => {
|
||||
const t = setTimeout(() => setDebouncedSearch(deferredSearch.trim()), 450);
|
||||
return () => clearTimeout(t);
|
||||
}, [deferredSearch]);
|
||||
|
||||
// Initialize search from route param if provided
|
||||
useEffect(() => {
|
||||
const q = (initialSearchQuery || "").trim();
|
||||
if (q && q !== searchQuery) {
|
||||
setSearchQuery(q);
|
||||
setPage(0);
|
||||
}
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, [initialSearchQuery]);
|
||||
|
||||
const queryArgs = useMemo(() => {
|
||||
const allowedSorts = [
|
||||
"title",
|
||||
"dueDate",
|
||||
"status",
|
||||
"priority",
|
||||
"createdAt",
|
||||
] as const;
|
||||
const sortBy = (allowedSorts as readonly string[]).includes(
|
||||
sortColumn || ""
|
||||
)
|
||||
? (sortColumn as (typeof allowedSorts)[number])
|
||||
: undefined;
|
||||
const sortDir = sortDirection;
|
||||
const searchParam =
|
||||
debouncedSearch && debouncedSearch.length >= 2
|
||||
? debouncedSearch
|
||||
: undefined;
|
||||
return {
|
||||
page: page + 1,
|
||||
limit: itemsPerPage,
|
||||
search: searchParam,
|
||||
statuses: selectedStatuses.length ? selectedStatuses : undefined,
|
||||
priorities: selectedPriorities.length ? selectedPriorities : undefined,
|
||||
sortBy,
|
||||
sortDir,
|
||||
};
|
||||
}, [
|
||||
page,
|
||||
itemsPerPage,
|
||||
debouncedSearch,
|
||||
selectedStatuses,
|
||||
selectedPriorities,
|
||||
sortColumn,
|
||||
sortDirection,
|
||||
]);
|
||||
|
||||
const {
|
||||
data: todosPage,
|
||||
isLoading,
|
||||
isFetching,
|
||||
error,
|
||||
refetch,
|
||||
} = useGetTodosQuery(queryArgs);
|
||||
const todos = todosPage?.items ?? [];
|
||||
const totalItems = todosPage?.total ?? 0;
|
||||
|
||||
|
||||
const [createTodo, { isLoading: isCreatingTodo }] = useCreateTodoMutation();
|
||||
const [updateTodo, { isLoading: isUpdatingTodo }] = useUpdateTodoMutation();
|
||||
const [deleteTodo, { isLoading: isDeletingTodo }] = useDeleteTodoMutation();
|
||||
|
||||
const [menuVisible, setMenuVisible] = useState(false);
|
||||
const [menuAnchor, setMenuAnchor] = useState<{ x: number; y: number }>({
|
||||
x: 0,
|
||||
y: 0,
|
||||
});
|
||||
const [addTodoModalVisible, setAddTodoModalVisible] = useState(false);
|
||||
const [editTodoModalVisible, setEditTodoModalVisible] = useState(false);
|
||||
const [deleteTodoModalVisible, setDeleteTodoModalVisible] = useState(false);
|
||||
const [currentTodo, setCurrentTodo] = useState<Todo | null>(null);
|
||||
const [snackbarVisible, setSnackbarVisible] = useState(false);
|
||||
const [snackbarMessage, setSnackbarMessage] = useState("");
|
||||
|
||||
const showSnackbar = (message: string) => {
|
||||
setSnackbarMessage(message);
|
||||
setSnackbarVisible(true);
|
||||
};
|
||||
|
||||
const handleCreateTodo = async (
|
||||
todo: Partial<Todo> & { assigneeIds?: string[]; userId?: string }
|
||||
) => {
|
||||
try {
|
||||
const unique = (arr: string[]) => Array.from(new Set(arr.filter(Boolean)));
|
||||
const payload: CreateTodoPayload = {
|
||||
title: todo.title || "",
|
||||
task: todo.task || "",
|
||||
status: todo.status || TodoStatus.NEW,
|
||||
priority: todo.priority || PriorityStatus.LOW_PRIORITY,
|
||||
dueDate: todo.dueDate || new Date(),
|
||||
createdById: user?.id || "",
|
||||
assigneeIds: unique([...(todo.assigneeIds || []), ...(todo.userId ? [todo.userId] : [])]),
|
||||
};
|
||||
|
||||
await createTodo(payload).unwrap();
|
||||
showSnackbar("Todo created successfully");
|
||||
setAddTodoModalVisible(false);
|
||||
refetch();
|
||||
} catch {
|
||||
showSnackbar("Error creating todo");
|
||||
}
|
||||
};
|
||||
|
||||
const handleUpdateTodo = async (
|
||||
todo: Partial<Todo> & { assigneeIds?: string[]; userId?: string }
|
||||
) => {
|
||||
try {
|
||||
const body: any = {
|
||||
...(todo.id ? { id: todo.id } : {}),
|
||||
...(todo.title ? { title: todo.title } : {}),
|
||||
...(todo.task ? { task: todo.task } : {}),
|
||||
...(todo.status ? { status: todo.status } : {}),
|
||||
...(todo.priority ? { priority: todo.priority } : {}),
|
||||
...(todo.dueDate ? { dueDate: todo.dueDate } : {}),
|
||||
};
|
||||
const combinedAssignees = Array.from(
|
||||
new Set([...(todo.assigneeIds || []), ...(todo.userId ? [todo.userId] : [])])
|
||||
);
|
||||
if (combinedAssignees.length) {
|
||||
body.assigneeIds = combinedAssignees;
|
||||
}
|
||||
await updateTodo(body).unwrap();
|
||||
showSnackbar("Todo updated successfully");
|
||||
setEditTodoModalVisible(false);
|
||||
refetch();
|
||||
} catch {
|
||||
showSnackbar("Error updating todo");
|
||||
}
|
||||
};
|
||||
|
||||
const handleDeleteTodo = async (todo: Todo) => {
|
||||
try {
|
||||
await deleteTodo(todo.id).unwrap();
|
||||
refetch();
|
||||
showSnackbar(`${todo.title} has been deleted successfully`);
|
||||
setDeleteTodoModalVisible(false);
|
||||
} catch {
|
||||
showSnackbar("Error deleting todo");
|
||||
}
|
||||
};
|
||||
|
||||
const handleSort = (column: string) => {
|
||||
setSortColumn(column);
|
||||
setSortDirection(
|
||||
sortDirection === TableSortDirection.ASCENDING
|
||||
? TableSortDirection.DESCENDING
|
||||
: TableSortDirection.ASCENDING
|
||||
);
|
||||
setPage(0);
|
||||
};
|
||||
|
||||
const handleItemsPerPageChange = (value: number) => {
|
||||
setItemsPerPage(value);
|
||||
};
|
||||
|
||||
const handleSearch = (query: string) => {
|
||||
setSearchQuery(query);
|
||||
setPage(0);
|
||||
};
|
||||
|
||||
const handleEdit = (todo: Todo) => {
|
||||
setCurrentTodo(todo);
|
||||
setEditTodoModalVisible(true);
|
||||
};
|
||||
|
||||
const openDeleteModal = (todo: Todo) => {
|
||||
setCurrentTodo(todo);
|
||||
setDeleteTodoModalVisible(true);
|
||||
};
|
||||
|
||||
const isMobile = useAppSelector(selectIsMobile);
|
||||
|
||||
const from = todos.length ? page * itemsPerPage + 1 : 0;
|
||||
const to = todos.length ? Math.min(from + todos.length - 1, totalItems) : 0;
|
||||
|
||||
useEffect(() => {
|
||||
setPage(0);
|
||||
}, [itemsPerPage]);
|
||||
|
||||
return (
|
||||
<>
|
||||
<View className="flex-1">
|
||||
<Text
|
||||
className={`text-2xl font-bold ${
|
||||
isMobile ? "mb-1" : "mb-4"
|
||||
} text-white`}
|
||||
>
|
||||
Todos manager
|
||||
</Text>
|
||||
<View
|
||||
className={`${
|
||||
isMobile ? "p-1" : "p-5"
|
||||
} bg-lightGray rounded-lg flex-1`}
|
||||
>
|
||||
{isLoading ? (
|
||||
<View className="flex-1 justify-center items-center">
|
||||
<LoadingSpinner />
|
||||
<Text className="mt-2">Loading todos...</Text>
|
||||
</View>
|
||||
) : error ? (
|
||||
<NoData
|
||||
message="Error loading todos"
|
||||
onRetry={refetch}
|
||||
isRetrying={isFetching}
|
||||
/>
|
||||
) : (
|
||||
<View className="flex-1 flex flex-col">
|
||||
<AdminTodosTableControls
|
||||
searchQuery={searchQuery}
|
||||
menuVisible={menuVisible}
|
||||
menuAnchor={menuAnchor}
|
||||
selectedStatuses={selectedStatuses}
|
||||
selectedPriorities={selectedPriorities}
|
||||
onSearch={handleSearch}
|
||||
onMenuDismiss={() => setMenuVisible(false)}
|
||||
onItemsPerPageChange={handleItemsPerPageChange}
|
||||
onSelectStatuses={setSelectedStatuses}
|
||||
onSelectPriorities={setSelectedPriorities}
|
||||
onAddTodo={() => setAddTodoModalVisible(true)}
|
||||
itemsPerPage={itemsPerPage}
|
||||
/>
|
||||
|
||||
<TodoModal
|
||||
visible={addTodoModalVisible}
|
||||
onClose={() => setAddTodoModalVisible(false)}
|
||||
onSubmit={handleCreateTodo}
|
||||
isLoading={isCreatingTodo}
|
||||
mode={OperationMode.ADD}
|
||||
/>
|
||||
|
||||
<TodoModal
|
||||
visible={editTodoModalVisible}
|
||||
todo={currentTodo}
|
||||
onClose={() => setEditTodoModalVisible(false)}
|
||||
onSubmit={handleUpdateTodo}
|
||||
isLoading={isUpdatingTodo}
|
||||
mode={OperationMode.EDIT}
|
||||
/>
|
||||
|
||||
<DeleteTodoModal
|
||||
visible={deleteTodoModalVisible}
|
||||
todo={currentTodo}
|
||||
onDismiss={() => setDeleteTodoModalVisible(false)}
|
||||
onDelete={handleDeleteTodo}
|
||||
isLoading={isDeletingTodo}
|
||||
/>
|
||||
|
||||
<View className="flex-1 overflow-hidden">
|
||||
{searchQuery && todos.length === 0 ? (
|
||||
<View
|
||||
className={`flex-1 items-center justify-center ${
|
||||
isMobile ? "p-1" : "p-5"
|
||||
}`}
|
||||
>
|
||||
<Text className="text-lg text-gray-500">
|
||||
No todos match your search for "{searchQuery}"
|
||||
</Text>
|
||||
</View>
|
||||
) : searchQuery.length === 0 && todos.length === 0 ? (
|
||||
<View
|
||||
className={`flex-1 items-center justify-center ${
|
||||
isMobile ? "p-1" : "p-5"
|
||||
}`}
|
||||
>
|
||||
<Text className="text-lg text-gray-500">
|
||||
No todos found
|
||||
</Text>
|
||||
</View>
|
||||
) : isMobile ? (
|
||||
<View className="flex-1">
|
||||
<MobileAdminTodosTable
|
||||
todos={todos}
|
||||
onEdit={handleEdit}
|
||||
onDelete={openDeleteModal}
|
||||
/>
|
||||
</View>
|
||||
) : (
|
||||
<View className="flex-1" style={{ height: 400 }}>
|
||||
<ScrollView className="flex-1">
|
||||
<DesktopAdminTodosTable
|
||||
todos={todos}
|
||||
sortColumn={sortColumn}
|
||||
sortDirection={sortDirection}
|
||||
onSort={handleSort}
|
||||
onEdit={handleEdit}
|
||||
onDelete={openDeleteModal}
|
||||
/>
|
||||
</ScrollView>
|
||||
</View>
|
||||
)}
|
||||
</View>
|
||||
<TablePagination
|
||||
page={page}
|
||||
itemsPerPage={itemsPerPage}
|
||||
totalItems={totalItems}
|
||||
from={from}
|
||||
to={to}
|
||||
onPageChange={setPage}
|
||||
onItemsPerPageButtonPress={(event: GestureResponderEvent) => {
|
||||
const { pageX, pageY } = event.nativeEvent;
|
||||
setMenuAnchor({ x: pageX, y: pageY });
|
||||
setMenuVisible(true);
|
||||
}}
|
||||
/>
|
||||
</View>
|
||||
)}
|
||||
</View>
|
||||
</View>
|
||||
|
||||
<AppSnackbar
|
||||
visible={snackbarVisible}
|
||||
onDismiss={() => setSnackbarVisible(false)}
|
||||
action={{ label: "Close", onPress: () => setSnackbarVisible(false) }}
|
||||
>
|
||||
{snackbarMessage}
|
||||
</AppSnackbar>
|
||||
</>
|
||||
);
|
||||
};
|
||||
Reference in New Issue
Block a user