chore: estructura inicial del MVP
Backend (Express + MariaDB) y frontend (React + Vite, identidad Fluent/Windows) con el nucleo funcional: seguridad, organizacion, proyectos, actividades, subtareas, dependencias, dashboard, calendario, gantt, comentarios, adjuntos, notificaciones in-app y catalogos. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,8 @@
|
||||
import { api } from './api.js';
|
||||
|
||||
export const listActivitiesByProject = (projectId) => api.get(`/projects/${projectId}/activities`);
|
||||
export const createActivity = (projectId, data) => api.post(`/projects/${projectId}/activities`, data);
|
||||
export const getActivity = (id) => api.get(`/activities/${id}`);
|
||||
export const updateActivity = (id, data) => api.put(`/activities/${id}`, data);
|
||||
export const addDependency = (id, dependsOnActivityId) => api.post(`/activities/${id}/dependencies`, { dependsOnActivityId });
|
||||
export const removeDependency = (id, dependsOnId) => api.delete(`/activities/${id}/dependencies/${dependsOnId}`);
|
||||
@@ -0,0 +1,52 @@
|
||||
const API_URL = import.meta.env.VITE_API_URL || '/api';
|
||||
|
||||
let onUnauthorized = () => {};
|
||||
export function setUnauthorizedHandler(fn) {
|
||||
onUnauthorized = fn;
|
||||
}
|
||||
|
||||
function getToken() {
|
||||
return localStorage.getItem('plc_token');
|
||||
}
|
||||
|
||||
async function request(method, path, { body, isForm } = {}) {
|
||||
const headers = {};
|
||||
const token = getToken();
|
||||
if (token) headers.Authorization = `Bearer ${token}`;
|
||||
if (!isForm && body !== undefined) headers['Content-Type'] = 'application/json';
|
||||
|
||||
const res = await fetch(`${API_URL}${path}`, {
|
||||
method,
|
||||
headers,
|
||||
body: isForm ? body : body !== undefined ? JSON.stringify(body) : undefined,
|
||||
});
|
||||
|
||||
if (res.status === 401) {
|
||||
onUnauthorized();
|
||||
}
|
||||
|
||||
if (res.status === 204) return null;
|
||||
|
||||
const isJson = (res.headers.get('content-type') || '').includes('application/json');
|
||||
const payload = isJson ? await res.json() : null;
|
||||
|
||||
if (!res.ok) {
|
||||
const message = payload?.error || `Error ${res.status}`;
|
||||
throw new Error(message);
|
||||
}
|
||||
return payload?.data;
|
||||
}
|
||||
|
||||
export const api = {
|
||||
get: (path) => request('GET', path),
|
||||
post: (path, body) => request('POST', path, { body }),
|
||||
put: (path, body) => request('PUT', path, { body }),
|
||||
patch: (path, body) => request('PATCH', path, { body }),
|
||||
delete: (path) => request('DELETE', path),
|
||||
postForm: (path, formData) => request('POST', path, { body: formData, isForm: true }),
|
||||
setToken: (token) => {
|
||||
if (token) localStorage.setItem('plc_token', token);
|
||||
else localStorage.removeItem('plc_token');
|
||||
},
|
||||
downloadUrl: (path) => `${API_URL}${path}`,
|
||||
};
|
||||
@@ -0,0 +1,30 @@
|
||||
import { api } from './api.js';
|
||||
|
||||
export const listAttachments = (entityType, entityId) => api.get(`/attachments?entityType=${entityType}&entityId=${entityId}`);
|
||||
|
||||
export async function uploadAttachment(entityType, entityId, file) {
|
||||
const form = new FormData();
|
||||
form.append('entityType', entityType);
|
||||
form.append('entityId', entityId);
|
||||
form.append('file', file);
|
||||
return api.postForm('/attachments', form);
|
||||
}
|
||||
|
||||
export const deleteAttachment = (id) => api.delete(`/attachments/${id}`);
|
||||
|
||||
export async function downloadAttachment(id, filename) {
|
||||
const token = localStorage.getItem('plc_token');
|
||||
const res = await fetch(api.downloadUrl(`/attachments/${id}/download`), {
|
||||
headers: token ? { Authorization: `Bearer ${token}` } : {},
|
||||
});
|
||||
if (!res.ok) throw new Error('No se pudo descargar el archivo');
|
||||
const blob = await res.blob();
|
||||
const url = URL.createObjectURL(blob);
|
||||
const a = document.createElement('a');
|
||||
a.href = url;
|
||||
a.download = filename;
|
||||
document.body.appendChild(a);
|
||||
a.click();
|
||||
a.remove();
|
||||
URL.revokeObjectURL(url);
|
||||
}
|
||||
@@ -0,0 +1,6 @@
|
||||
import { api } from './api.js';
|
||||
|
||||
export const listCatalog = (key) => api.get(`/catalogs/${key}`);
|
||||
export const createCatalogItem = (key, data) => api.post(`/catalogs/${key}`, data);
|
||||
export const updateCatalogItem = (key, id, data) => api.put(`/catalogs/${key}/${id}`, data);
|
||||
export const deleteCatalogItem = (key, id) => api.delete(`/catalogs/${key}/${id}`);
|
||||
@@ -0,0 +1,4 @@
|
||||
import { api } from './api.js';
|
||||
|
||||
export const listComments = (entityType, entityId) => api.get(`/comments?entityType=${entityType}&entityId=${entityId}`);
|
||||
export const createComment = (entityType, entityId, message) => api.post('/comments', { entityType, entityId, message });
|
||||
@@ -0,0 +1,4 @@
|
||||
import { api } from './api.js';
|
||||
|
||||
export const getSummary = () => api.get('/dashboard/summary');
|
||||
export const getCalendar = (month) => api.get(`/dashboard/calendar?month=${month}`);
|
||||
@@ -0,0 +1,6 @@
|
||||
import { api } from './api.js';
|
||||
|
||||
export const listDepartments = () => api.get('/departments');
|
||||
export const createDepartment = (data) => api.post('/departments', data);
|
||||
export const updateDepartment = (id, data) => api.put(`/departments/${id}`, data);
|
||||
export const deleteDepartment = (id) => api.delete(`/departments/${id}`);
|
||||
@@ -0,0 +1,5 @@
|
||||
import { api } from './api.js';
|
||||
|
||||
export const listNotifications = () => api.get('/notifications');
|
||||
export const markNotificationRead = (id) => api.patch(`/notifications/${id}/read`);
|
||||
export const markAllNotificationsRead = () => api.patch('/notifications/read-all');
|
||||
@@ -0,0 +1,12 @@
|
||||
import { api } from './api.js';
|
||||
|
||||
export const listProjects = (filters = {}) => {
|
||||
const qs = new URLSearchParams(Object.entries(filters).filter(([, v]) => v)).toString();
|
||||
return api.get(`/projects${qs ? `?${qs}` : ''}`);
|
||||
};
|
||||
export const getProject = (id) => api.get(`/projects/${id}`);
|
||||
export const createProject = (data) => api.post('/projects', data);
|
||||
export const updateProject = (id, data) => api.put(`/projects/${id}`, data);
|
||||
export const addProjectMember = (id, userId, roleInProject) => api.post(`/projects/${id}/members`, { userId, roleInProject });
|
||||
export const removeProjectMember = (id, userId) => api.delete(`/projects/${id}/members/${userId}`);
|
||||
export const getGantt = (id) => api.get(`/projects/${id}/gantt`);
|
||||
@@ -0,0 +1,7 @@
|
||||
import { api } from './api.js';
|
||||
|
||||
export const listSubtasks = (activityId) => api.get(`/activities/${activityId}/subtasks`);
|
||||
export const createSubtask = (activityId, data) => api.post(`/activities/${activityId}/subtasks`, data);
|
||||
export const updateSubtask = (id, data) => api.put(`/subtasks/${id}`, data);
|
||||
export const setSubtaskDone = (id, isDone) => api.patch(`/subtasks/${id}/done`, { isDone });
|
||||
export const deleteSubtask = (id) => api.delete(`/subtasks/${id}`);
|
||||
@@ -0,0 +1,7 @@
|
||||
import { api } from './api.js';
|
||||
|
||||
export const listUsers = () => api.get('/users');
|
||||
export const listRoles = () => api.get('/users/roles');
|
||||
export const createUser = (data) => api.post('/users', data);
|
||||
export const updateUser = (id, data) => api.put(`/users/${id}`, data);
|
||||
export const setUserActive = (id, isActive) => api.patch(`/users/${id}/active`, { isActive });
|
||||
Reference in New Issue
Block a user