Refactor services

This commit is contained in:
Chris Veleris 2025-02-21 17:45:08 +02:00
parent 1ee59c0403
commit e5c0825985
21 changed files with 322 additions and 516 deletions

View file

@ -15,23 +15,11 @@ import { Project } from "./entities/Project";
import { Task } from "./entities/Task";
import { User } from "./entities/User";
import { useStore } from "./store/useStore";
import {
fetchNotes,
createNote,
updateNote,
fetchAreas,
createArea,
updateArea,
fetchTags,
createTag,
updateTag,
fetchProjects,
createProject,
updateProject,
fetchTasks,
createTask,
updateTask,
} from "./utils/apiService";
import { fetchNotes, createNote, updateNote } from "./utils/notesService";
import { fetchAreas, createArea, updateArea } from "./utils/areasService";
import { fetchTags, createTag, updateTag } from "./utils/tagsService";
import { fetchProjects, createProject, updateProject } from "./utils/projectsService";
import { fetchTasks, createTask, updateTask } from "./utils/tasksService";
interface LayoutProps {
currentUser: User;

View file

@ -8,7 +8,7 @@ import {
import ConfirmDialog from './Shared/ConfirmDialog';
import AreaModal from './Area/AreaModal';
import { useStore } from '../store/useStore';
import { fetchAreas, createArea, updateArea, deleteArea } from '../utils/apiService';
import { fetchAreas, createArea, updateArea, deleteArea } from '../utils/areasService';
import { Area } from '../entities/Area';
const Areas: React.FC = () => {

View file

@ -4,7 +4,7 @@ import { PencilSquareIcon, TrashIcon, TagIcon, DocumentTextIcon } from '@heroico
import ConfirmDialog from '../Shared/ConfirmDialog';
import NoteModal from './NoteModal';
import { Note } from '../../entities/Note';
import { fetchNotes, deleteNote as apiDeleteNote, updateNote as apiUpdateNote } from '../../utils/apiService';
import { fetchNotes, deleteNote as apiDeleteNote, updateNote as apiUpdateNote } from '../../utils/notesService';
const NoteDetails: React.FC = () => {
const { id } = useParams<{ id: string }>();

View file

@ -3,7 +3,7 @@ import { Note } from '../../entities/Note';
import { useToast } from '../Shared/ToastContext';
import TagInput from '../Tag/TagInput';
import { Tag } from '../../entities/Tag';
import { fetchTags } from '../../utils/apiService';
import { fetchTags } from '../../utils/tagsService';
interface NoteModalProps {
isOpen: boolean;

View file

@ -14,7 +14,7 @@ import {
createNote,
updateNote,
deleteNote as apiDeleteNote,
} from '../utils/apiService';
} from '../utils/notesService';
const Notes: React.FC = () => {
const [notes, setNotes] = useState<Note[]>([]);

View file

@ -13,7 +13,11 @@ import { useStore } from "../../store/useStore";
import NewTask from "../Task/NewTask";
import { Project } from "../../entities/Project";
import { PriorityType, Task } from "../../entities/Task";
import { fetchProjectById, createTask, updateTask, deleteTask, updateProject, deleteProject, fetchAreas } from "../../utils/apiService";
import { fetchProjectById, updateProject, deleteProject } from "../../utils/projectsService";
import { createTask, updateTask, deleteTask } from "../../utils/tasksService";
import { fetchAreas } from "../../utils/areasService";
type PriorityStyles = Record<PriorityType, string> & { default: string };

View file

@ -10,7 +10,7 @@ import Switch from "../Shared/Switch";
import { useStore } from "../../store/useStore";
import {
fetchTags,
} from '../../utils/apiService';
} from '../../utils/tagsService';
interface ProjectModalProps {
isOpen: boolean;

View file

@ -8,7 +8,9 @@ import {
import ConfirmDialog from "./Shared/ConfirmDialog";
import ProjectModal from "./Project/ProjectModal";
import { useStore } from "../store/useStore";
import { fetchProjects, createProject, updateProject, deleteProject, fetchAreas } from "../utils/apiService";
import { fetchProjects, createProject, updateProject, deleteProject } from "../utils/projectsService";
import { fetchAreas } from "../utils/areasService";
import { Project } from "../entities/Project";
import { PriorityType, StatusType } from "../entities/Task";
import { useSearchParams } from "react-router-dom";

View file

@ -4,7 +4,7 @@ import { PencilSquareIcon, TrashIcon, TagIcon, MagnifyingGlassIcon } from '@hero
import ConfirmDialog from './Shared/ConfirmDialog';
import TagModal from './Tag/TagModal';
import { Tag } from '../entities/Tag';
import { fetchTags, createTag, updateTag, deleteTag as apiDeleteTag } from '../utils/apiService';
import { fetchTags, createTag, updateTag, deleteTag as apiDeleteTag } from '../utils/tagsService';
const Tags: React.FC = () => {
const [tags, setTags] = useState<Tag[]>([]);

View file

@ -8,7 +8,7 @@ import { useToast } from "../Shared/ToastContext";
import TagInput from "../Tag/TagInput";
import { Project } from "../../entities/Project";
import { useStore } from "../../store/useStore";
import { fetchTags } from '../../utils/apiService';
import { fetchTags } from '../../utils/tagsService';
interface TaskModalProps {
isOpen: boolean;

View file

@ -6,7 +6,8 @@ import {
CalendarDaysIcon,
ClockIcon,
} from "@heroicons/react/24/outline";
import { fetchTasks, updateTask, deleteTask, fetchProjects } from "../../utils/apiService";
import { fetchTasks, updateTask, deleteTask } from "../../utils/tasksService";
import { fetchProjects } from "../../utils/projectsService";
import { Task } from "../../entities/Task";
import { useStore } from "../../store/useStore";
import TaskList from "./TaskList";

View file

@ -1,53 +0,0 @@
import { useState, useEffect } from 'react';
interface UseFetchResult<T> {
data: T | null;
loading: boolean;
error: string | null;
}
const useFetch = <T,>(url: string, options?: RequestInit): UseFetchResult<T> => {
const [data, setData] = useState<T | null>(null);
const [loading, setLoading] = useState<boolean>(true);
const [error, setError] = useState<string | null>(null);
useEffect(() => {
let isMounted = true;
const controller = new AbortController();
const fetchData = async () => {
setLoading(true);
setError(null);
try {
const response = await fetch(url, { ...options, signal: controller.signal });
if (!response.ok) {
const errorData = await response.json();
throw new Error(errorData.error || 'Failed to fetch data.');
}
const result: T = await response.json();
if (isMounted) {
setData(result);
}
} catch (err: any) {
if (isMounted) {
if (err.name !== 'AbortError') {
setError(err.message);
}
}
} finally {
if (isMounted) setLoading(false);
}
};
fetchData();
return () => {
isMounted = false;
controller.abort();
};
}, [url, JSON.stringify(options)]);
return { data, loading, error };
};
export default useFetch;

View file

@ -1,69 +0,0 @@
import useSWR from 'swr';
import { Task } from '../entities/Task';
interface UseFetchTasksOptions {
type?: string;
tag?: string;
}
interface Metrics {
total_open_tasks: number;
tasks_pending_over_month: number;
tasks_in_progress_count: number;
tasks_in_progress: Task[];
tasks_due_today: Task[];
suggested_tasks: Task[];
}
interface UseFetchTasksResult {
tasks: Task[];
metrics: Metrics;
isLoading: boolean;
isError: boolean;
mutate: () => void;
}
const initialMetrics: Metrics = {
total_open_tasks: 0,
tasks_pending_over_month: 0,
tasks_in_progress_count: 0,
tasks_in_progress: [],
tasks_due_today: [],
suggested_tasks: [],
};
const fetcher = (url: string) =>
fetch(url, {
credentials: 'include',
headers: { Accept: 'application/json' },
}).then((res) => {
if (!res.ok) {
throw new Error('Failed to fetch tasks.');
}
return res.json();
});
const useFetchTasks = (options?: UseFetchTasksOptions): UseFetchTasksResult => {
const params = new URLSearchParams();
if (options?.type) {
params.append('type', options.type);
}
if (options?.tag) {
params.append('tag', options.tag);
}
const queryString = params.toString();
const url = `/api/tasks${queryString ? `?${queryString}` : ''}`;
const { data, error, mutate } = useSWR(url, fetcher);
return {
tasks: data?.tasks || [],
metrics: data?.metrics || initialMetrics,
isLoading: !error && !data,
isError: !!error,
mutate,
};
};
export default useFetchTasks;

View file

@ -1,91 +0,0 @@
import { useState } from 'react';
import { Task } from '../entities/Task';
const useManageTasks = () => {
const [tasks, setTasks] = useState<Task[]>([]);
const [isLoading, setIsLoading] = useState<boolean>(false);
const [isError, setIsError] = useState<boolean>(false);
const fetchTasks = async (query: string = '') => {
setIsLoading(true);
setIsError(false);
try {
const response = await fetch(`/api/tasks${query}`, {
credentials: 'include',
headers: { Accept: 'application/json' },
});
if (response.ok) {
const data = await response.json();
setTasks(data);
} else {
throw new Error('Failed to fetch tasks.');
}
} catch (error) {
setIsError(true);
} finally {
setIsLoading(false);
}
};
const createTask = async (taskData: Partial<Task>) => {
try {
const response = await fetch('/api/task', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
credentials: 'include',
body: JSON.stringify(taskData),
});
if (response.ok) {
const newTask = await response.json();
setTasks((prevTasks) => [newTask, ...prevTasks]);
} else {
throw new Error('Failed to create task.');
}
} catch (error) {
console.error('Error creating task:', error);
}
};
const updateTask = async (taskId: number, taskData: Partial<Task>) => {
try {
const response = await fetch(`/api/task/${taskId}`, {
method: 'PATCH',
headers: { 'Content-Type': 'application/json' },
credentials: 'include',
body: JSON.stringify(taskData),
});
if (response.ok) {
const updatedTask = await response.json();
setTasks((prevTasks) =>
prevTasks.map((task) => (task.id === taskId ? updatedTask : task))
);
} else {
throw new Error('Failed to update task.');
}
} catch (error) {
console.error('Error updating task:', error);
}
};
const deleteTask = async (taskId: number) => {
try {
const response = await fetch(`/api/task/${taskId}`, {
method: 'DELETE',
credentials: 'include',
});
if (response.ok) {
setTasks((prevTasks) => prevTasks.filter((task) => task.id !== taskId));
} else {
throw new Error('Failed to delete task.');
}
} catch (error) {
console.error('Error deleting task:', error);
}
};
const mutateTasks = fetchTasks;
return { tasks, isLoading, isError, fetchTasks, mutateTasks, createTask, updateTask, deleteTask };
};
export default useManageTasks;

View file

@ -1,255 +0,0 @@
import { Project } from "../entities/Project";
import { Area } from "../entities/Area";
import { Note } from "../entities/Note";
import { Task } from "../entities/Task";
import { Tag } from "../entities/Tag";
import { Metrics } from "../entities/Metrics";
/**
* Projects API
*/
export const fetchProjects = async (activeFilter = "all", areaFilter = ""): Promise<Project[]> => {
let url = `/api/projects`;
const params = new URLSearchParams();
if (activeFilter !== "all") params.append("active", activeFilter);
if (areaFilter) params.append("area_id", areaFilter);
if (params.toString()) url += `?${params.toString()}`;
const response = await fetch(url, {
credentials: 'include',
headers: { Accept: 'application/json' },
});
if (!response.ok) throw new Error('Failed to fetch projects.');
const data = await response.json();
return data.projects || data;
};
export const fetchProjectById = async (projectId: string): Promise<Project> => {
const response = await fetch(`/api/project/${projectId}`, {
credentials: 'include',
headers: { Accept: 'application/json' },
});
if (!response.ok) throw new Error('Failed to fetch project details.');
return await response.json();
};
export const createProject = async (projectData: Partial<Project>): Promise<Project> => {
const response = await fetch('/api/project', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(projectData),
});
if (!response.ok) throw new Error('Failed to create project.');
return await response.json();
};
export const updateProject = async (projectId: number, projectData: Partial<Project>): Promise<Project> => {
const response = await fetch(`/api/project/${projectId}`, {
method: 'PATCH',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(projectData),
});
if (!response.ok) throw new Error('Failed to update project.');
return await response.json();
};
export const deleteProject = async (projectId: number): Promise<void> => {
const response = await fetch(`/api/project/${projectId}`, {
method: 'DELETE',
});
if (!response.ok) throw new Error('Failed to delete project.');
};
/**
* Areas API
*/
export const fetchAreas = async (): Promise<Area[]> => {
const response = await fetch("/api/areas?active=true");
if (!response.ok) throw new Error('Failed to fetch areas.');
return await response.json();
};
export const createArea = async (areaData: Partial<Area>): Promise<Area> => {
const response = await fetch('/api/areas', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(areaData),
});
if (!response.ok) throw new Error('Failed to create area.');
return await response.json();
};
export const updateArea = async (areaId: number, areaData: Partial<Area>): Promise<Area> => {
const response = await fetch(`/api/areas/${areaId}`, {
method: 'PATCH',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(areaData),
});
if (!response.ok) throw new Error('Failed to update area.');
return await response.json();
};
export const deleteArea = async (areaId: number): Promise<void> => {
const response = await fetch(`/api/areas/${areaId}`, {
method: 'DELETE',
});
if (!response.ok) throw new Error('Failed to delete area.');
};
/**
* Notes API
*/
export const fetchNotes = async (): Promise<Note[]> => {
const response = await fetch("/api/notes");
if (!response.ok) throw new Error('Failed to fetch notes.');
return await response.json();
};
export const createNote = async (noteData: Note): Promise<Note> => {
const response = await fetch('/api/notes', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(noteData),
});
if (!response.ok) throw new Error('Failed to create note.');
return await response.json();
};
export const updateNote = async (noteId: number, noteData: Note): Promise<Note> => {
const response = await fetch(`/api/notes/${noteId}`, {
method: 'PATCH',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(noteData),
});
if (!response.ok) throw new Error('Failed to update note.');
return await response.json();
};
export const deleteNote = async (noteId: number): Promise<void> => {
const response = await fetch(`/api/notes/${noteId}`, {
method: 'DELETE',
});
if (!response.ok) throw new Error('Failed to delete note.');
};
/**
* Tasks API
*/
export const fetchTasks = async (query = ''): Promise<{ tasks: Task[]; metrics: Metrics }> => {
const response = await fetch(`/api/tasks${query}`);
if (!response.ok) throw new Error('Failed to fetch tasks.');
const result = await response.json();
if (!Array.isArray(result.tasks)) {
throw new Error('Resulting tasks are not an array.');
}
if (!result.metrics) {
throw new Error('Metrics data is not included.');
}
return { tasks: result.tasks, metrics: result.metrics };
};
export const createTask = async (taskData: Task): Promise<Task> => {
const response = await fetch('/api/task', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(taskData),
});
if (!response.ok) throw new Error('Failed to create task.');
return await response.json();
};
export const updateTask = async (taskId: number, taskData: Task): Promise<Task> => {
const response = await fetch(`/api/task/${taskId}`, {
method: 'PATCH',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(taskData),
});
if (!response.ok) throw new Error('Failed to update task.');
return await response.json();
};
export const deleteTask = async (taskId: number): Promise<void> => {
const response = await fetch(`/api/task/${taskId}`, {
method: 'DELETE',
});
if (!response.ok) throw new Error('Failed to delete task.');
};
/**
* Tags API
*/
export const fetchTags = async (): Promise<Tag[]> => {
const response = await fetch("/api/tags");
if (!response.ok) throw new Error('Failed to fetch tags.');
return await response.json();
};
export const createTag = async (tagData: Tag): Promise<Tag> => {
const response = await fetch('/api/tag', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(tagData),
});
if (!response.ok) throw new Error('Failed to create tag.');
return await response.json();
};
export const updateTag = async (tagId: number, tagData: Tag): Promise<Tag> => {
const response = await fetch(`/api/tag/${tagId}`, {
method: 'PATCH',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(tagData),
});
if (!response.ok) throw new Error('Failed to update tag.');
return await response.json();
};
export const deleteTag = async (tagId: number): Promise<void> => {
const response = await fetch(`/api/tag/${tagId}`, {
method: 'DELETE',
});
if (!response.ok) throw new Error('Failed to delete tag.');
};

View file

@ -0,0 +1,40 @@
import { Area } from "../entities/Area";
export const fetchAreas = async (): Promise<Area[]> => {
const response = await fetch("/api/areas?active=true");
if (!response.ok) throw new Error('Failed to fetch areas.');
return await response.json();
};
export const createArea = async (areaData: Partial<Area>): Promise<Area> => {
const response = await fetch('/api/areas', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(areaData),
});
if (!response.ok) throw new Error('Failed to create area.');
return await response.json();
};
export const updateArea = async (areaId: number, areaData: Partial<Area>): Promise<Area> => {
const response = await fetch(`/api/areas/${areaId}`, {
method: 'PATCH',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(areaData),
});
if (!response.ok) throw new Error('Failed to update area.');
return await response.json();
};
export const deleteArea = async (areaId: number): Promise<void> => {
const response = await fetch(`/api/areas/${areaId}`, {
method: 'DELETE',
});
if (!response.ok) throw new Error('Failed to delete area.');
};

View file

@ -0,0 +1,40 @@
import { Note } from "../entities/Note";
export const fetchNotes = async (): Promise<Note[]> => {
const response = await fetch("/api/notes");
if (!response.ok) throw new Error('Failed to fetch notes.');
return await response.json();
};
export const createNote = async (noteData: Note): Promise<Note> => {
const response = await fetch('/api/notes', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(noteData),
});
if (!response.ok) throw new Error('Failed to create note.');
return await response.json();
};
export const updateNote = async (noteId: number, noteData: Note): Promise<Note> => {
const response = await fetch(`/api/notes/${noteId}`, {
method: 'PATCH',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(noteData),
});
if (!response.ok) throw new Error('Failed to update note.');
return await response.json();
};
export const deleteNote = async (noteId: number): Promise<void> => {
const response = await fetch(`/api/notes/${noteId}`, {
method: 'DELETE',
});
if (!response.ok) throw new Error('Failed to delete note.');
};

