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
@@ -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 &quot;{searchQuery}&quot;
</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>
</>
);
};