View file

@ -0,0 +1,63 @@
import { Project } from "../entities/Project";
export const fetchProjects = async (activeFilter = "all", areaFilter = ""): Promise<Project[]> => {
let url = `/api/projects`;
const params = new URLSearchParams();
if (activeFilter !== "all") params.append("active", activeFilter);
if (areaFilter) params.append("area_id", areaFilter);
if (params.toString()) url += `?${params.toString()}`;
const response = await fetch(url, {
credentials: 'include',
headers: { Accept: 'application/json' },
});
if (!response.ok) throw new Error('Failed to fetch projects.');
const data = await response.json();
return data.projects || data;
};
export const fetchProjectById = async (projectId: string): Promise<Project> => {
const response = await fetch(`/api/project/${projectId}`, {
credentials: 'include',
headers: { Accept: 'application/json' },
});
if (!response.ok) throw new Error('Failed to fetch project details.');
return await response.json();
};
export const createProject = async (projectData: Partial<Project>): Promise<Project> => {
const response = await fetch('/api/project', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(projectData),
});
if (!response.ok) throw new Error('Failed to create project.');
return await response.json();
};
export const updateProject = async (projectId: number, projectData: Partial<Project>): Promise<Project> => {
const response = await fetch(`/api/project/${projectId}`, {
method: 'PATCH',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(projectData),
});
if (!response.ok) throw new Error('Failed to update project.');
return await response.json();
};
export const deleteProject = async (projectId: number): Promise<void> => {
const response = await fetch(`/api/project/${projectId}`, {
method: 'DELETE',
});
if (!response.ok) throw new Error('Failed to delete project.');
};

View file

@ -0,0 +1,40 @@
import { Tag } from "../entities/Tag";
export const fetchTags = async (): Promise<Tag[]> => {
const response = await fetch("/api/tags");
if (!response.ok) throw new Error('Failed to fetch tags.');
return await response.json();
};
export const createTag = async (tagData: Tag): Promise<Tag> => {
const response = await fetch('/api/tag', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(tagData),
});
if (!response.ok) throw new Error('Failed to create tag.');
return await response.json();
};
export const updateTag = async (tagId: number, tagData: Tag): Promise<Tag> => {
const response = await fetch(`/api/tag/${tagId}`, {
method: 'PATCH',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(tagData),
});
if (!response.ok) throw new Error('Failed to update tag.');
return await response.json();
};
export const deleteTag = async (tagId: number): Promise<void> => {
const response = await fetch(`/api/tag/${tagId}`, {
method: 'DELETE',
});
if (!response.ok) throw new Error('Failed to delete tag.');
};

View file

@ -0,0 +1,52 @@
import { Metrics } from "../entities/Metrics";
import { Task } from "../entities/Task";
export const fetchTasks = async (query = ''): Promise<{ tasks: Task[]; metrics: Metrics }> => {
const response = await fetch(`/api/tasks${query}`);
if (!response.ok) throw new Error('Failed to fetch tasks.');
const result = await response.json();
if (!Array.isArray(result.tasks)) {
throw new Error('Resulting tasks are not an array.');
}
if (!result.metrics) {
throw new Error('Metrics data is not included.');
}
return { tasks: result.tasks, metrics: result.metrics };
};
export const createTask = async (taskData: Task): Promise<Task> => {
const response = await fetch('/api/task', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(taskData),
});
if (!response.ok) throw new Error('Failed to create task.');
return await response.json();
};
export const updateTask = async (taskId: number, taskData: Task): Promise<Task> => {
const response = await fetch(`/api/task/${taskId}`, {
method: 'PATCH',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(taskData),
});
if (!response.ok) throw new Error('Failed to update task.');
return await response.json();
};
export const deleteTask = async (taskId: number): Promise<void> => {
const response = await fetch(`/api/task/${taskId}`, {
method: 'DELETE',
});
if (!response.ok) throw new Error('Failed to delete task.');
};

File diff suppressed because one or more lines are too long