expand sub tasks
This commit is contained in:
@@ -3,6 +3,7 @@
|
|||||||
Worklenz is a project management application built with React, TypeScript, and Ant Design. The project is bundled using [Vite](https://vitejs.dev/).
|
Worklenz is a project management application built with React, TypeScript, and Ant Design. The project is bundled using [Vite](https://vitejs.dev/).
|
||||||
|
|
||||||
## Table of Contents
|
## Table of Contents
|
||||||
|
|
||||||
- [Getting Started](#getting-started)
|
- [Getting Started](#getting-started)
|
||||||
- [Available Scripts](#available-scripts)
|
- [Available Scripts](#available-scripts)
|
||||||
- [Project Structure](#project-structure)
|
- [Project Structure](#project-structure)
|
||||||
|
|||||||
@@ -14,4 +14,4 @@
|
|||||||
/* Maintain hover state */
|
/* Maintain hover state */
|
||||||
.table-body-row:hover .sticky-column {
|
.table-body-row:hover .sticky-column {
|
||||||
background-color: var(--background-hover);
|
background-color: var(--background-hover);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -4,4 +4,4 @@
|
|||||||
|
|
||||||
// Set undefined values so the application falls back to build-time env vars
|
// Set undefined values so the application falls back to build-time env vars
|
||||||
window.VITE_API_URL = undefined;
|
window.VITE_API_URL = undefined;
|
||||||
window.VITE_SOCKET_URL = undefined;
|
window.VITE_SOCKET_URL = undefined;
|
||||||
|
|||||||
@@ -12,4 +12,4 @@
|
|||||||
"enterSubtaskName": "Shkruani emrin e nën-detyrës...",
|
"enterSubtaskName": "Shkruani emrin e nën-detyrës...",
|
||||||
"add": "Shto",
|
"add": "Shto",
|
||||||
"cancel": "Anulo"
|
"cancel": "Anulo"
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -26,4 +26,4 @@
|
|||||||
"add-sub-task": "+ Unteraufgabe hinzufügen",
|
"add-sub-task": "+ Unteraufgabe hinzufügen",
|
||||||
"refresh-sub-tasks": "Unteraufgaben aktualisieren"
|
"refresh-sub-tasks": "Unteraufgaben aktualisieren"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -12,4 +12,4 @@
|
|||||||
"enterSubtaskName": "Unteraufgabenname eingeben...",
|
"enterSubtaskName": "Unteraufgabenname eingeben...",
|
||||||
"add": "Hinzufügen",
|
"add": "Hinzufügen",
|
||||||
"cancel": "Abbrechen"
|
"cancel": "Abbrechen"
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -25,7 +25,7 @@
|
|||||||
"paymentMethod": "Payment Method",
|
"paymentMethod": "Payment Method",
|
||||||
"status": "Status",
|
"status": "Status",
|
||||||
"ltdUsers": "You can add up to {{ltd_users}} users.",
|
"ltdUsers": "You can add up to {{ltd_users}} users.",
|
||||||
|
|
||||||
"totalSeats": "Total seats",
|
"totalSeats": "Total seats",
|
||||||
"availableSeats": "Available seats",
|
"availableSeats": "Available seats",
|
||||||
"addMoreSeats": "Add more seats",
|
"addMoreSeats": "Add more seats",
|
||||||
@@ -103,11 +103,11 @@
|
|||||||
"perMonthPerUser": "per user/month",
|
"perMonthPerUser": "per user/month",
|
||||||
"viewInvoice": "View Invoice",
|
"viewInvoice": "View Invoice",
|
||||||
"switchToFreePlan": "Switch to Free Plan",
|
"switchToFreePlan": "Switch to Free Plan",
|
||||||
|
|
||||||
"expirestoday": "today",
|
"expirestoday": "today",
|
||||||
"expirestomorrow": "tomorrow",
|
"expirestomorrow": "tomorrow",
|
||||||
"expiredDaysAgo": "{{days}} days ago",
|
"expiredDaysAgo": "{{days}} days ago",
|
||||||
|
|
||||||
"continueWith": "Continue with {{plan}}",
|
"continueWith": "Continue with {{plan}}",
|
||||||
"changeToPlan": "Change to {{plan}}",
|
"changeToPlan": "Change to {{plan}}",
|
||||||
"creditPlan": "Credit Plan",
|
"creditPlan": "Credit Plan",
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
{
|
{
|
||||||
"configurePhases": "Configure Phases",
|
"configurePhases": "Configure Phases",
|
||||||
"phaseLabel": "Phase Label",
|
"phaseLabel": "Phase Label",
|
||||||
"enterPhaseName": "Enter a name for phase label",
|
"enterPhaseName": "Enter a name for phase label",
|
||||||
"addOption": "Add Option",
|
"addOption": "Add Option",
|
||||||
"phaseOptions": "Phase Options:"
|
"phaseOptions": "Phase Options:"
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,11 +1,11 @@
|
|||||||
{
|
{
|
||||||
"importTaskTemplate": "Import Task Template",
|
"importTaskTemplate": "Import Task Template",
|
||||||
"templateName": "Template Name",
|
"templateName": "Template Name",
|
||||||
"templateDescription": "Template Description",
|
"templateDescription": "Template Description",
|
||||||
"selectedTasks": "Selected Tasks",
|
"selectedTasks": "Selected Tasks",
|
||||||
"tasks": "Tasks",
|
"tasks": "Tasks",
|
||||||
"templates": "Templates",
|
"templates": "Templates",
|
||||||
"remove": "Remove",
|
"remove": "Remove",
|
||||||
"cancel": "Cancel",
|
"cancel": "Cancel",
|
||||||
"import": "Import"
|
"import": "Import"
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,8 +1,7 @@
|
|||||||
{
|
{
|
||||||
"title": "Project Members",
|
"title": "Project Members",
|
||||||
"searchLabel": "Add members by adding their name or email",
|
"searchLabel": "Add members by adding their name or email",
|
||||||
"searchPlaceholder": "Type name or email",
|
"searchPlaceholder": "Type name or email",
|
||||||
"inviteAsAMember": "Invite as a member",
|
"inviteAsAMember": "Invite as a member",
|
||||||
"inviteNewMemberByEmail": "Invite new member by email"
|
"inviteNewMemberByEmail": "Invite new member by email"
|
||||||
|
}
|
||||||
}
|
|
||||||
|
|||||||
@@ -1,17 +1,17 @@
|
|||||||
{
|
{
|
||||||
"importTasks": "Import tasks",
|
"importTasks": "Import tasks",
|
||||||
"importTask": "Import task",
|
"importTask": "Import task",
|
||||||
"createTask": "Create task",
|
"createTask": "Create task",
|
||||||
"settings": "Settings",
|
"settings": "Settings",
|
||||||
"subscribe": "Subscribe",
|
"subscribe": "Subscribe",
|
||||||
"unsubscribe": "Unsubscribe",
|
"unsubscribe": "Unsubscribe",
|
||||||
"deleteProject": "Delete project",
|
"deleteProject": "Delete project",
|
||||||
"startDate": "Start date",
|
"startDate": "Start date",
|
||||||
"endDate": "End date",
|
"endDate": "End date",
|
||||||
"projectSettings": "Project settings",
|
"projectSettings": "Project settings",
|
||||||
"projectSummary": "Project summary",
|
"projectSummary": "Project summary",
|
||||||
"receiveProjectSummary": "Receive a project summary every evening.",
|
"receiveProjectSummary": "Receive a project summary every evening.",
|
||||||
"refreshProject": "Refresh project",
|
"refreshProject": "Refresh project",
|
||||||
"saveAsTemplate": "Save as template",
|
"saveAsTemplate": "Save as template",
|
||||||
"invite": "Invite"
|
"invite": "Invite"
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -2,4 +2,4 @@
|
|||||||
"title": "Appearance",
|
"title": "Appearance",
|
||||||
"darkMode": "Dark Mode",
|
"darkMode": "Dark Mode",
|
||||||
"darkModeDescription": "Switch between light and dark mode to customize your viewing experience."
|
"darkModeDescription": "Switch between light and dark mode to customize your viewing experience."
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -27,4 +27,4 @@
|
|||||||
"add-sub-task": "+ Add Sub Task",
|
"add-sub-task": "+ Add Sub Task",
|
||||||
"refresh-sub-tasks": "Refresh Sub Tasks"
|
"refresh-sub-tasks": "Refresh Sub Tasks"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -31,4 +31,4 @@
|
|||||||
"intervalWeeks": "Interval (weeks)",
|
"intervalWeeks": "Interval (weeks)",
|
||||||
"intervalMonths": "Interval (months)",
|
"intervalMonths": "Interval (months)",
|
||||||
"saveChanges": "Save Changes"
|
"saveChanges": "Save Changes"
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -47,7 +47,7 @@
|
|||||||
"searchInputPlaceholder": "Search or create",
|
"searchInputPlaceholder": "Search or create",
|
||||||
"assigneeSelectorInviteButton": "Invite a new member by email",
|
"assigneeSelectorInviteButton": "Invite a new member by email",
|
||||||
"labelInputPlaceholder": "Search or create",
|
"labelInputPlaceholder": "Search or create",
|
||||||
|
|
||||||
"pendingInvitation": "Pending Invitation",
|
"pendingInvitation": "Pending Invitation",
|
||||||
|
|
||||||
"contextMenu": {
|
"contextMenu": {
|
||||||
|
|||||||
@@ -12,4 +12,4 @@
|
|||||||
"enterSubtaskName": "Enter subtask name...",
|
"enterSubtaskName": "Enter subtask name...",
|
||||||
"add": "Add",
|
"add": "Add",
|
||||||
"cancel": "Cancel"
|
"cancel": "Cancel"
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,41 +1,41 @@
|
|||||||
{
|
{
|
||||||
"taskSelected": "task selected",
|
"taskSelected": "task selected",
|
||||||
"tasksSelected": "tasks selected",
|
"tasksSelected": "tasks selected",
|
||||||
"changeStatus": "Change Status/ Prioriy/ Phases",
|
"changeStatus": "Change Status/ Prioriy/ Phases",
|
||||||
"changeLabel": "Change Label",
|
"changeLabel": "Change Label",
|
||||||
"assignToMe": "Assign to me",
|
"assignToMe": "Assign to me",
|
||||||
"changeAssignees": "Change Assignees",
|
"changeAssignees": "Change Assignees",
|
||||||
"archive": "Archive",
|
"archive": "Archive",
|
||||||
"unarchive": "Unarchive",
|
"unarchive": "Unarchive",
|
||||||
"delete": "Delete",
|
"delete": "Delete",
|
||||||
"moreOptions": "More options",
|
"moreOptions": "More options",
|
||||||
"deselectAll": "Deselect all",
|
"deselectAll": "Deselect all",
|
||||||
"status": "Status",
|
"status": "Status",
|
||||||
"priority": "Priority",
|
"priority": "Priority",
|
||||||
"phase": "Phase",
|
"phase": "Phase",
|
||||||
"member": "Member",
|
"member": "Member",
|
||||||
"createTaskTemplate": "Create Task Template",
|
"createTaskTemplate": "Create Task Template",
|
||||||
"apply": "Apply",
|
"apply": "Apply",
|
||||||
"createLabel": "+ Create Label",
|
"createLabel": "+ Create Label",
|
||||||
"searchOrCreateLabel": "Search or create label...",
|
"searchOrCreateLabel": "Search or create label...",
|
||||||
"hitEnterToCreate": "Press Enter to create",
|
"hitEnterToCreate": "Press Enter to create",
|
||||||
"labelExists": "Label already exists",
|
"labelExists": "Label already exists",
|
||||||
"pendingInvitation": "Pending Invitation",
|
"pendingInvitation": "Pending Invitation",
|
||||||
"noMatchingLabels": "No matching labels",
|
"noMatchingLabels": "No matching labels",
|
||||||
"noLabels": "No labels",
|
"noLabels": "No labels",
|
||||||
"CHANGE_STATUS": "Change Status",
|
"CHANGE_STATUS": "Change Status",
|
||||||
"CHANGE_PRIORITY": "Change Priority",
|
"CHANGE_PRIORITY": "Change Priority",
|
||||||
"CHANGE_PHASE": "Change Phase",
|
"CHANGE_PHASE": "Change Phase",
|
||||||
"ADD_LABELS": "Add Labels",
|
"ADD_LABELS": "Add Labels",
|
||||||
"ASSIGN_TO_ME": "Assign to Me",
|
"ASSIGN_TO_ME": "Assign to Me",
|
||||||
"ASSIGN_MEMBERS": "Assign Members",
|
"ASSIGN_MEMBERS": "Assign Members",
|
||||||
"ARCHIVE": "Archive",
|
"ARCHIVE": "Archive",
|
||||||
"DELETE": "Delete",
|
"DELETE": "Delete",
|
||||||
"CANCEL": "Cancel",
|
"CANCEL": "Cancel",
|
||||||
"CLEAR_SELECTION": "Clear Selection",
|
"CLEAR_SELECTION": "Clear Selection",
|
||||||
"TASKS_SELECTED": "{{count}} task selected",
|
"TASKS_SELECTED": "{{count}} task selected",
|
||||||
"TASKS_SELECTED_plural": "{{count}} tasks selected",
|
"TASKS_SELECTED_plural": "{{count}} tasks selected",
|
||||||
"DELETE_TASKS_CONFIRM": "Delete {{count}} task?",
|
"DELETE_TASKS_CONFIRM": "Delete {{count}} task?",
|
||||||
"DELETE_TASKS_CONFIRM_plural": "Delete {{count}} tasks?",
|
"DELETE_TASKS_CONFIRM_plural": "Delete {{count}} tasks?",
|
||||||
"DELETE_TASKS_WARNING": "This action cannot be undone."
|
"DELETE_TASKS_WARNING": "This action cannot be undone."
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
{
|
{
|
||||||
"title": "Unauthorized!",
|
"title": "Unauthorized!",
|
||||||
"subtitle": "You are not authorized to access this page",
|
"subtitle": "You are not authorized to access this page",
|
||||||
"button": "Go to Home"
|
"button": "Go to Home"
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -24,7 +24,7 @@
|
|||||||
"paymentMethod": "Método de Pago",
|
"paymentMethod": "Método de Pago",
|
||||||
"status": "Estado",
|
"status": "Estado",
|
||||||
"ltdUsers": "Puedes agregar hasta {{ltd_users}} usuarios.",
|
"ltdUsers": "Puedes agregar hasta {{ltd_users}} usuarios.",
|
||||||
|
|
||||||
"drawerTitle": "Canjear Código",
|
"drawerTitle": "Canjear Código",
|
||||||
"label": "Canjear Código",
|
"label": "Canjear Código",
|
||||||
"drawerPlaceholder": "Ingrese su código de canje",
|
"drawerPlaceholder": "Ingrese su código de canje",
|
||||||
@@ -98,7 +98,7 @@
|
|||||||
"perMonthPerUser": "por usuario / mes",
|
"perMonthPerUser": "por usuario / mes",
|
||||||
"viewInvoice": "Ver Factura",
|
"viewInvoice": "Ver Factura",
|
||||||
"switchToFreePlan": "Cambiar a Plan Gratuito",
|
"switchToFreePlan": "Cambiar a Plan Gratuito",
|
||||||
|
|
||||||
"expirestoday": "hoy",
|
"expirestoday": "hoy",
|
||||||
"expirestomorrow": "mañana",
|
"expirestomorrow": "mañana",
|
||||||
"expiredDaysAgo": "hace {{days}} días",
|
"expiredDaysAgo": "hace {{days}} días",
|
||||||
|
|||||||
@@ -20,4 +20,4 @@
|
|||||||
|
|
||||||
"newTaskNamePlaceholder": "Escribe un nombre de tarea",
|
"newTaskNamePlaceholder": "Escribe un nombre de tarea",
|
||||||
"newSubtaskNamePlaceholder": "Escribe un nombre de subtarea"
|
"newSubtaskNamePlaceholder": "Escribe un nombre de subtarea"
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
{
|
{
|
||||||
"configurePhases": "Configurar fases",
|
"configurePhases": "Configurar fases",
|
||||||
"phaseLabel": "Etiqueta de fase",
|
"phaseLabel": "Etiqueta de fase",
|
||||||
"enterPhaseName": "Ingrese un nombre para la etiqueta de fase",
|
"enterPhaseName": "Ingrese un nombre para la etiqueta de fase",
|
||||||
"addOption": "Agregar opción",
|
"addOption": "Agregar opción",
|
||||||
"phaseOptions": "Opciones de fase:"
|
"phaseOptions": "Opciones de fase:"
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,11 +1,11 @@
|
|||||||
{
|
{
|
||||||
"importTaskTemplate": "Importar plantilla de tarea",
|
"importTaskTemplate": "Importar plantilla de tarea",
|
||||||
"templateName": "Nombre de la plantilla",
|
"templateName": "Nombre de la plantilla",
|
||||||
"templateDescription": "Descripción de la plantilla",
|
"templateDescription": "Descripción de la plantilla",
|
||||||
"selectedTasks": "Tareas seleccionadas",
|
"selectedTasks": "Tareas seleccionadas",
|
||||||
"tasks": "Tareas",
|
"tasks": "Tareas",
|
||||||
"templates": "Plantillas",
|
"templates": "Plantillas",
|
||||||
"remove": "Eliminar",
|
"remove": "Eliminar",
|
||||||
"cancel": "Cancelar",
|
"cancel": "Cancelar",
|
||||||
"import": "Importar"
|
"import": "Importar"
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,8 +1,7 @@
|
|||||||
{
|
{
|
||||||
"title": "Miembros del Proyecto",
|
"title": "Miembros del Proyecto",
|
||||||
"searchLabel": "Agregar miembros ingresando su nombre o correo electrónico",
|
"searchLabel": "Agregar miembros ingresando su nombre o correo electrónico",
|
||||||
"searchPlaceholder": "Escriba nombre o correo electrónico",
|
"searchPlaceholder": "Escriba nombre o correo electrónico",
|
||||||
"inviteAsAMember": "Invitar como miembro",
|
"inviteAsAMember": "Invitar como miembro",
|
||||||
"inviteNewMemberByEmail": "Invitar nuevo miembro por correo electrónico"
|
"inviteNewMemberByEmail": "Invitar nuevo miembro por correo electrónico"
|
||||||
|
}
|
||||||
}
|
|
||||||
|
|||||||
@@ -1,17 +1,17 @@
|
|||||||
{
|
{
|
||||||
"importTasks": "Importar tareas",
|
"importTasks": "Importar tareas",
|
||||||
"importTask": "Importar tarea",
|
"importTask": "Importar tarea",
|
||||||
"createTask": "Crear tarea",
|
"createTask": "Crear tarea",
|
||||||
"settings": "Ajustes",
|
"settings": "Ajustes",
|
||||||
"subscribe": "Suscribirse",
|
"subscribe": "Suscribirse",
|
||||||
"unsubscribe": "Cancelar suscripción",
|
"unsubscribe": "Cancelar suscripción",
|
||||||
"deleteProject": "Eliminar proyecto",
|
"deleteProject": "Eliminar proyecto",
|
||||||
"startDate": "Fecha de inicio",
|
"startDate": "Fecha de inicio",
|
||||||
"endDate": "Fecha de finalización",
|
"endDate": "Fecha de finalización",
|
||||||
"projectSettings": "Ajustes del proyecto",
|
"projectSettings": "Ajustes del proyecto",
|
||||||
"projectSummary": "Resumen del proyecto",
|
"projectSummary": "Resumen del proyecto",
|
||||||
"receiveProjectSummary": "Recibir un resumen del proyecto todas las noches.",
|
"receiveProjectSummary": "Recibir un resumen del proyecto todas las noches.",
|
||||||
"refreshProject": "Actualizar proyecto",
|
"refreshProject": "Actualizar proyecto",
|
||||||
"saveAsTemplate": "Guardar como plantilla",
|
"saveAsTemplate": "Guardar como plantilla",
|
||||||
"invite": "Invitar"
|
"invite": "Invitar"
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -10,7 +10,7 @@
|
|||||||
"taskIncludes": "¿Qué se debe incluir en la plantilla de las tareas?",
|
"taskIncludes": "¿Qué se debe incluir en la plantilla de las tareas?",
|
||||||
"taskIncludesOptions": {
|
"taskIncludesOptions": {
|
||||||
"statuses": "Estados",
|
"statuses": "Estados",
|
||||||
"phases": "Fases",
|
"phases": "Fases",
|
||||||
"labels": "Etiquetas",
|
"labels": "Etiquetas",
|
||||||
"name": "Nombre",
|
"name": "Nombre",
|
||||||
"priority": "Prioridad",
|
"priority": "Prioridad",
|
||||||
|
|||||||
@@ -2,4 +2,4 @@
|
|||||||
"title": "Apariencia",
|
"title": "Apariencia",
|
||||||
"darkMode": "Modo Oscuro",
|
"darkMode": "Modo Oscuro",
|
||||||
"darkModeDescription": "Cambia entre el modo claro y oscuro para personalizar tu experiencia visual."
|
"darkModeDescription": "Cambia entre el modo claro y oscuro para personalizar tu experiencia visual."
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -27,4 +27,4 @@
|
|||||||
"add-sub-task": "+ Añadir subtarea",
|
"add-sub-task": "+ Añadir subtarea",
|
||||||
"refresh-sub-tasks": "Actualizar subtareas"
|
"refresh-sub-tasks": "Actualizar subtareas"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -31,4 +31,4 @@
|
|||||||
"intervalWeeks": "Intervalo (semanas)",
|
"intervalWeeks": "Intervalo (semanas)",
|
||||||
"intervalMonths": "Intervalo (meses)",
|
"intervalMonths": "Intervalo (meses)",
|
||||||
"saveChanges": "Guardar cambios"
|
"saveChanges": "Guardar cambios"
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -90,4 +90,4 @@
|
|||||||
"cancelMarkAsDone": "No, mantener estado actual",
|
"cancelMarkAsDone": "No, mantener estado actual",
|
||||||
"markAsDoneDescription": "Has establecido el progreso al 100%. ¿Quieres actualizar el estado de la tarea a \"Completada\"?"
|
"markAsDoneDescription": "Has establecido el progreso al 100%. ¿Quieres actualizar el estado de la tarea a \"Completada\"?"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -47,7 +47,7 @@
|
|||||||
"searchInputPlaceholder": "Buscar o crear",
|
"searchInputPlaceholder": "Buscar o crear",
|
||||||
"assigneeSelectorInviteButton": "Invitar a un nuevo miembro por correo",
|
"assigneeSelectorInviteButton": "Invitar a un nuevo miembro por correo",
|
||||||
"labelInputPlaceholder": "Buscar o crear",
|
"labelInputPlaceholder": "Buscar o crear",
|
||||||
|
|
||||||
"pendingInvitation": "Invitación pendiente",
|
"pendingInvitation": "Invitación pendiente",
|
||||||
|
|
||||||
"contextMenu": {
|
"contextMenu": {
|
||||||
|
|||||||
@@ -12,4 +12,4 @@
|
|||||||
"enterSubtaskName": "Ingresa el nombre de la subtarea...",
|
"enterSubtaskName": "Ingresa el nombre de la subtarea...",
|
||||||
"add": "Añadir",
|
"add": "Añadir",
|
||||||
"cancel": "Cancelar"
|
"cancel": "Cancelar"
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,41 +1,41 @@
|
|||||||
{
|
{
|
||||||
"taskSelected": "Tarea seleccionada",
|
"taskSelected": "Tarea seleccionada",
|
||||||
"tasksSelected": "Tareas seleccionadas",
|
"tasksSelected": "Tareas seleccionadas",
|
||||||
"changeStatus": "Cambiar estado/ prioridad/ fases",
|
"changeStatus": "Cambiar estado/ prioridad/ fases",
|
||||||
"changeLabel": "Cambiar etiqueta",
|
"changeLabel": "Cambiar etiqueta",
|
||||||
"assignToMe": "Asignar a mí",
|
"assignToMe": "Asignar a mí",
|
||||||
"changeAssignees": "Cambiar asignados",
|
"changeAssignees": "Cambiar asignados",
|
||||||
"archive": "Archivar",
|
"archive": "Archivar",
|
||||||
"unarchive": "Desarchivar",
|
"unarchive": "Desarchivar",
|
||||||
"delete": "Eliminar",
|
"delete": "Eliminar",
|
||||||
"moreOptions": "Más opciones",
|
"moreOptions": "Más opciones",
|
||||||
"deselectAll": "Deseleccionar todo",
|
"deselectAll": "Deseleccionar todo",
|
||||||
"status": "Estado",
|
"status": "Estado",
|
||||||
"priority": "Prioridad",
|
"priority": "Prioridad",
|
||||||
"phase": "Fase",
|
"phase": "Fase",
|
||||||
"member": "Miembro",
|
"member": "Miembro",
|
||||||
"createTaskTemplate": "Crear plantilla de tarea",
|
"createTaskTemplate": "Crear plantilla de tarea",
|
||||||
"apply": "Aplicar",
|
"apply": "Aplicar",
|
||||||
"createLabel": "+ Crear etiqueta",
|
"createLabel": "+ Crear etiqueta",
|
||||||
"searchOrCreateLabel": "Buscar o crear etiqueta...",
|
"searchOrCreateLabel": "Buscar o crear etiqueta...",
|
||||||
"hitEnterToCreate": "Presione Enter para crear",
|
"hitEnterToCreate": "Presione Enter para crear",
|
||||||
"labelExists": "La etiqueta ya existe",
|
"labelExists": "La etiqueta ya existe",
|
||||||
"pendingInvitation": "Invitación Pendiente",
|
"pendingInvitation": "Invitación Pendiente",
|
||||||
"noMatchingLabels": "No hay etiquetas coincidentes",
|
"noMatchingLabels": "No hay etiquetas coincidentes",
|
||||||
"noLabels": "Sin etiquetas",
|
"noLabels": "Sin etiquetas",
|
||||||
"CHANGE_STATUS": "Cambiar Estado",
|
"CHANGE_STATUS": "Cambiar Estado",
|
||||||
"CHANGE_PRIORITY": "Cambiar Prioridad",
|
"CHANGE_PRIORITY": "Cambiar Prioridad",
|
||||||
"CHANGE_PHASE": "Cambiar Fase",
|
"CHANGE_PHASE": "Cambiar Fase",
|
||||||
"ADD_LABELS": "Agregar Etiquetas",
|
"ADD_LABELS": "Agregar Etiquetas",
|
||||||
"ASSIGN_TO_ME": "Asignar a Mí",
|
"ASSIGN_TO_ME": "Asignar a Mí",
|
||||||
"ASSIGN_MEMBERS": "Asignar Miembros",
|
"ASSIGN_MEMBERS": "Asignar Miembros",
|
||||||
"ARCHIVE": "Archivar",
|
"ARCHIVE": "Archivar",
|
||||||
"DELETE": "Eliminar",
|
"DELETE": "Eliminar",
|
||||||
"CANCEL": "Cancelar",
|
"CANCEL": "Cancelar",
|
||||||
"CLEAR_SELECTION": "Limpiar Selección",
|
"CLEAR_SELECTION": "Limpiar Selección",
|
||||||
"TASKS_SELECTED": "{{count}} tarea seleccionada",
|
"TASKS_SELECTED": "{{count}} tarea seleccionada",
|
||||||
"TASKS_SELECTED_plural": "{{count}} tareas seleccionadas",
|
"TASKS_SELECTED_plural": "{{count}} tareas seleccionadas",
|
||||||
"DELETE_TASKS_CONFIRM": "¿Eliminar {{count}} tarea?",
|
"DELETE_TASKS_CONFIRM": "¿Eliminar {{count}} tarea?",
|
||||||
"DELETE_TASKS_CONFIRM_plural": "¿Eliminar {{count}} tareas?",
|
"DELETE_TASKS_CONFIRM_plural": "¿Eliminar {{count}} tareas?",
|
||||||
"DELETE_TASKS_WARNING": "Esta acción no se puede deshacer."
|
"DELETE_TASKS_WARNING": "Esta acción no se puede deshacer."
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
{
|
{
|
||||||
"title": "¡No autorizado!",
|
"title": "¡No autorizado!",
|
||||||
"subtitle": "No tienes permisos para acceder a esta página",
|
"subtitle": "No tienes permisos para acceder a esta página",
|
||||||
"button": "Ir a Inicio"
|
"button": "Ir a Inicio"
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -98,7 +98,7 @@
|
|||||||
"perMonthPerUser": "por usuário / mês",
|
"perMonthPerUser": "por usuário / mês",
|
||||||
"viewInvoice": "Ver Fatura",
|
"viewInvoice": "Ver Fatura",
|
||||||
"switchToFreePlan": "Mudar para Plano Gratuito",
|
"switchToFreePlan": "Mudar para Plano Gratuito",
|
||||||
|
|
||||||
"expirestoday": "hoje",
|
"expirestoday": "hoje",
|
||||||
"expirestomorrow": "amanhã",
|
"expirestomorrow": "amanhã",
|
||||||
"expiredDaysAgo": "há {{days}} dias",
|
"expiredDaysAgo": "há {{days}} dias",
|
||||||
|
|||||||
@@ -20,4 +20,4 @@
|
|||||||
|
|
||||||
"newTaskNamePlaceholder": "Escreva um nome de tarefa",
|
"newTaskNamePlaceholder": "Escreva um nome de tarefa",
|
||||||
"newSubtaskNamePlaceholder": "Escreva um nome de subtarefa"
|
"newSubtaskNamePlaceholder": "Escreva um nome de subtarefa"
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
{
|
{
|
||||||
"configurePhases": "Configurar fases",
|
"configurePhases": "Configurar fases",
|
||||||
"phaseLabel": "Etiqueta de fase",
|
"phaseLabel": "Etiqueta de fase",
|
||||||
"enterPhaseName": "Ingrese un nombre para la etiqueta de fase",
|
"enterPhaseName": "Ingrese un nombre para la etiqueta de fase",
|
||||||
"addOption": "Agregar opción",
|
"addOption": "Agregar opción",
|
||||||
"phaseOptions": "Opciones de fase:"
|
"phaseOptions": "Opciones de fase:"
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,11 +1,11 @@
|
|||||||
{
|
{
|
||||||
"importTaskTemplate": "Importar modelo de tarefa",
|
"importTaskTemplate": "Importar modelo de tarefa",
|
||||||
"templateName": "Nome do modelo",
|
"templateName": "Nome do modelo",
|
||||||
"templateDescription": "Descrição do modelo",
|
"templateDescription": "Descrição do modelo",
|
||||||
"selectedTasks": "Tarefas selecionadas",
|
"selectedTasks": "Tarefas selecionadas",
|
||||||
"tasks": "Tarefas",
|
"tasks": "Tarefas",
|
||||||
"templates": "Modelos",
|
"templates": "Modelos",
|
||||||
"remove": "Remover",
|
"remove": "Remover",
|
||||||
"cancel": "Cancelar",
|
"cancel": "Cancelar",
|
||||||
"import": "Importar"
|
"import": "Importar"
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,8 +1,7 @@
|
|||||||
{
|
{
|
||||||
"title": "Membros do Projeto",
|
"title": "Membros do Projeto",
|
||||||
"searchLabel": "Adicionar membros inserindo nome ou e-mail",
|
"searchLabel": "Adicionar membros inserindo nome ou e-mail",
|
||||||
"searchPlaceholder": "Digite nome ou e-mail",
|
"searchPlaceholder": "Digite nome ou e-mail",
|
||||||
"inviteAsAMember": "Convidar como membro",
|
"inviteAsAMember": "Convidar como membro",
|
||||||
"inviteNewMemberByEmail": "Convidar novo membro por e-mail"
|
"inviteNewMemberByEmail": "Convidar novo membro por e-mail"
|
||||||
|
}
|
||||||
}
|
|
||||||
|
|||||||
@@ -1,17 +1,17 @@
|
|||||||
{
|
{
|
||||||
"importTasks": "Importar tarefas",
|
"importTasks": "Importar tarefas",
|
||||||
"importTask": "Importar tarefa",
|
"importTask": "Importar tarefa",
|
||||||
"createTask": "Criar tarefa",
|
"createTask": "Criar tarefa",
|
||||||
"settings": "Configurações",
|
"settings": "Configurações",
|
||||||
"subscribe": "Inscrever-se",
|
"subscribe": "Inscrever-se",
|
||||||
"unsubscribe": "Cancelar inscrição",
|
"unsubscribe": "Cancelar inscrição",
|
||||||
"deleteProject": "Excluir projeto",
|
"deleteProject": "Excluir projeto",
|
||||||
"startDate": "Data de início",
|
"startDate": "Data de início",
|
||||||
"endDate": "Data de fim",
|
"endDate": "Data de fim",
|
||||||
"projectSettings": "Configurações do projeto",
|
"projectSettings": "Configurações do projeto",
|
||||||
"projectSummary": "Resumo do projeto",
|
"projectSummary": "Resumo do projeto",
|
||||||
"receiveProjectSummary": "Receber um resumo do projeto todas as noites.",
|
"receiveProjectSummary": "Receber um resumo do projeto todas as noites.",
|
||||||
"refreshProject": "Atualizar projeto",
|
"refreshProject": "Atualizar projeto",
|
||||||
"saveAsTemplate": "Salvar como modelo",
|
"saveAsTemplate": "Salvar como modelo",
|
||||||
"invite": "Convidar"
|
"invite": "Convidar"
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -11,7 +11,7 @@
|
|||||||
"taskIncludesOptions": {
|
"taskIncludesOptions": {
|
||||||
"statuses": "Status",
|
"statuses": "Status",
|
||||||
"phases": "Fases",
|
"phases": "Fases",
|
||||||
"labels": "Etiquetas",
|
"labels": "Etiquetas",
|
||||||
"name": "Nome",
|
"name": "Nome",
|
||||||
"priority": "Prioridade",
|
"priority": "Prioridade",
|
||||||
"status": "Status",
|
"status": "Status",
|
||||||
|
|||||||
@@ -2,4 +2,4 @@
|
|||||||
"title": "Aparência",
|
"title": "Aparência",
|
||||||
"darkMode": "Modo Escuro",
|
"darkMode": "Modo Escuro",
|
||||||
"darkModeDescription": "Alterne entre o modo claro e escuro para personalizar sua experiência de visualização."
|
"darkModeDescription": "Alterne entre o modo claro e escuro para personalizar sua experiência de visualização."
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -7,4 +7,4 @@
|
|||||||
"searchPlaceholder": "Pesquisar por nome",
|
"searchPlaceholder": "Pesquisar por nome",
|
||||||
"emptyText": "As categorias podem ser criadas ao atualizar ou criar projetos.",
|
"emptyText": "As categorias podem ser criadas ao atualizar ou criar projetos.",
|
||||||
"colorChangeTooltip": "Clique para mudar a cor"
|
"colorChangeTooltip": "Clique para mudar a cor"
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -19,4 +19,4 @@
|
|||||||
"createClientErrorMessage": "Criar cliente falhou!",
|
"createClientErrorMessage": "Criar cliente falhou!",
|
||||||
"updateClientSuccessMessage": "Atualizar cliente sucesso!",
|
"updateClientSuccessMessage": "Atualizar cliente sucesso!",
|
||||||
"updateClientErrorMessage": "Atualizar cliente falhou!"
|
"updateClientErrorMessage": "Atualizar cliente falhou!"
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -17,4 +17,4 @@
|
|||||||
"createJobTitleErrorMessage": "Falha ao criar título de emprego!",
|
"createJobTitleErrorMessage": "Falha ao criar título de emprego!",
|
||||||
"updateJobTitleSuccessMessage": "Atualizar título de emprego com sucesso!",
|
"updateJobTitleSuccessMessage": "Atualizar título de emprego com sucesso!",
|
||||||
"updateJobTitleErrorMessage": "Falha ao atualizar título de emprego!"
|
"updateJobTitleErrorMessage": "Falha ao atualizar título de emprego!"
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -8,4 +8,4 @@
|
|||||||
"emptyText": "Os rótulos podem ser criados ao atualizar ou criar tarefas.",
|
"emptyText": "Os rótulos podem ser criados ao atualizar ou criar tarefas.",
|
||||||
"pinTooltip": "Clique para fixar isso no menu principal",
|
"pinTooltip": "Clique para fixar isso no menu principal",
|
||||||
"colorChangeTooltip": "Clique para mudar a cor"
|
"colorChangeTooltip": "Clique para mudar a cor"
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -4,4 +4,4 @@
|
|||||||
"time_zone": "Fuso horário",
|
"time_zone": "Fuso horário",
|
||||||
"time_zone_required": "O fuso horário é obrigatório",
|
"time_zone_required": "O fuso horário é obrigatório",
|
||||||
"save_changes": "Salvar alterações"
|
"save_changes": "Salvar alterações"
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -7,4 +7,4 @@
|
|||||||
"popupDescription": "As notificações pop-up podem ser desativadas pelo seu navegador. Altere as configurações do seu navegador para permiti-las.",
|
"popupDescription": "As notificações pop-up podem ser desativadas pelo seu navegador. Altere as configurações do seu navegador para permiti-las.",
|
||||||
"unreadItemsTitle": "Mostrar o número de itens não lidos",
|
"unreadItemsTitle": "Mostrar o número de itens não lidos",
|
||||||
"unreadItemsDescription": "Você verá contagens para cada notificação."
|
"unreadItemsDescription": "Você verá contagens para cada notificação."
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -10,4 +10,4 @@
|
|||||||
"profileJoinedText": "Entrou há um mês",
|
"profileJoinedText": "Entrou há um mês",
|
||||||
"profileLastUpdatedText": "Última atualização há um mês",
|
"profileLastUpdatedText": "Última atualização há um mês",
|
||||||
"avatarTooltip": "Clique para carregar um avatar"
|
"avatarTooltip": "Clique para carregar um avatar"
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -5,4 +5,4 @@
|
|||||||
"confirmText": "Tem a certeza?",
|
"confirmText": "Tem a certeza?",
|
||||||
"okText": "Sim",
|
"okText": "Sim",
|
||||||
"cancelText": "Cancelar"
|
"cancelText": "Cancelar"
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -12,4 +12,4 @@
|
|||||||
"change-password": "Alterar Senha",
|
"change-password": "Alterar Senha",
|
||||||
"language-and-region": "Idioma e Região",
|
"language-and-region": "Idioma e Região",
|
||||||
"appearance": "Aparência"
|
"appearance": "Aparência"
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -6,4 +6,4 @@
|
|||||||
"confirmText": "Tem a certeza?",
|
"confirmText": "Tem a certeza?",
|
||||||
"okText": "Sim",
|
"okText": "Sim",
|
||||||
"cancelText": "Cancelar"
|
"cancelText": "Cancelar"
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -41,4 +41,4 @@
|
|||||||
"addedText": "Adicionado",
|
"addedText": "Adicionado",
|
||||||
"updatedText": "Atualizado",
|
"updatedText": "Atualizado",
|
||||||
"noResultFound": "Digite um endereço de email e pressione enter..."
|
"noResultFound": "Digite um endereço de email e pressione enter..."
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -27,4 +27,4 @@
|
|||||||
"add-sub-task": "+ Adicionar subtarefa",
|
"add-sub-task": "+ Adicionar subtarefa",
|
||||||
"refresh-sub-tasks": "Atualizar subtarefas"
|
"refresh-sub-tasks": "Atualizar subtarefas"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -31,4 +31,4 @@
|
|||||||
"intervalWeeks": "Intervalo (semanas)",
|
"intervalWeeks": "Intervalo (semanas)",
|
||||||
"intervalMonths": "Intervalo (meses)",
|
"intervalMonths": "Intervalo (meses)",
|
||||||
"saveChanges": "Salvar alterações"
|
"saveChanges": "Salvar alterações"
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -90,4 +90,4 @@
|
|||||||
"cancelMarkAsDone": "Não, manter status atual",
|
"cancelMarkAsDone": "Não, manter status atual",
|
||||||
"markAsDoneDescription": "Você definiu o progresso como 100%. Deseja atualizar o status da tarefa para \"Concluída\"?"
|
"markAsDoneDescription": "Você definiu o progresso como 100%. Deseja atualizar o status da tarefa para \"Concluída\"?"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -34,7 +34,7 @@
|
|||||||
"completeddateText": "Data de Conclusão",
|
"completeddateText": "Data de Conclusão",
|
||||||
"createddateText": "Data de Criação",
|
"createddateText": "Data de Criação",
|
||||||
"lastupdatedText": "Última Atualização",
|
"lastupdatedText": "Última Atualização",
|
||||||
|
|
||||||
"lowText": "Baixa",
|
"lowText": "Baixa",
|
||||||
"mediumText": "Média",
|
"mediumText": "Média",
|
||||||
"highText": "Alta",
|
"highText": "Alta",
|
||||||
|
|||||||
@@ -47,7 +47,7 @@
|
|||||||
"searchInputPlaceholder": "Buscar ou criar",
|
"searchInputPlaceholder": "Buscar ou criar",
|
||||||
"assigneeSelectorInviteButton": "Convide um novo membro por e-mail",
|
"assigneeSelectorInviteButton": "Convide um novo membro por e-mail",
|
||||||
"labelInputPlaceholder": "Buscar ou criar",
|
"labelInputPlaceholder": "Buscar ou criar",
|
||||||
|
|
||||||
"pendingInvitation": "Convite Pendente",
|
"pendingInvitation": "Convite Pendente",
|
||||||
|
|
||||||
"contextMenu": {
|
"contextMenu": {
|
||||||
|
|||||||
@@ -12,4 +12,4 @@
|
|||||||
"enterSubtaskName": "Digite o nome da subtarefa...",
|
"enterSubtaskName": "Digite o nome da subtarefa...",
|
||||||
"add": "Adicionar",
|
"add": "Adicionar",
|
||||||
"cancel": "Cancelar"
|
"cancel": "Cancelar"
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,41 +1,41 @@
|
|||||||
{
|
{
|
||||||
"taskSelected": "Tarefa selecionada",
|
"taskSelected": "Tarefa selecionada",
|
||||||
"tasksSelected": "Tarefas selecionadas",
|
"tasksSelected": "Tarefas selecionadas",
|
||||||
"changeStatus": "Alterar Status/ Prioridade/ Fases",
|
"changeStatus": "Alterar Status/ Prioridade/ Fases",
|
||||||
"changeLabel": "Alterar Etiqueta",
|
"changeLabel": "Alterar Etiqueta",
|
||||||
"assignToMe": "Atribuir a mim",
|
"assignToMe": "Atribuir a mim",
|
||||||
"changeAssignees": "Alterar Assignados",
|
"changeAssignees": "Alterar Assignados",
|
||||||
"archive": "Arquivar",
|
"archive": "Arquivar",
|
||||||
"unarchive": "Desarquivar",
|
"unarchive": "Desarquivar",
|
||||||
"delete": "Deletar",
|
"delete": "Deletar",
|
||||||
"moreOptions": "Mais opções",
|
"moreOptions": "Mais opções",
|
||||||
"deselectAll": "Desmarcar todas",
|
"deselectAll": "Desmarcar todas",
|
||||||
"status": "Status",
|
"status": "Status",
|
||||||
"priority": "Prioridade",
|
"priority": "Prioridade",
|
||||||
"phase": "Fase",
|
"phase": "Fase",
|
||||||
"member": "Membro",
|
"member": "Membro",
|
||||||
"createTaskTemplate": "Criar Modelo de Tarefa",
|
"createTaskTemplate": "Criar Modelo de Tarefa",
|
||||||
"apply": "Aplicar",
|
"apply": "Aplicar",
|
||||||
"createLabel": "+ Criar etiqueta",
|
"createLabel": "+ Criar etiqueta",
|
||||||
"searchOrCreateLabel": "Pesquisar ou criar etiqueta...",
|
"searchOrCreateLabel": "Pesquisar ou criar etiqueta...",
|
||||||
"hitEnterToCreate": "Pressione Enter para criar",
|
"hitEnterToCreate": "Pressione Enter para criar",
|
||||||
"labelExists": "A etiqueta já existe",
|
"labelExists": "A etiqueta já existe",
|
||||||
"pendingInvitation": "Convite Pendente",
|
"pendingInvitation": "Convite Pendente",
|
||||||
"noMatchingLabels": "Nenhuma etiqueta correspondente",
|
"noMatchingLabels": "Nenhuma etiqueta correspondente",
|
||||||
"noLabels": "Sem etiquetas",
|
"noLabels": "Sem etiquetas",
|
||||||
"CHANGE_STATUS": "Alterar Status",
|
"CHANGE_STATUS": "Alterar Status",
|
||||||
"CHANGE_PRIORITY": "Alterar Prioridade",
|
"CHANGE_PRIORITY": "Alterar Prioridade",
|
||||||
"CHANGE_PHASE": "Alterar Fase",
|
"CHANGE_PHASE": "Alterar Fase",
|
||||||
"ADD_LABELS": "Adicionar Etiquetas",
|
"ADD_LABELS": "Adicionar Etiquetas",
|
||||||
"ASSIGN_TO_ME": "Atribuir a Mim",
|
"ASSIGN_TO_ME": "Atribuir a Mim",
|
||||||
"ASSIGN_MEMBERS": "Atribuir Membros",
|
"ASSIGN_MEMBERS": "Atribuir Membros",
|
||||||
"ARCHIVE": "Arquivar",
|
"ARCHIVE": "Arquivar",
|
||||||
"DELETE": "Deletar",
|
"DELETE": "Deletar",
|
||||||
"CANCEL": "Cancelar",
|
"CANCEL": "Cancelar",
|
||||||
"CLEAR_SELECTION": "Limpar Seleção",
|
"CLEAR_SELECTION": "Limpar Seleção",
|
||||||
"TASKS_SELECTED": "{{count}} tarefa selecionada",
|
"TASKS_SELECTED": "{{count}} tarefa selecionada",
|
||||||
"TASKS_SELECTED_plural": "{{count}} tarefas selecionadas",
|
"TASKS_SELECTED_plural": "{{count}} tarefas selecionadas",
|
||||||
"DELETE_TASKS_CONFIRM": "Deletar {{count}} tarefa?",
|
"DELETE_TASKS_CONFIRM": "Deletar {{count}} tarefa?",
|
||||||
"DELETE_TASKS_CONFIRM_plural": "Deletar {{count}} tarefas?",
|
"DELETE_TASKS_CONFIRM_plural": "Deletar {{count}} tarefas?",
|
||||||
"DELETE_TASKS_WARNING": "Esta ação não pode ser desfeita."
|
"DELETE_TASKS_WARNING": "Esta ação não pode ser desfeita."
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
{
|
{
|
||||||
"title": "¡Não autorizado!",
|
"title": "¡Não autorizado!",
|
||||||
"subtitle": "Você não tem permissão para acessar esta página",
|
"subtitle": "Você não tem permissão para acessar esta página",
|
||||||
"button": "Ir para Início"
|
"button": "Ir para Início"
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,9 +1,9 @@
|
|||||||
if ('serviceWorker' in navigator) {
|
if ('serviceWorker' in navigator) {
|
||||||
// Check if we've already attempted to unregister in this session
|
// Check if we've already attempted to unregister in this session
|
||||||
if (!sessionStorage.getItem('swUnregisterAttempted')) {
|
if (!sessionStorage.getItem('swUnregisterAttempted')) {
|
||||||
navigator.serviceWorker.getRegistrations().then(function(registrations) {
|
navigator.serviceWorker.getRegistrations().then(function (registrations) {
|
||||||
const ngswWorker = registrations.find(reg => reg.active?.scriptURL.includes('ngsw-worker'));
|
const ngswWorker = registrations.find(reg => reg.active?.scriptURL.includes('ngsw-worker'));
|
||||||
|
|
||||||
if (ngswWorker) {
|
if (ngswWorker) {
|
||||||
// Mark that we've attempted to unregister
|
// Mark that we've attempted to unregister
|
||||||
sessionStorage.setItem('swUnregisterAttempted', 'true');
|
sessionStorage.setItem('swUnregisterAttempted', 'true');
|
||||||
@@ -14,10 +14,10 @@ if ('serviceWorker' in navigator) {
|
|||||||
});
|
});
|
||||||
} else {
|
} else {
|
||||||
// If no ngsw-worker is found, unregister any other service workers
|
// If no ngsw-worker is found, unregister any other service workers
|
||||||
for(let registration of registrations) {
|
for (let registration of registrations) {
|
||||||
registration.unregister();
|
registration.unregister();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -16,7 +16,7 @@ copyFolderRecursiveSync(sourceDir, path.join(__dirname, '..', 'public'));
|
|||||||
|
|
||||||
function copyFolderRecursiveSync(source, target) {
|
function copyFolderRecursiveSync(source, target) {
|
||||||
const targetFolder = path.join(target, path.basename(source));
|
const targetFolder = path.join(target, path.basename(source));
|
||||||
|
|
||||||
// Create target folder if it doesn't exist
|
// Create target folder if it doesn't exist
|
||||||
if (!fs.existsSync(targetFolder)) {
|
if (!fs.existsSync(targetFolder)) {
|
||||||
fs.mkdirSync(targetFolder);
|
fs.mkdirSync(targetFolder);
|
||||||
@@ -25,7 +25,7 @@ function copyFolderRecursiveSync(source, target) {
|
|||||||
// Copy files
|
// Copy files
|
||||||
if (fs.lstatSync(source).isDirectory()) {
|
if (fs.lstatSync(source).isDirectory()) {
|
||||||
const files = fs.readdirSync(source);
|
const files = fs.readdirSync(source);
|
||||||
files.forEach(function(file) {
|
files.forEach(function (file) {
|
||||||
const curSource = path.join(source, file);
|
const curSource = path.join(source, file);
|
||||||
if (fs.lstatSync(curSource).isDirectory()) {
|
if (fs.lstatSync(curSource).isDirectory()) {
|
||||||
copyFolderRecursiveSync(curSource, targetFolder);
|
copyFolderRecursiveSync(curSource, targetFolder);
|
||||||
@@ -36,4 +36,4 @@ function copyFolderRecursiveSync(source, target) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
console.log('TinyMCE files copied successfully!');
|
console.log('TinyMCE files copied successfully!');
|
||||||
|
|||||||
@@ -22,7 +22,7 @@ import { SuspenseFallback } from './components/suspense-fallback/suspense-fallba
|
|||||||
|
|
||||||
/**
|
/**
|
||||||
* Main App Component - Performance Optimized
|
* Main App Component - Performance Optimized
|
||||||
*
|
*
|
||||||
* Performance optimizations applied:
|
* Performance optimizations applied:
|
||||||
* 1. React.memo() - Prevents unnecessary re-renders
|
* 1. React.memo() - Prevents unnecessary re-renders
|
||||||
* 2. useMemo() - Memoizes expensive computations
|
* 2. useMemo() - Memoizes expensive computations
|
||||||
@@ -37,7 +37,7 @@ const App: React.FC = memo(() => {
|
|||||||
|
|
||||||
// Memoize mixpanel initialization to prevent re-initialization
|
// Memoize mixpanel initialization to prevent re-initialization
|
||||||
const mixpanelToken = useMemo(() => import.meta.env.VITE_MIXPANEL_TOKEN as string, []);
|
const mixpanelToken = useMemo(() => import.meta.env.VITE_MIXPANEL_TOKEN as string, []);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
initMixpanel(mixpanelToken);
|
initMixpanel(mixpanelToken);
|
||||||
}, [mixpanelToken]);
|
}, [mixpanelToken]);
|
||||||
@@ -60,12 +60,12 @@ const App: React.FC = memo(() => {
|
|||||||
// Initialize CSRF token and translations on app startup
|
// Initialize CSRF token and translations on app startup
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
let isMounted = true;
|
let isMounted = true;
|
||||||
|
|
||||||
const initializeApp = async () => {
|
const initializeApp = async () => {
|
||||||
try {
|
try {
|
||||||
// Initialize CSRF token
|
// Initialize CSRF token
|
||||||
await initializeCsrfToken();
|
await initializeCsrfToken();
|
||||||
|
|
||||||
// Preload essential translations
|
// Preload essential translations
|
||||||
await ensureTranslationsLoaded();
|
await ensureTranslationsLoaded();
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
@@ -85,11 +85,11 @@ const App: React.FC = memo(() => {
|
|||||||
return (
|
return (
|
||||||
<Suspense fallback={<SuspenseFallback />}>
|
<Suspense fallback={<SuspenseFallback />}>
|
||||||
<ThemeWrapper>
|
<ThemeWrapper>
|
||||||
<RouterProvider
|
<RouterProvider
|
||||||
router={router}
|
router={router}
|
||||||
future={{
|
future={{
|
||||||
v7_startTransition: true
|
v7_startTransition: true,
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
</ThemeWrapper>
|
</ThemeWrapper>
|
||||||
</Suspense>
|
</Suspense>
|
||||||
|
|||||||
@@ -112,7 +112,7 @@ export const adminCenterApiService = {
|
|||||||
|
|
||||||
async updateTeam(
|
async updateTeam(
|
||||||
team_id: string,
|
team_id: string,
|
||||||
body: {name: string, teamMembers: IOrganizationUser[]}
|
body: { name: string; teamMembers: IOrganizationUser[] }
|
||||||
): Promise<IServerResponse<IOrganization>> {
|
): Promise<IServerResponse<IOrganization>> {
|
||||||
const response = await apiClient.put<IServerResponse<IOrganization>>(
|
const response = await apiClient.put<IServerResponse<IOrganization>>(
|
||||||
`${rootUrl}/organization/team/${team_id}`,
|
`${rootUrl}/organization/team/${team_id}`,
|
||||||
@@ -152,7 +152,6 @@ export const adminCenterApiService = {
|
|||||||
return response.data;
|
return response.data;
|
||||||
},
|
},
|
||||||
|
|
||||||
|
|
||||||
// Billing - Configuration
|
// Billing - Configuration
|
||||||
async getCountries(): Promise<IServerResponse<IBillingConfigurationCountry[]>> {
|
async getCountries(): Promise<IServerResponse<IBillingConfigurationCountry[]>> {
|
||||||
const response = await apiClient.get<IServerResponse<IBillingConfigurationCountry[]>>(
|
const response = await apiClient.get<IServerResponse<IBillingConfigurationCountry[]>>(
|
||||||
@@ -168,7 +167,9 @@ export const adminCenterApiService = {
|
|||||||
return response.data;
|
return response.data;
|
||||||
},
|
},
|
||||||
|
|
||||||
async updateBillingConfiguration(body: IBillingConfiguration): Promise<IServerResponse<IBillingConfiguration>> {
|
async updateBillingConfiguration(
|
||||||
|
body: IBillingConfiguration
|
||||||
|
): Promise<IServerResponse<IBillingConfiguration>> {
|
||||||
const response = await apiClient.put<IServerResponse<IBillingConfiguration>>(
|
const response = await apiClient.put<IServerResponse<IBillingConfiguration>>(
|
||||||
`${rootUrl}/billing/configuration`,
|
`${rootUrl}/billing/configuration`,
|
||||||
body
|
body
|
||||||
@@ -178,42 +179,58 @@ export const adminCenterApiService = {
|
|||||||
|
|
||||||
// Billing - Current Bill
|
// Billing - Current Bill
|
||||||
async getCharges(): Promise<IServerResponse<IBillingChargesResponse>> {
|
async getCharges(): Promise<IServerResponse<IBillingChargesResponse>> {
|
||||||
const response = await apiClient.get<IServerResponse<IBillingChargesResponse>>(`${rootUrl}/billing/charges`);
|
const response = await apiClient.get<IServerResponse<IBillingChargesResponse>>(
|
||||||
|
`${rootUrl}/billing/charges`
|
||||||
|
);
|
||||||
return response.data;
|
return response.data;
|
||||||
},
|
},
|
||||||
|
|
||||||
async getTransactions(): Promise<IServerResponse<IBillingTransaction[]>> {
|
async getTransactions(): Promise<IServerResponse<IBillingTransaction[]>> {
|
||||||
const response = await apiClient.get<IServerResponse<IBillingTransaction[]>>(`${rootUrl}/billing/transactions`);
|
const response = await apiClient.get<IServerResponse<IBillingTransaction[]>>(
|
||||||
|
`${rootUrl}/billing/transactions`
|
||||||
|
);
|
||||||
return response.data;
|
return response.data;
|
||||||
},
|
},
|
||||||
|
|
||||||
async getBillingAccountInfo(): Promise<IServerResponse<IBillingAccountInfo>> {
|
async getBillingAccountInfo(): Promise<IServerResponse<IBillingAccountInfo>> {
|
||||||
const response = await apiClient.get<IServerResponse<IBillingAccountInfo>>(`${rootUrl}/billing/info`);
|
const response = await apiClient.get<IServerResponse<IBillingAccountInfo>>(
|
||||||
|
`${rootUrl}/billing/info`
|
||||||
|
);
|
||||||
return response.data;
|
return response.data;
|
||||||
},
|
},
|
||||||
|
|
||||||
async getFreePlanSettings(): Promise<IServerResponse<IFreePlanSettings>> {
|
async getFreePlanSettings(): Promise<IServerResponse<IFreePlanSettings>> {
|
||||||
const response = await apiClient.get<IServerResponse<IFreePlanSettings>>(`${rootUrl}/billing/free-plan`);
|
const response = await apiClient.get<IServerResponse<IFreePlanSettings>>(
|
||||||
|
`${rootUrl}/billing/free-plan`
|
||||||
|
);
|
||||||
return response.data;
|
return response.data;
|
||||||
},
|
},
|
||||||
|
|
||||||
async upgradePlan(plan: string): Promise<IServerResponse<IUpgradeSubscriptionPlanResponse>> {
|
async upgradePlan(plan: string): Promise<IServerResponse<IUpgradeSubscriptionPlanResponse>> {
|
||||||
const response = await apiClient.get<IServerResponse<IUpgradeSubscriptionPlanResponse>>(`${rootUrl}/billing/upgrade-plan${toQueryString({plan})}`);
|
const response = await apiClient.get<IServerResponse<IUpgradeSubscriptionPlanResponse>>(
|
||||||
|
`${rootUrl}/billing/upgrade-plan${toQueryString({ plan })}`
|
||||||
|
);
|
||||||
return response.data;
|
return response.data;
|
||||||
},
|
},
|
||||||
|
|
||||||
async changePlan(plan: string): Promise<IServerResponse<IUpgradeSubscriptionPlanResponse>> {
|
async changePlan(plan: string): Promise<IServerResponse<IUpgradeSubscriptionPlanResponse>> {
|
||||||
const response = await apiClient.get<IServerResponse<IUpgradeSubscriptionPlanResponse>>(`${rootUrl}/billing/change-plan${toQueryString({plan})}`);
|
const response = await apiClient.get<IServerResponse<IUpgradeSubscriptionPlanResponse>>(
|
||||||
|
`${rootUrl}/billing/change-plan${toQueryString({ plan })}`
|
||||||
|
);
|
||||||
return response.data;
|
return response.data;
|
||||||
},
|
},
|
||||||
|
|
||||||
async getPlans(): Promise<IServerResponse<IPricingPlans>> {
|
async getPlans(): Promise<IServerResponse<IPricingPlans>> {
|
||||||
const response = await apiClient.get<IServerResponse<IPricingPlans>>(`${rootUrl}/billing/plans`);
|
const response = await apiClient.get<IServerResponse<IPricingPlans>>(
|
||||||
|
`${rootUrl}/billing/plans`
|
||||||
|
);
|
||||||
return response.data;
|
return response.data;
|
||||||
},
|
},
|
||||||
|
|
||||||
async getStorageInfo(): Promise<IServerResponse<IStorageInfo>> {
|
async getStorageInfo(): Promise<IServerResponse<IStorageInfo>> {
|
||||||
const response = await apiClient.get<IServerResponse<IStorageInfo>>(`${rootUrl}/billing/storage`);
|
const response = await apiClient.get<IServerResponse<IStorageInfo>>(
|
||||||
|
`${rootUrl}/billing/storage`
|
||||||
|
);
|
||||||
return response.data;
|
return response.data;
|
||||||
},
|
},
|
||||||
|
|
||||||
@@ -225,7 +242,7 @@ export const adminCenterApiService = {
|
|||||||
async resumeSubscription(): Promise<IServerResponse<any>> {
|
async resumeSubscription(): Promise<IServerResponse<any>> {
|
||||||
const response = await apiClient.get<IServerResponse<any>>(`${rootUrl}/billing/resume-plan`);
|
const response = await apiClient.get<IServerResponse<any>>(`${rootUrl}/billing/resume-plan`);
|
||||||
return response.data;
|
return response.data;
|
||||||
},
|
},
|
||||||
|
|
||||||
async cancelSubscription(): Promise<IServerResponse<any>> {
|
async cancelSubscription(): Promise<IServerResponse<any>> {
|
||||||
const response = await apiClient.get<IServerResponse<any>>(`${rootUrl}/billing/cancel-plan`);
|
const response = await apiClient.get<IServerResponse<any>>(`${rootUrl}/billing/cancel-plan`);
|
||||||
@@ -233,26 +250,34 @@ export const adminCenterApiService = {
|
|||||||
},
|
},
|
||||||
|
|
||||||
async addMoreSeats(totalSeats: number): Promise<IServerResponse<any>> {
|
async addMoreSeats(totalSeats: number): Promise<IServerResponse<any>> {
|
||||||
const response = await apiClient.post<IServerResponse<any>>(`${rootUrl}/billing/purchase-more-seats`, {seatCount: totalSeats});
|
const response = await apiClient.post<IServerResponse<any>>(
|
||||||
|
`${rootUrl}/billing/purchase-more-seats`,
|
||||||
|
{ seatCount: totalSeats }
|
||||||
|
);
|
||||||
return response.data;
|
return response.data;
|
||||||
},
|
},
|
||||||
|
|
||||||
async redeemCode(code: string): Promise<IServerResponse<IUpgradeSubscriptionPlanResponse>> {
|
async redeemCode(code: string): Promise<IServerResponse<IUpgradeSubscriptionPlanResponse>> {
|
||||||
const response = await apiClient.post<IServerResponse<IUpgradeSubscriptionPlanResponse>>(`${rootUrl}/billing/redeem`, {
|
const response = await apiClient.post<IServerResponse<IUpgradeSubscriptionPlanResponse>>(
|
||||||
code,
|
`${rootUrl}/billing/redeem`,
|
||||||
});
|
{
|
||||||
|
code,
|
||||||
|
}
|
||||||
|
);
|
||||||
return response.data;
|
return response.data;
|
||||||
},
|
},
|
||||||
|
|
||||||
async getAccountStorage(): Promise<IServerResponse<IBillingAccountStorage>> {
|
async getAccountStorage(): Promise<IServerResponse<IBillingAccountStorage>> {
|
||||||
const response = await apiClient.get<IServerResponse<IBillingAccountStorage>>(`${rootUrl}/billing/account-storage`);
|
const response = await apiClient.get<IServerResponse<IBillingAccountStorage>>(
|
||||||
|
`${rootUrl}/billing/account-storage`
|
||||||
|
);
|
||||||
return response.data;
|
return response.data;
|
||||||
},
|
},
|
||||||
|
|
||||||
async switchToFreePlan(teamId: string): Promise<IServerResponse<any>> {
|
async switchToFreePlan(teamId: string): Promise<IServerResponse<any>> {
|
||||||
const response = await apiClient.get<IServerResponse<any>>(`${rootUrl}/billing/switch-to-free-plan/${teamId}`);
|
const response = await apiClient.get<IServerResponse<any>>(
|
||||||
|
`${rootUrl}/billing/switch-to-free-plan/${teamId}`
|
||||||
|
);
|
||||||
return response.data;
|
return response.data;
|
||||||
},
|
},
|
||||||
|
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@@ -6,7 +6,10 @@ import { IUpgradeSubscriptionPlanResponse } from '@/types/admin-center/admin-cen
|
|||||||
|
|
||||||
const rootUrl = `${API_BASE_URL}/billing`;
|
const rootUrl = `${API_BASE_URL}/billing`;
|
||||||
export const billingApiService = {
|
export const billingApiService = {
|
||||||
async upgradeToPaidPlan(plan: string, seatCount: number): Promise<IServerResponse<IUpgradeSubscriptionPlanResponse>> {
|
async upgradeToPaidPlan(
|
||||||
|
plan: string,
|
||||||
|
seatCount: number
|
||||||
|
): Promise<IServerResponse<IUpgradeSubscriptionPlanResponse>> {
|
||||||
const q = toQueryString({ plan, seatCount });
|
const q = toQueryString({ plan, seatCount });
|
||||||
const response = await apiClient.get<IServerResponse<any>>(
|
const response = await apiClient.get<IServerResponse<any>>(
|
||||||
`${rootUrl}/upgrade-to-paid-plan${q}`
|
`${rootUrl}/upgrade-to-paid-plan${q}`
|
||||||
@@ -14,7 +17,9 @@ export const billingApiService = {
|
|||||||
return response.data;
|
return response.data;
|
||||||
},
|
},
|
||||||
|
|
||||||
async purchaseMoreSeats(seatCount: number): Promise<IServerResponse<IUpgradeSubscriptionPlanResponse>> {
|
async purchaseMoreSeats(
|
||||||
|
seatCount: number
|
||||||
|
): Promise<IServerResponse<IUpgradeSubscriptionPlanResponse>> {
|
||||||
const response = await apiClient.post<IServerResponse<IUpgradeSubscriptionPlanResponse>>(
|
const response = await apiClient.post<IServerResponse<IUpgradeSubscriptionPlanResponse>>(
|
||||||
`${rootUrl}/purchase-more-seats`,
|
`${rootUrl}/purchase-more-seats`,
|
||||||
{ seatCount }
|
{ seatCount }
|
||||||
@@ -27,9 +32,5 @@ export const billingApiService = {
|
|||||||
`${rootUrl}/contact-us${toQueryString({ contactNo })}`
|
`${rootUrl}/contact-us${toQueryString({ contactNo })}`
|
||||||
);
|
);
|
||||||
return response.data;
|
return response.data;
|
||||||
}
|
},
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -16,16 +16,16 @@ export const refreshCsrfToken = async (): Promise<string | null> => {
|
|||||||
try {
|
try {
|
||||||
const tokenStart = performance.now();
|
const tokenStart = performance.now();
|
||||||
console.log('[CSRF] Starting CSRF token refresh...');
|
console.log('[CSRF] Starting CSRF token refresh...');
|
||||||
|
|
||||||
// Make a GET request to the server to get a fresh CSRF token with timeout
|
// Make a GET request to the server to get a fresh CSRF token with timeout
|
||||||
const response = await axios.get(`${config.apiUrl}/csrf-token`, {
|
const response = await axios.get(`${config.apiUrl}/csrf-token`, {
|
||||||
withCredentials: true,
|
withCredentials: true,
|
||||||
timeout: 10000 // 10 second timeout for CSRF token requests
|
timeout: 10000, // 10 second timeout for CSRF token requests
|
||||||
});
|
});
|
||||||
|
|
||||||
const tokenEnd = performance.now();
|
const tokenEnd = performance.now();
|
||||||
console.log(`[CSRF] CSRF token refresh completed in ${(tokenEnd - tokenStart).toFixed(2)}ms`);
|
console.log(`[CSRF] CSRF token refresh completed in ${(tokenEnd - tokenStart).toFixed(2)}ms`);
|
||||||
|
|
||||||
if (response.data && response.data.token) {
|
if (response.data && response.data.token) {
|
||||||
csrfToken = response.data.token;
|
csrfToken = response.data.token;
|
||||||
console.log('[CSRF] CSRF token successfully refreshed');
|
console.log('[CSRF] CSRF token successfully refreshed');
|
||||||
@@ -61,22 +61,22 @@ const apiClient = axios.create({
|
|||||||
apiClient.interceptors.request.use(
|
apiClient.interceptors.request.use(
|
||||||
async config => {
|
async config => {
|
||||||
const requestStart = performance.now();
|
const requestStart = performance.now();
|
||||||
|
|
||||||
// Ensure we have a CSRF token before making requests
|
// Ensure we have a CSRF token before making requests
|
||||||
if (!csrfToken) {
|
if (!csrfToken) {
|
||||||
const tokenStart = performance.now();
|
const tokenStart = performance.now();
|
||||||
await refreshCsrfToken();
|
await refreshCsrfToken();
|
||||||
const tokenEnd = performance.now();
|
const tokenEnd = performance.now();
|
||||||
}
|
}
|
||||||
|
|
||||||
if (csrfToken) {
|
if (csrfToken) {
|
||||||
config.headers['X-CSRF-Token'] = csrfToken;
|
config.headers['X-CSRF-Token'] = csrfToken;
|
||||||
} else {
|
} else {
|
||||||
console.warn('No CSRF token available after refresh attempt');
|
console.warn('No CSRF token available after refresh attempt');
|
||||||
}
|
}
|
||||||
|
|
||||||
const requestEnd = performance.now();
|
const requestEnd = performance.now();
|
||||||
|
|
||||||
return config;
|
return config;
|
||||||
},
|
},
|
||||||
error => Promise.reject(error)
|
error => Promise.reject(error)
|
||||||
@@ -114,14 +114,17 @@ apiClient.interceptors.response.use(
|
|||||||
const errorResponse = error.response;
|
const errorResponse = error.response;
|
||||||
|
|
||||||
// Handle CSRF token errors
|
// Handle CSRF token errors
|
||||||
if (errorResponse?.status === 403 &&
|
if (
|
||||||
(typeof errorResponse.data === 'object' &&
|
errorResponse?.status === 403 &&
|
||||||
errorResponse.data !== null &&
|
((typeof errorResponse.data === 'object' &&
|
||||||
'message' in errorResponse.data &&
|
errorResponse.data !== null &&
|
||||||
(errorResponse.data.message === 'invalid csrf token' || errorResponse.data.message === 'Invalid CSRF token') ||
|
'message' in errorResponse.data &&
|
||||||
(error as any).code === 'EBADCSRFTOKEN')) {
|
(errorResponse.data.message === 'invalid csrf token' ||
|
||||||
|
errorResponse.data.message === 'Invalid CSRF token')) ||
|
||||||
|
(error as any).code === 'EBADCSRFTOKEN')
|
||||||
|
) {
|
||||||
alertService.error('Security Error', 'Invalid security token. Refreshing your session...');
|
alertService.error('Security Error', 'Invalid security token. Refreshing your session...');
|
||||||
|
|
||||||
// Try to refresh the CSRF token and retry the request
|
// Try to refresh the CSRF token and retry the request
|
||||||
const newToken = await refreshCsrfToken();
|
const newToken = await refreshCsrfToken();
|
||||||
if (newToken && error.config) {
|
if (newToken && error.config) {
|
||||||
|
|||||||
@@ -1,25 +1,37 @@
|
|||||||
import { IServerResponse } from "@/types/common.types";
|
import { IServerResponse } from '@/types/common.types';
|
||||||
import { IProjectAttachmentsViewModel } from "@/types/tasks/task-attachment-view-model";
|
import { IProjectAttachmentsViewModel } from '@/types/tasks/task-attachment-view-model';
|
||||||
import apiClient from "../api-client";
|
import apiClient from '../api-client';
|
||||||
import { API_BASE_URL } from "@/shared/constants";
|
import { API_BASE_URL } from '@/shared/constants';
|
||||||
import { toQueryString } from "@/utils/toQueryString";
|
import { toQueryString } from '@/utils/toQueryString';
|
||||||
|
|
||||||
const rootUrl = `${API_BASE_URL}/attachments`;
|
const rootUrl = `${API_BASE_URL}/attachments`;
|
||||||
|
|
||||||
export const attachmentsApiService = {
|
export const attachmentsApiService = {
|
||||||
getTaskAttachments: async (taskId: string): Promise<IServerResponse<IProjectAttachmentsViewModel>> => {
|
getTaskAttachments: async (
|
||||||
const response = await apiClient.get<IServerResponse<IProjectAttachmentsViewModel>>(`${rootUrl}/tasks/${taskId}`);
|
taskId: string
|
||||||
|
): Promise<IServerResponse<IProjectAttachmentsViewModel>> => {
|
||||||
|
const response = await apiClient.get<IServerResponse<IProjectAttachmentsViewModel>>(
|
||||||
|
`${rootUrl}/tasks/${taskId}`
|
||||||
|
);
|
||||||
return response.data;
|
return response.data;
|
||||||
},
|
},
|
||||||
|
|
||||||
getProjectAttachments: async (projectId: string, index: number, size: number): Promise<IServerResponse<IProjectAttachmentsViewModel>> => {
|
getProjectAttachments: async (
|
||||||
|
projectId: string,
|
||||||
|
index: number,
|
||||||
|
size: number
|
||||||
|
): Promise<IServerResponse<IProjectAttachmentsViewModel>> => {
|
||||||
const q = toQueryString({ index, size });
|
const q = toQueryString({ index, size });
|
||||||
const response = await apiClient.get<IServerResponse<IProjectAttachmentsViewModel>>(`${rootUrl}/project/${projectId}${q}`);
|
const response = await apiClient.get<IServerResponse<IProjectAttachmentsViewModel>>(
|
||||||
|
`${rootUrl}/project/${projectId}${q}`
|
||||||
|
);
|
||||||
return response.data;
|
return response.data;
|
||||||
},
|
},
|
||||||
|
|
||||||
downloadAttachment: async (id: string, filename: string): Promise<IServerResponse<string>> => {
|
downloadAttachment: async (id: string, filename: string): Promise<IServerResponse<string>> => {
|
||||||
const response = await apiClient.get<IServerResponse<string>>(`${rootUrl}/download?id=${id}&file=${filename}`);
|
const response = await apiClient.get<IServerResponse<string>>(
|
||||||
|
`${rootUrl}/download?id=${id}&file=${filename}`
|
||||||
|
);
|
||||||
return response.data;
|
return response.data;
|
||||||
},
|
},
|
||||||
|
|
||||||
@@ -27,7 +39,4 @@ export const attachmentsApiService = {
|
|||||||
const response = await apiClient.delete<IServerResponse<string>>(`${rootUrl}/tasks/${id}`);
|
const response = await apiClient.delete<IServerResponse<string>>(`${rootUrl}/tasks/${id}`);
|
||||||
return response.data;
|
return response.data;
|
||||||
},
|
},
|
||||||
|
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -20,7 +20,7 @@ const api = createApi({
|
|||||||
if (!token) {
|
if (!token) {
|
||||||
token = await refreshCsrfToken();
|
token = await refreshCsrfToken();
|
||||||
}
|
}
|
||||||
|
|
||||||
if (token) {
|
if (token) {
|
||||||
headers.set('X-CSRF-Token', token);
|
headers.set('X-CSRF-Token', token);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -10,7 +10,7 @@ export const projectMembersApiService = {
|
|||||||
createProjectMember: async (
|
createProjectMember: async (
|
||||||
body: IProjectMemberViewModel
|
body: IProjectMemberViewModel
|
||||||
): Promise<IServerResponse<IProjectMemberViewModel>> => {
|
): Promise<IServerResponse<IProjectMemberViewModel>> => {
|
||||||
const q = toQueryString({current_project_id: body.project_id});
|
const q = toQueryString({ current_project_id: body.project_id });
|
||||||
|
|
||||||
const response = await apiClient.post<IServerResponse<IProjectMemberViewModel>>(
|
const response = await apiClient.post<IServerResponse<IProjectMemberViewModel>>(
|
||||||
`${rootUrl}${q}`,
|
`${rootUrl}${q}`,
|
||||||
|
|||||||
@@ -34,7 +34,9 @@ export const projectTemplatesApiService = {
|
|||||||
return response.data;
|
return response.data;
|
||||||
},
|
},
|
||||||
|
|
||||||
createCustomTemplate: async (body: { template_id: string }): Promise<IServerResponse<IProjectTemplate>> => {
|
createCustomTemplate: async (body: {
|
||||||
|
template_id: string;
|
||||||
|
}): Promise<IServerResponse<IProjectTemplate>> => {
|
||||||
const response = await apiClient.post(`${rootUrl}/custom-template`, body);
|
const response = await apiClient.post(`${rootUrl}/custom-template`, body);
|
||||||
return response.data;
|
return response.data;
|
||||||
},
|
},
|
||||||
@@ -44,15 +46,17 @@ export const projectTemplatesApiService = {
|
|||||||
return response.data;
|
return response.data;
|
||||||
},
|
},
|
||||||
|
|
||||||
createFromWorklenzTemplate: async (body: { template_id: string }): Promise<IServerResponse<IProjectTemplate>> => {
|
createFromWorklenzTemplate: async (body: {
|
||||||
|
template_id: string;
|
||||||
|
}): Promise<IServerResponse<IProjectTemplate>> => {
|
||||||
const response = await apiClient.post(`${rootUrl}/import-template`, body);
|
const response = await apiClient.post(`${rootUrl}/import-template`, body);
|
||||||
return response.data;
|
return response.data;
|
||||||
},
|
|
||||||
|
|
||||||
createFromCustomTemplate: async (body: { template_id: string }): Promise<IServerResponse<IProjectTemplate>> => {
|
|
||||||
const response = await apiClient.post(`${rootUrl}/import-custom-template`, body);
|
|
||||||
return response.data;
|
|
||||||
},
|
},
|
||||||
|
|
||||||
|
createFromCustomTemplate: async (body: {
|
||||||
|
template_id: string;
|
||||||
|
}): Promise<IServerResponse<IProjectTemplate>> => {
|
||||||
|
const response = await apiClient.post(`${rootUrl}/import-custom-template`, body);
|
||||||
|
return response.data;
|
||||||
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@@ -101,7 +101,9 @@ export const projectsApiService = {
|
|||||||
return response.data;
|
return response.data;
|
||||||
},
|
},
|
||||||
|
|
||||||
updateProject: async (payload: UpdateProjectPayload): Promise<IServerResponse<IProjectViewModel>> => {
|
updateProject: async (
|
||||||
|
payload: UpdateProjectPayload
|
||||||
|
): Promise<IServerResponse<IProjectViewModel>> => {
|
||||||
const { id, ...data } = payload;
|
const { id, ...data } = payload;
|
||||||
const q = toQueryString({ current_project_id: id });
|
const q = toQueryString({ current_project_id: id });
|
||||||
const url = `${API_BASE_URL}/projects/${id}${q}`;
|
const url = `${API_BASE_URL}/projects/${id}${q}`;
|
||||||
@@ -127,7 +129,10 @@ export const projectsApiService = {
|
|||||||
return response.data;
|
return response.data;
|
||||||
},
|
},
|
||||||
|
|
||||||
updateDefaultTab: async (body: { project_id: string; default_view: string }): Promise<IServerResponse<any>> => {
|
updateDefaultTab: async (body: {
|
||||||
|
project_id: string;
|
||||||
|
default_view: string;
|
||||||
|
}): Promise<IServerResponse<any>> => {
|
||||||
const url = `${rootUrl}/update-pinned-view`;
|
const url = `${rootUrl}/update-pinned-view`;
|
||||||
const response = await apiClient.put<IServerResponse<IProjectViewModel>>(`${url}`, body);
|
const response = await apiClient.put<IServerResponse<IProjectViewModel>>(`${url}`, body);
|
||||||
return response.data;
|
return response.data;
|
||||||
@@ -139,4 +144,3 @@ export const projectsApiService = {
|
|||||||
return response.data;
|
return response.data;
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@@ -20,7 +20,7 @@ export const projectsApi = createApi({
|
|||||||
if (!token) {
|
if (!token) {
|
||||||
token = await refreshCsrfToken();
|
token = await refreshCsrfToken();
|
||||||
}
|
}
|
||||||
|
|
||||||
if (token) {
|
if (token) {
|
||||||
headers.set('X-CSRF-Token', token);
|
headers.set('X-CSRF-Token', token);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,5 +1,10 @@
|
|||||||
import { IServerResponse } from '@/types/common.types';
|
import { IServerResponse } from '@/types/common.types';
|
||||||
import { IGetProjectsRequestBody, IRPTMembersViewModel, IRPTOverviewProjectMember, IRPTProjectsViewModel } from '@/types/reporting/reporting.types';
|
import {
|
||||||
|
IGetProjectsRequestBody,
|
||||||
|
IRPTMembersViewModel,
|
||||||
|
IRPTOverviewProjectMember,
|
||||||
|
IRPTProjectsViewModel,
|
||||||
|
} from '@/types/reporting/reporting.types';
|
||||||
import apiClient from '../api-client';
|
import apiClient from '../api-client';
|
||||||
import { API_BASE_URL } from '@/shared/constants';
|
import { API_BASE_URL } from '@/shared/constants';
|
||||||
import { toQueryString } from '@/utils/toQueryString';
|
import { toQueryString } from '@/utils/toQueryString';
|
||||||
@@ -7,9 +12,7 @@ import { toQueryString } from '@/utils/toQueryString';
|
|||||||
const rootUrl = `${API_BASE_URL}/reporting/members`;
|
const rootUrl = `${API_BASE_URL}/reporting/members`;
|
||||||
|
|
||||||
export const reportingMembersApiService = {
|
export const reportingMembersApiService = {
|
||||||
getMembers: async (
|
getMembers: async (body: any): Promise<IServerResponse<IRPTMembersViewModel>> => {
|
||||||
body: any
|
|
||||||
): Promise<IServerResponse<IRPTMembersViewModel>> => {
|
|
||||||
const q = toQueryString(body);
|
const q = toQueryString(body);
|
||||||
const url = `${rootUrl}${q}`;
|
const url = `${rootUrl}${q}`;
|
||||||
const response = await apiClient.get<IServerResponse<IRPTMembersViewModel>>(url);
|
const response = await apiClient.get<IServerResponse<IRPTMembersViewModel>>(url);
|
||||||
|
|||||||
@@ -1,5 +1,10 @@
|
|||||||
import { IServerResponse } from '@/types/common.types';
|
import { IServerResponse } from '@/types/common.types';
|
||||||
import { IGetProjectsRequestBody, IRPTOverviewProjectInfo, IRPTOverviewProjectMember, IRPTProjectsViewModel } from '@/types/reporting/reporting.types';
|
import {
|
||||||
|
IGetProjectsRequestBody,
|
||||||
|
IRPTOverviewProjectInfo,
|
||||||
|
IRPTOverviewProjectMember,
|
||||||
|
IRPTProjectsViewModel,
|
||||||
|
} from '@/types/reporting/reporting.types';
|
||||||
import apiClient from '../api-client';
|
import apiClient from '../api-client';
|
||||||
import { API_BASE_URL } from '@/shared/constants';
|
import { API_BASE_URL } from '@/shared/constants';
|
||||||
import { toQueryString } from '@/utils/toQueryString';
|
import { toQueryString } from '@/utils/toQueryString';
|
||||||
@@ -33,8 +38,11 @@ export const reportingProjectsApiService = {
|
|||||||
return response.data;
|
return response.data;
|
||||||
},
|
},
|
||||||
|
|
||||||
getTasks: async (projectId: string, groupBy: string): Promise<IServerResponse<ITaskListGroup[]>> => {
|
getTasks: async (
|
||||||
const q = toQueryString({group: groupBy})
|
projectId: string,
|
||||||
|
groupBy: string
|
||||||
|
): Promise<IServerResponse<ITaskListGroup[]>> => {
|
||||||
|
const q = toQueryString({ group: groupBy });
|
||||||
|
|
||||||
const url = `${API_BASE_URL}/reporting/overview/project/tasks/${projectId}${q}`;
|
const url = `${API_BASE_URL}/reporting/overview/project/tasks/${projectId}${q}`;
|
||||||
const response = await apiClient.get<IServerResponse<ITaskListGroup[]>>(url);
|
const response = await apiClient.get<IServerResponse<ITaskListGroup[]>>(url);
|
||||||
|
|||||||
@@ -3,12 +3,20 @@ import { toQueryString } from '@/utils/toQueryString';
|
|||||||
import apiClient from '../api-client';
|
import apiClient from '../api-client';
|
||||||
import { IServerResponse } from '@/types/common.types';
|
import { IServerResponse } from '@/types/common.types';
|
||||||
import { IAllocationViewModel } from '@/types/reporting/reporting-allocation.types';
|
import { IAllocationViewModel } from '@/types/reporting/reporting-allocation.types';
|
||||||
import { IProjectLogsBreakdown, IRPTTimeMember, IRPTTimeProject, ITimeLogBreakdownReq } from '@/types/reporting/reporting.types';
|
import {
|
||||||
|
IProjectLogsBreakdown,
|
||||||
|
IRPTTimeMember,
|
||||||
|
IRPTTimeProject,
|
||||||
|
ITimeLogBreakdownReq,
|
||||||
|
} from '@/types/reporting/reporting.types';
|
||||||
|
|
||||||
const rootUrl = `${API_BASE_URL}/reporting`;
|
const rootUrl = `${API_BASE_URL}/reporting`;
|
||||||
|
|
||||||
export const reportingTimesheetApiService = {
|
export const reportingTimesheetApiService = {
|
||||||
getTimeSheetData: async (body = {}, archived = false): Promise<IServerResponse<IAllocationViewModel>> => {
|
getTimeSheetData: async (
|
||||||
|
body = {},
|
||||||
|
archived = false
|
||||||
|
): Promise<IServerResponse<IAllocationViewModel>> => {
|
||||||
const q = toQueryString({ archived });
|
const q = toQueryString({ archived });
|
||||||
const response = await apiClient.post(`${rootUrl}/allocation/${q}`, body);
|
const response = await apiClient.post(`${rootUrl}/allocation/${q}`, body);
|
||||||
return response.data;
|
return response.data;
|
||||||
@@ -19,24 +27,35 @@ export const reportingTimesheetApiService = {
|
|||||||
return response.data;
|
return response.data;
|
||||||
},
|
},
|
||||||
|
|
||||||
getProjectTimeSheets: async (body = {}, archived = false): Promise<IServerResponse<IRPTTimeProject[]>> => {
|
getProjectTimeSheets: async (
|
||||||
|
body = {},
|
||||||
|
archived = false
|
||||||
|
): Promise<IServerResponse<IRPTTimeProject[]>> => {
|
||||||
const q = toQueryString({ archived });
|
const q = toQueryString({ archived });
|
||||||
const response = await apiClient.post(`${rootUrl}/time-reports/projects/${q}`, body);
|
const response = await apiClient.post(`${rootUrl}/time-reports/projects/${q}`, body);
|
||||||
return response.data;
|
return response.data;
|
||||||
},
|
},
|
||||||
|
|
||||||
getMemberTimeSheets: async (body = {}, archived = false): Promise<IServerResponse<IRPTTimeMember[]>> => {
|
getMemberTimeSheets: async (
|
||||||
|
body = {},
|
||||||
|
archived = false
|
||||||
|
): Promise<IServerResponse<IRPTTimeMember[]>> => {
|
||||||
const q = toQueryString({ archived });
|
const q = toQueryString({ archived });
|
||||||
const response = await apiClient.post(`${rootUrl}/time-reports/members/${q}`, body);
|
const response = await apiClient.post(`${rootUrl}/time-reports/members/${q}`, body);
|
||||||
return response.data;
|
return response.data;
|
||||||
},
|
},
|
||||||
|
|
||||||
getProjectTimeLogs: async (body: ITimeLogBreakdownReq): Promise<IServerResponse<IProjectLogsBreakdown[]>> => {
|
getProjectTimeLogs: async (
|
||||||
|
body: ITimeLogBreakdownReq
|
||||||
|
): Promise<IServerResponse<IProjectLogsBreakdown[]>> => {
|
||||||
const response = await apiClient.post(`${rootUrl}/project-timelogs`, body);
|
const response = await apiClient.post(`${rootUrl}/project-timelogs`, body);
|
||||||
return response.data;
|
return response.data;
|
||||||
},
|
},
|
||||||
|
|
||||||
getProjectEstimatedVsActual: async (body = {}, archived = false): Promise<IServerResponse<IRPTTimeProject[]>> => {
|
getProjectEstimatedVsActual: async (
|
||||||
|
body = {},
|
||||||
|
archived = false
|
||||||
|
): Promise<IServerResponse<IRPTTimeProject[]>> => {
|
||||||
const q = toQueryString({ archived });
|
const q = toQueryString({ archived });
|
||||||
const response = await apiClient.post(`${rootUrl}/time-reports/estimated-vs-actual${q}`, body);
|
const response = await apiClient.post(`${rootUrl}/time-reports/estimated-vs-actual${q}`, body);
|
||||||
return response.data;
|
return response.data;
|
||||||
|
|||||||
@@ -2,7 +2,13 @@ import { API_BASE_URL } from '@/shared/constants';
|
|||||||
import apiClient from '../api-client';
|
import apiClient from '../api-client';
|
||||||
import { IServerResponse } from '@/types/common.types';
|
import { IServerResponse } from '@/types/common.types';
|
||||||
import { ITeamMemberViewModel } from '@/types/teamMembers/teamMembersGetResponse.types';
|
import { ITeamMemberViewModel } from '@/types/teamMembers/teamMembersGetResponse.types';
|
||||||
import { DateList, Member, Project, ScheduleData, Settings } from '@/types/schedule/schedule-v2.types';
|
import {
|
||||||
|
DateList,
|
||||||
|
Member,
|
||||||
|
Project,
|
||||||
|
ScheduleData,
|
||||||
|
Settings,
|
||||||
|
} from '@/types/schedule/schedule-v2.types';
|
||||||
|
|
||||||
const rootUrl = `${API_BASE_URL}/schedule-gannt-v2`;
|
const rootUrl = `${API_BASE_URL}/schedule-gannt-v2`;
|
||||||
|
|
||||||
@@ -45,16 +51,18 @@ export const scheduleAPIService = {
|
|||||||
},
|
},
|
||||||
|
|
||||||
fetchMemberProjects: async ({ id }: { id: string }): Promise<IServerResponse<Project>> => {
|
fetchMemberProjects: async ({ id }: { id: string }): Promise<IServerResponse<Project>> => {
|
||||||
const response = await apiClient.get<IServerResponse<Project>>(`${rootUrl}/members/projects/${id}`);
|
const response = await apiClient.get<IServerResponse<Project>>(
|
||||||
|
`${rootUrl}/members/projects/${id}`
|
||||||
|
);
|
||||||
return response.data;
|
return response.data;
|
||||||
},
|
},
|
||||||
|
|
||||||
submitScheduleData: async ({
|
submitScheduleData: async ({
|
||||||
schedule
|
schedule,
|
||||||
}: {
|
}: {
|
||||||
schedule: ScheduleData
|
schedule: ScheduleData;
|
||||||
}): Promise<IServerResponse<any>> => {
|
}): Promise<IServerResponse<any>> => {
|
||||||
const response = await apiClient.post<IServerResponse<any>>(`${rootUrl}/schedule`, schedule);
|
const response = await apiClient.post<IServerResponse<any>>(`${rootUrl}/schedule`, schedule);
|
||||||
return response.data;
|
return response.data;
|
||||||
}
|
},
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -52,8 +52,11 @@ export const profileSettingsApiService = {
|
|||||||
return response.data;
|
return response.data;
|
||||||
},
|
},
|
||||||
|
|
||||||
updateTeamName: async (id: string, body: ITeam): Promise<IServerResponse<ITeam>> => {
|
updateTeamName: async (id: string, body: ITeam): Promise<IServerResponse<ITeam>> => {
|
||||||
const response = await apiClient.put<IServerResponse<ITeam>>(`${rootUrl}/team-name/${id}`, body);
|
const response = await apiClient.put<IServerResponse<ITeam>>(
|
||||||
|
`${rootUrl}/team-name/${id}`,
|
||||||
|
body
|
||||||
|
);
|
||||||
return response.data;
|
return response.data;
|
||||||
},
|
},
|
||||||
|
|
||||||
|
|||||||
@@ -21,12 +21,18 @@ export const taskTemplatesApiService = {
|
|||||||
const response = await apiClient.get<IServerResponse<ITaskTemplateGetResponse>>(`${url}`);
|
const response = await apiClient.get<IServerResponse<ITaskTemplateGetResponse>>(`${url}`);
|
||||||
return response.data;
|
return response.data;
|
||||||
},
|
},
|
||||||
createTemplate: async (body: { name: string, tasks: IProjectTask[] }): Promise<IServerResponse<ITask>> => {
|
createTemplate: async (body: {
|
||||||
|
name: string;
|
||||||
|
tasks: IProjectTask[];
|
||||||
|
}): Promise<IServerResponse<ITask>> => {
|
||||||
const url = `${rootUrl}`;
|
const url = `${rootUrl}`;
|
||||||
const response = await apiClient.post<IServerResponse<ITask>>(`${url}`, body);
|
const response = await apiClient.post<IServerResponse<ITask>>(`${url}`, body);
|
||||||
return response.data;
|
return response.data;
|
||||||
},
|
},
|
||||||
updateTemplate: async (id: string, body: { name: string, tasks: IProjectTask[] }): Promise<IServerResponse<ITask>> => {
|
updateTemplate: async (
|
||||||
|
id: string,
|
||||||
|
body: { name: string; tasks: IProjectTask[] }
|
||||||
|
): Promise<IServerResponse<ITask>> => {
|
||||||
const url = `${rootUrl}/${id}`;
|
const url = `${rootUrl}/${id}`;
|
||||||
const response = await apiClient.put<IServerResponse<ITask>>(`${url}`, body);
|
const response = await apiClient.put<IServerResponse<ITask>>(`${url}`, body);
|
||||||
return response.data;
|
return response.data;
|
||||||
|
|||||||
@@ -43,7 +43,7 @@ export const phasesApiService = {
|
|||||||
return response.data;
|
return response.data;
|
||||||
},
|
},
|
||||||
|
|
||||||
updateNameOfPhase: async (phaseId: string, body: ITaskPhase, projectId: string,) => {
|
updateNameOfPhase: async (phaseId: string, body: ITaskPhase, projectId: string) => {
|
||||||
const q = toQueryString({ id: projectId, current_project_id: projectId });
|
const q = toQueryString({ id: projectId, current_project_id: projectId });
|
||||||
const response = await apiClient.put<IServerResponse<ITaskPhase>>(
|
const response = await apiClient.put<IServerResponse<ITaskPhase>>(
|
||||||
`${rootUrl}/${phaseId}${q}`,
|
`${rootUrl}/${phaseId}${q}`,
|
||||||
|
|||||||
@@ -69,8 +69,16 @@ export const statusApiService = {
|
|||||||
return response.data;
|
return response.data;
|
||||||
},
|
},
|
||||||
|
|
||||||
deleteStatus: async (statusId: string, projectId: string, replacingStatusId: string): Promise<IServerResponse<void>> => {
|
deleteStatus: async (
|
||||||
const q = toQueryString({ project: projectId, current_project_id: projectId, replace: replacingStatusId || null });
|
statusId: string,
|
||||||
|
projectId: string,
|
||||||
|
replacingStatusId: string
|
||||||
|
): Promise<IServerResponse<void>> => {
|
||||||
|
const q = toQueryString({
|
||||||
|
project: projectId,
|
||||||
|
current_project_id: projectId,
|
||||||
|
replace: replacingStatusId || null,
|
||||||
|
});
|
||||||
const response = await apiClient.delete<IServerResponse<void>>(`${rootUrl}/${statusId}${q}`);
|
const response = await apiClient.delete<IServerResponse<void>>(`${rootUrl}/${statusId}${q}`);
|
||||||
return response.data;
|
return response.data;
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
import { API_BASE_URL } from "@/shared/constants";
|
import { API_BASE_URL } from '@/shared/constants';
|
||||||
import apiClient from "../api-client";
|
import apiClient from '../api-client';
|
||||||
import { IServerResponse } from "@/types/common.types";
|
import { IServerResponse } from '@/types/common.types';
|
||||||
import { ISubTask } from "@/types/tasks/subTask.types";
|
import { ISubTask } from '@/types/tasks/subTask.types';
|
||||||
|
|
||||||
const root = `${API_BASE_URL}/sub-tasks`;
|
const root = `${API_BASE_URL}/sub-tasks`;
|
||||||
|
|
||||||
@@ -10,7 +10,4 @@ export const subTasksApiService = {
|
|||||||
const response = await apiClient.get(`${root}/${parentTaskId}`);
|
const response = await apiClient.get(`${root}/${parentTaskId}`);
|
||||||
return response.data;
|
return response.data;
|
||||||
},
|
},
|
||||||
|
|
||||||
|
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@@ -1,5 +1,9 @@
|
|||||||
import { IServerResponse } from '@/types/common.types';
|
import { IServerResponse } from '@/types/common.types';
|
||||||
import { IProjectAttachmentsViewModel, ITaskAttachment, ITaskAttachmentViewModel } from '@/types/tasks/task-attachment-view-model';
|
import {
|
||||||
|
IProjectAttachmentsViewModel,
|
||||||
|
ITaskAttachment,
|
||||||
|
ITaskAttachmentViewModel,
|
||||||
|
} from '@/types/tasks/task-attachment-view-model';
|
||||||
import apiClient from '../api-client';
|
import apiClient from '../api-client';
|
||||||
import { API_BASE_URL } from '@/shared/constants';
|
import { API_BASE_URL } from '@/shared/constants';
|
||||||
import { IAvatarAttachment } from '@/types/avatarAttachment.types';
|
import { IAvatarAttachment } from '@/types/avatarAttachment.types';
|
||||||
@@ -8,7 +12,6 @@ import { toQueryString } from '@/utils/toQueryString';
|
|||||||
const rootUrl = `${API_BASE_URL}/attachments`;
|
const rootUrl = `${API_BASE_URL}/attachments`;
|
||||||
|
|
||||||
const taskAttachmentsApiService = {
|
const taskAttachmentsApiService = {
|
||||||
|
|
||||||
createTaskAttachment: async (
|
createTaskAttachment: async (
|
||||||
body: ITaskAttachment
|
body: ITaskAttachment
|
||||||
): Promise<IServerResponse<ITaskAttachmentViewModel>> => {
|
): Promise<IServerResponse<ITaskAttachmentViewModel>> => {
|
||||||
@@ -16,18 +19,26 @@ const taskAttachmentsApiService = {
|
|||||||
return response.data;
|
return response.data;
|
||||||
},
|
},
|
||||||
|
|
||||||
createAvatarAttachment: async (body: IAvatarAttachment): Promise<IServerResponse<{ url: string; }>> => {
|
createAvatarAttachment: async (
|
||||||
|
body: IAvatarAttachment
|
||||||
|
): Promise<IServerResponse<{ url: string }>> => {
|
||||||
const response = await apiClient.post(`${rootUrl}/avatar`, body);
|
const response = await apiClient.post(`${rootUrl}/avatar`, body);
|
||||||
return response.data;
|
return response.data;
|
||||||
},
|
},
|
||||||
|
|
||||||
getTaskAttachments: async (taskId: string): Promise<IServerResponse<ITaskAttachmentViewModel[]>> => {
|
getTaskAttachments: async (
|
||||||
|
taskId: string
|
||||||
|
): Promise<IServerResponse<ITaskAttachmentViewModel[]>> => {
|
||||||
const response = await apiClient.get(`${rootUrl}/tasks/${taskId}`);
|
const response = await apiClient.get(`${rootUrl}/tasks/${taskId}`);
|
||||||
return response.data;
|
return response.data;
|
||||||
},
|
},
|
||||||
|
|
||||||
getProjectAttachments: async (projectId: string, index: number, size: number ): Promise<IServerResponse<IProjectAttachmentsViewModel>> => {
|
getProjectAttachments: async (
|
||||||
const q = toQueryString({ index, size });
|
projectId: string,
|
||||||
|
index: number,
|
||||||
|
size: number
|
||||||
|
): Promise<IServerResponse<IProjectAttachmentsViewModel>> => {
|
||||||
|
const q = toQueryString({ index, size });
|
||||||
const response = await apiClient.get(`${rootUrl}/project/${projectId}${q}`);
|
const response = await apiClient.get(`${rootUrl}/project/${projectId}${q}`);
|
||||||
return response.data;
|
return response.data;
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -2,10 +2,16 @@ import apiClient from '@api/api-client';
|
|||||||
import { API_BASE_URL } from '@/shared/constants';
|
import { API_BASE_URL } from '@/shared/constants';
|
||||||
import { IServerResponse } from '@/types/common.types';
|
import { IServerResponse } from '@/types/common.types';
|
||||||
import { toQueryString } from '@/utils/toQueryString';
|
import { toQueryString } from '@/utils/toQueryString';
|
||||||
import { ITaskComment, ITaskCommentsCreateRequest, ITaskCommentViewModel } from '@/types/tasks/task-comments.types';
|
import {
|
||||||
|
ITaskComment,
|
||||||
|
ITaskCommentsCreateRequest,
|
||||||
|
ITaskCommentViewModel,
|
||||||
|
} from '@/types/tasks/task-comments.types';
|
||||||
|
|
||||||
const taskCommentsApiService = {
|
const taskCommentsApiService = {
|
||||||
create: async (data: ITaskCommentsCreateRequest): Promise<IServerResponse<ITaskCommentsCreateRequest>> => {
|
create: async (
|
||||||
|
data: ITaskCommentsCreateRequest
|
||||||
|
): Promise<IServerResponse<ITaskCommentsCreateRequest>> => {
|
||||||
const response = await apiClient.post(`${API_BASE_URL}/task-comments`, data);
|
const response = await apiClient.post(`${API_BASE_URL}/task-comments`, data);
|
||||||
return response.data;
|
return response.data;
|
||||||
},
|
},
|
||||||
@@ -21,12 +27,16 @@ const taskCommentsApiService = {
|
|||||||
},
|
},
|
||||||
|
|
||||||
deleteAttachment: async (id: string, taskId: string): Promise<IServerResponse<ITaskComment>> => {
|
deleteAttachment: async (id: string, taskId: string): Promise<IServerResponse<ITaskComment>> => {
|
||||||
const response = await apiClient.delete(`${API_BASE_URL}/task-comments/attachment/${id}/${taskId}`);
|
const response = await apiClient.delete(
|
||||||
|
`${API_BASE_URL}/task-comments/attachment/${id}/${taskId}`
|
||||||
|
);
|
||||||
return response.data;
|
return response.data;
|
||||||
},
|
},
|
||||||
|
|
||||||
download: async (id: string, filename: string): Promise<IServerResponse<any>> => {
|
download: async (id: string, filename: string): Promise<IServerResponse<any>> => {
|
||||||
const response = await apiClient.get(`${API_BASE_URL}/task-comments/download?id=${id}&file=${filename}`);
|
const response = await apiClient.get(
|
||||||
|
`${API_BASE_URL}/task-comments/download?id=${id}&file=${filename}`
|
||||||
|
);
|
||||||
return response.data;
|
return response.data;
|
||||||
},
|
},
|
||||||
|
|
||||||
@@ -35,8 +45,13 @@ const taskCommentsApiService = {
|
|||||||
return response.data;
|
return response.data;
|
||||||
},
|
},
|
||||||
|
|
||||||
updateReaction: async (id: string, body: {reaction_type: string, task_id: string}): Promise<IServerResponse<ITaskComment>> => {
|
updateReaction: async (
|
||||||
const response = await apiClient.put(`${API_BASE_URL}/task-comments/reaction/${id}${toQueryString(body)}`);
|
id: string,
|
||||||
|
body: { reaction_type: string; task_id: string }
|
||||||
|
): Promise<IServerResponse<ITaskComment>> => {
|
||||||
|
const response = await apiClient.put(
|
||||||
|
`${API_BASE_URL}/task-comments/reaction/${id}${toQueryString(body)}`
|
||||||
|
);
|
||||||
return response.data;
|
return response.data;
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
import { API_BASE_URL } from "@/shared/constants";
|
import { API_BASE_URL } from '@/shared/constants';
|
||||||
import apiClient from "../api-client";
|
import apiClient from '../api-client';
|
||||||
import { ITaskDependency } from "@/types/tasks/task-dependency.types";
|
import { ITaskDependency } from '@/types/tasks/task-dependency.types';
|
||||||
import { IServerResponse } from "@/types/common.types";
|
import { IServerResponse } from '@/types/common.types';
|
||||||
|
|
||||||
const rootUrl = `${API_BASE_URL}/task-dependencies`;
|
const rootUrl = `${API_BASE_URL}/task-dependencies`;
|
||||||
|
|
||||||
@@ -10,7 +10,9 @@ export const taskDependenciesApiService = {
|
|||||||
const response = await apiClient.get(`${rootUrl}/${taskId}`);
|
const response = await apiClient.get(`${rootUrl}/${taskId}`);
|
||||||
return response.data;
|
return response.data;
|
||||||
},
|
},
|
||||||
createTaskDependency: async (body: ITaskDependency): Promise<IServerResponse<ITaskDependency>> => {
|
createTaskDependency: async (
|
||||||
|
body: ITaskDependency
|
||||||
|
): Promise<IServerResponse<ITaskDependency>> => {
|
||||||
const response = await apiClient.post(`${rootUrl}`, body);
|
const response = await apiClient.post(`${rootUrl}`, body);
|
||||||
return response.data;
|
return response.data;
|
||||||
},
|
},
|
||||||
@@ -18,4 +20,4 @@ export const taskDependenciesApiService = {
|
|||||||
const response = await apiClient.delete(`${rootUrl}/${dependencyId}`);
|
const response = await apiClient.delete(`${rootUrl}/${dependencyId}`);
|
||||||
return response.data;
|
return response.data;
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -1,16 +1,21 @@
|
|||||||
import { API_BASE_URL } from "@/shared/constants";
|
import { API_BASE_URL } from '@/shared/constants';
|
||||||
import { IServerResponse } from "@/types/common.types";
|
import { IServerResponse } from '@/types/common.types';
|
||||||
import { ITaskRecurringSchedule } from "@/types/tasks/task-recurring-schedule";
|
import { ITaskRecurringSchedule } from '@/types/tasks/task-recurring-schedule';
|
||||||
import apiClient from "../api-client";
|
import apiClient from '../api-client';
|
||||||
|
|
||||||
const rootUrl = `${API_BASE_URL}/task-recurring`;
|
const rootUrl = `${API_BASE_URL}/task-recurring`;
|
||||||
|
|
||||||
export const taskRecurringApiService = {
|
export const taskRecurringApiService = {
|
||||||
getTaskRecurringData: async (schedule_id: string): Promise<IServerResponse<ITaskRecurringSchedule>> => {
|
getTaskRecurringData: async (
|
||||||
const response = await apiClient.get(`${rootUrl}/${schedule_id}`);
|
schedule_id: string
|
||||||
return response.data;
|
): Promise<IServerResponse<ITaskRecurringSchedule>> => {
|
||||||
},
|
const response = await apiClient.get(`${rootUrl}/${schedule_id}`);
|
||||||
updateTaskRecurringData: async (schedule_id: string, body: any): Promise<IServerResponse<ITaskRecurringSchedule>> => {
|
return response.data;
|
||||||
return apiClient.put(`${rootUrl}/${schedule_id}`, body);
|
},
|
||||||
}
|
updateTaskRecurringData: async (
|
||||||
}
|
schedule_id: string,
|
||||||
|
body: any
|
||||||
|
): Promise<IServerResponse<ITaskRecurringSchedule>> => {
|
||||||
|
return apiClient.put(`${rootUrl}/${schedule_id}`, body);
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
import { API_BASE_URL } from "@/shared/constants";
|
import { API_BASE_URL } from '@/shared/constants';
|
||||||
import apiClient from "../api-client";
|
import apiClient from '../api-client';
|
||||||
import { IServerResponse } from "@/types/common.types";
|
import { IServerResponse } from '@/types/common.types';
|
||||||
import { ITaskLogViewModel } from "@/types/tasks/task-log-view.types";
|
import { ITaskLogViewModel } from '@/types/tasks/task-log-view.types';
|
||||||
|
|
||||||
const rootUrl = `${API_BASE_URL}/task-time-log`;
|
const rootUrl = `${API_BASE_URL}/task-time-log`;
|
||||||
|
|
||||||
@@ -16,12 +16,12 @@ export interface IRunningTimer {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export const taskTimeLogsApiService = {
|
export const taskTimeLogsApiService = {
|
||||||
getByTask: async (id: string) : Promise<IServerResponse<ITaskLogViewModel[]>> => {
|
getByTask: async (id: string): Promise<IServerResponse<ITaskLogViewModel[]>> => {
|
||||||
const response = await apiClient.get(`${rootUrl}/task/${id}`);
|
const response = await apiClient.get(`${rootUrl}/task/${id}`);
|
||||||
return response.data;
|
return response.data;
|
||||||
},
|
},
|
||||||
|
|
||||||
delete: async (id: string, taskId: string) : Promise<IServerResponse<void>> => {
|
delete: async (id: string, taskId: string): Promise<IServerResponse<void>> => {
|
||||||
const response = await apiClient.delete(`${rootUrl}/${id}?task=${taskId}`);
|
const response = await apiClient.delete(`${rootUrl}/${id}?task=${taskId}`);
|
||||||
return response.data;
|
return response.data;
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -1,23 +1,23 @@
|
|||||||
import { ITaskListColumn } from "@/types/tasks/taskList.types";
|
import { ITaskListColumn } from '@/types/tasks/taskList.types';
|
||||||
import apiClient from "../api-client";
|
import apiClient from '../api-client';
|
||||||
import { IServerResponse } from "@/types/common.types";
|
import { IServerResponse } from '@/types/common.types';
|
||||||
|
|
||||||
export const tasksCustomColumnsService = {
|
export const tasksCustomColumnsService = {
|
||||||
getCustomColumns: async (projectId: string): Promise<IServerResponse<ITaskListColumn[]>> => {
|
getCustomColumns: async (projectId: string): Promise<IServerResponse<ITaskListColumn[]>> => {
|
||||||
const response = await apiClient.get(`/api/v1/custom-columns/project/${projectId}/columns`);
|
const response = await apiClient.get(`/api/v1/custom-columns/project/${projectId}/columns`);
|
||||||
return response.data;
|
return response.data;
|
||||||
},
|
},
|
||||||
|
|
||||||
updateTaskCustomColumnValue: async (
|
updateTaskCustomColumnValue: async (
|
||||||
taskId: string,
|
taskId: string,
|
||||||
columnKey: string,
|
columnKey: string,
|
||||||
value: string | number | boolean,
|
value: string | number | boolean,
|
||||||
projectId: string
|
projectId: string
|
||||||
): Promise<IServerResponse<any>> => {
|
): Promise<IServerResponse<any>> => {
|
||||||
const response = await apiClient.put(`/api/v1/tasks/${taskId}/custom-column`, {
|
const response = await apiClient.put(`/api/v1/tasks/${taskId}/custom-column`, {
|
||||||
column_key: columnKey,
|
column_key: columnKey,
|
||||||
value: value,
|
value: value,
|
||||||
project_id: projectId
|
project_id: projectId,
|
||||||
});
|
});
|
||||||
return response.data;
|
return response.data;
|
||||||
},
|
},
|
||||||
@@ -35,7 +35,7 @@ export const tasksCustomColumnsService = {
|
|||||||
): Promise<IServerResponse<any>> => {
|
): Promise<IServerResponse<any>> => {
|
||||||
const response = await apiClient.post('/api/v1/custom-columns', {
|
const response = await apiClient.post('/api/v1/custom-columns', {
|
||||||
project_id: projectId,
|
project_id: projectId,
|
||||||
...columnData
|
...columnData,
|
||||||
});
|
});
|
||||||
return response.data;
|
return response.data;
|
||||||
},
|
},
|
||||||
@@ -63,7 +63,10 @@ export const tasksCustomColumnsService = {
|
|||||||
projectId: string,
|
projectId: string,
|
||||||
item: ITaskListColumn
|
item: ITaskListColumn
|
||||||
): Promise<IServerResponse<ITaskListColumn>> => {
|
): Promise<IServerResponse<ITaskListColumn>> => {
|
||||||
const response = await apiClient.put(`/api/v1/custom-columns/project/${projectId}/columns`, item);
|
const response = await apiClient.put(
|
||||||
|
`/api/v1/custom-columns/project/${projectId}/columns`,
|
||||||
|
item
|
||||||
|
);
|
||||||
return response.data;
|
return response.data;
|
||||||
}
|
},
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -131,14 +131,19 @@ export const tasksApiService = {
|
|||||||
return response.data;
|
return response.data;
|
||||||
},
|
},
|
||||||
|
|
||||||
getTaskDependencyStatus: async (taskId: string, statusId: string): Promise<IServerResponse<{ can_continue: boolean }>> => {
|
getTaskDependencyStatus: async (
|
||||||
const q = toQueryString({taskId, statusId});
|
taskId: string,
|
||||||
|
statusId: string
|
||||||
|
): Promise<IServerResponse<{ can_continue: boolean }>> => {
|
||||||
|
const q = toQueryString({ taskId, statusId });
|
||||||
const response = await apiClient.get(`${rootUrl}/dependency-status${q}`);
|
const response = await apiClient.get(`${rootUrl}/dependency-status${q}`);
|
||||||
return response.data;
|
return response.data;
|
||||||
},
|
},
|
||||||
|
|
||||||
getTaskListV3: async (config: ITaskListConfigV2): Promise<IServerResponse<ITaskListV3Response>> => {
|
getTaskListV3: async (
|
||||||
const q = toQueryString({ ...config, include_empty: "true" });
|
config: ITaskListConfigV2
|
||||||
|
): Promise<IServerResponse<ITaskListV3Response>> => {
|
||||||
|
const q = toQueryString({ ...config, include_empty: 'true' });
|
||||||
const response = await apiClient.get(`${rootUrl}/list/v3/${config.id}${q}`);
|
const response = await apiClient.get(`${rootUrl}/list/v3/${config.id}${q}`);
|
||||||
return response.data;
|
return response.data;
|
||||||
},
|
},
|
||||||
@@ -148,20 +153,28 @@ export const tasksApiService = {
|
|||||||
return response.data;
|
return response.data;
|
||||||
},
|
},
|
||||||
|
|
||||||
getTaskProgressStatus: async (projectId: string): Promise<IServerResponse<{
|
getTaskProgressStatus: async (
|
||||||
projectId: string;
|
projectId: string
|
||||||
totalTasks: number;
|
): Promise<
|
||||||
completedTasks: number;
|
IServerResponse<{
|
||||||
avgProgress: number;
|
projectId: string;
|
||||||
lastUpdated: string;
|
totalTasks: number;
|
||||||
completionPercentage: number;
|
completedTasks: number;
|
||||||
}>> => {
|
avgProgress: number;
|
||||||
|
lastUpdated: string;
|
||||||
|
completionPercentage: number;
|
||||||
|
}>
|
||||||
|
> => {
|
||||||
const response = await apiClient.get(`${rootUrl}/progress-status/${projectId}`);
|
const response = await apiClient.get(`${rootUrl}/progress-status/${projectId}`);
|
||||||
return response.data;
|
return response.data;
|
||||||
},
|
},
|
||||||
|
|
||||||
// API method to reorder tasks
|
// API method to reorder tasks
|
||||||
reorderTasks: async (params: { taskIds: string[]; newOrder: number[]; projectId: string }): Promise<IServerResponse<{ done: boolean }>> => {
|
reorderTasks: async (params: {
|
||||||
|
taskIds: string[];
|
||||||
|
newOrder: number[];
|
||||||
|
projectId: string;
|
||||||
|
}): Promise<IServerResponse<{ done: boolean }>> => {
|
||||||
const response = await apiClient.post(`${rootUrl}/reorder`, {
|
const response = await apiClient.post(`${rootUrl}/reorder`, {
|
||||||
task_ids: params.taskIds,
|
task_ids: params.taskIds,
|
||||||
new_order: params.newOrder,
|
new_order: params.newOrder,
|
||||||
@@ -171,7 +184,12 @@ export const tasksApiService = {
|
|||||||
},
|
},
|
||||||
|
|
||||||
// API method to update task group (status, priority, phase)
|
// API method to update task group (status, priority, phase)
|
||||||
updateTaskGroup: async (params: { taskId: string; groupType: 'status' | 'priority' | 'phase'; groupValue: string; projectId: string }): Promise<IServerResponse<{ done: boolean }>> => {
|
updateTaskGroup: async (params: {
|
||||||
|
taskId: string;
|
||||||
|
groupType: 'status' | 'priority' | 'phase';
|
||||||
|
groupValue: string;
|
||||||
|
projectId: string;
|
||||||
|
}): Promise<IServerResponse<{ done: boolean }>> => {
|
||||||
const response = await apiClient.put(`${rootUrl}/${params.taskId}/group`, {
|
const response = await apiClient.put(`${rootUrl}/${params.taskId}/group`, {
|
||||||
group_type: params.groupType,
|
group_type: params.groupType,
|
||||||
group_value: params.groupValue,
|
group_value: params.groupValue,
|
||||||
|
|||||||
@@ -44,7 +44,9 @@ export const teamMembersApiService = {
|
|||||||
return response.data;
|
return response.data;
|
||||||
},
|
},
|
||||||
|
|
||||||
getAll: async (projectId: string | null = null): Promise<IServerResponse<ITeamMemberViewModel[]>> => {
|
getAll: async (
|
||||||
|
projectId: string | null = null
|
||||||
|
): Promise<IServerResponse<ITeamMemberViewModel[]>> => {
|
||||||
const params = new URLSearchParams(projectId ? { project: projectId } : {});
|
const params = new URLSearchParams(projectId ? { project: projectId } : {});
|
||||||
const response = await apiClient.get<IServerResponse<ITeamMemberViewModel[]>>(
|
const response = await apiClient.get<IServerResponse<ITeamMemberViewModel[]>>(
|
||||||
`${rootUrl}/all${params.toString() ? '?' + params.toString() : ''}`
|
`${rootUrl}/all${params.toString() ? '?' + params.toString() : ''}`
|
||||||
|
|||||||
@@ -14,11 +14,8 @@ const rootUrl = `${API_BASE_URL}/teams`;
|
|||||||
|
|
||||||
export const teamsApiService = {
|
export const teamsApiService = {
|
||||||
getTeams: async (): Promise<IServerResponse<ITeamGetResponse[]>> => {
|
getTeams: async (): Promise<IServerResponse<ITeamGetResponse[]>> => {
|
||||||
const response = await apiClient.get<IServerResponse<ITeamGetResponse[]>>(
|
const response = await apiClient.get<IServerResponse<ITeamGetResponse[]>>(`${rootUrl}`);
|
||||||
`${rootUrl}`
|
|
||||||
);
|
|
||||||
return response.data;
|
return response.data;
|
||||||
|
|
||||||
},
|
},
|
||||||
|
|
||||||
setActiveTeam: async (teamId: string): Promise<IServerResponse<ITeamActivateResponse>> => {
|
setActiveTeam: async (teamId: string): Promise<IServerResponse<ITeamActivateResponse>> => {
|
||||||
@@ -29,23 +26,18 @@ export const teamsApiService = {
|
|||||||
return response.data;
|
return response.data;
|
||||||
},
|
},
|
||||||
|
|
||||||
|
|
||||||
createTeam: async (team: IOrganizationTeam): Promise<IServerResponse<ITeam>> => {
|
createTeam: async (team: IOrganizationTeam): Promise<IServerResponse<ITeam>> => {
|
||||||
const response = await apiClient.post<IServerResponse<ITeam>>(`${rootUrl}`, team);
|
const response = await apiClient.post<IServerResponse<ITeam>>(`${rootUrl}`, team);
|
||||||
return response.data;
|
return response.data;
|
||||||
},
|
},
|
||||||
|
|
||||||
|
|
||||||
getInvitations: async (): Promise<IServerResponse<ITeamInvites[]>> => {
|
getInvitations: async (): Promise<IServerResponse<ITeamInvites[]>> => {
|
||||||
const response = await apiClient.get<IServerResponse<ITeamInvites[]>>(
|
const response = await apiClient.get<IServerResponse<ITeamInvites[]>>(`${rootUrl}/invites`);
|
||||||
`${rootUrl}/invites`
|
|
||||||
);
|
|
||||||
return response.data;
|
return response.data;
|
||||||
},
|
},
|
||||||
|
|
||||||
acceptInvitation: async (body: IAcceptTeamInvite): Promise<IServerResponse<ITeamInvites>> => {
|
acceptInvitation: async (body: IAcceptTeamInvite): Promise<IServerResponse<ITeamInvites>> => {
|
||||||
const response = await apiClient.put<IServerResponse<ITeamInvites>>(`${rootUrl}`, body);
|
const response = await apiClient.put<IServerResponse<ITeamInvites>>(`${rootUrl}`, body);
|
||||||
return response.data;
|
return response.data;
|
||||||
}
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@@ -15,7 +15,7 @@ class ReduxPerformanceMonitor {
|
|||||||
|
|
||||||
logMetric(metric: PerformanceMetrics) {
|
logMetric(metric: PerformanceMetrics) {
|
||||||
this.metrics.push(metric);
|
this.metrics.push(metric);
|
||||||
|
|
||||||
// Keep only recent metrics
|
// Keep only recent metrics
|
||||||
if (this.metrics.length > this.maxMetrics) {
|
if (this.metrics.length > this.maxMetrics) {
|
||||||
this.metrics = this.metrics.slice(-this.maxMetrics);
|
this.metrics = this.metrics.slice(-this.maxMetrics);
|
||||||
@@ -49,14 +49,14 @@ class ReduxPerformanceMonitor {
|
|||||||
export const performanceMonitor = new ReduxPerformanceMonitor();
|
export const performanceMonitor = new ReduxPerformanceMonitor();
|
||||||
|
|
||||||
// Redux middleware for performance monitoring
|
// Redux middleware for performance monitoring
|
||||||
export const performanceMiddleware: Middleware = (store) => (next) => (action: any) => {
|
export const performanceMiddleware: Middleware = store => next => (action: any) => {
|
||||||
const start = performance.now();
|
const start = performance.now();
|
||||||
|
|
||||||
const result = next(action);
|
const result = next(action);
|
||||||
|
|
||||||
const end = performance.now();
|
const end = performance.now();
|
||||||
const duration = end - start;
|
const duration = end - start;
|
||||||
|
|
||||||
// Calculate approximate state size (in development only)
|
// Calculate approximate state size (in development only)
|
||||||
let stateSize = 0;
|
let stateSize = 0;
|
||||||
if (process.env.NODE_ENV === 'development') {
|
if (process.env.NODE_ENV === 'development') {
|
||||||
@@ -101,7 +101,7 @@ export function analyzeReduxPerformance() {
|
|||||||
|
|
||||||
// Count action frequencies
|
// Count action frequencies
|
||||||
metrics.forEach(m => {
|
metrics.forEach(m => {
|
||||||
analysis.mostFrequentActions[m.actionType] =
|
analysis.mostFrequentActions[m.actionType] =
|
||||||
(analysis.mostFrequentActions[m.actionType] || 0) + 1;
|
(analysis.mostFrequentActions[m.actionType] || 0) + 1;
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -109,14 +109,15 @@ export function analyzeReduxPerformance() {
|
|||||||
if (analysis.slowActions > analysis.totalActions * 0.1) {
|
if (analysis.slowActions > analysis.totalActions * 0.1) {
|
||||||
analysis.recommendations.push('Consider optimizing selectors with createSelector');
|
analysis.recommendations.push('Consider optimizing selectors with createSelector');
|
||||||
}
|
}
|
||||||
|
|
||||||
if (analysis.largestStateSize > 1000000) { // 1MB
|
if (analysis.largestStateSize > 1000000) {
|
||||||
|
// 1MB
|
||||||
analysis.recommendations.push('State size is large - consider normalizing data');
|
analysis.recommendations.push('State size is large - consider normalizing data');
|
||||||
}
|
}
|
||||||
|
|
||||||
if (analysis.averageActionTime > 20) {
|
if (analysis.averageActionTime > 20) {
|
||||||
analysis.recommendations.push('Average action time is high - check for expensive reducers');
|
analysis.recommendations.push('Average action time is high - check for expensive reducers');
|
||||||
}
|
}
|
||||||
|
|
||||||
return analysis;
|
return analysis;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -62,10 +62,12 @@ export const AdminGuard = memo(({ children }: GuardProps) => {
|
|||||||
const guardResult = useMemo(() => {
|
const guardResult = useMemo(() => {
|
||||||
try {
|
try {
|
||||||
// Defensive checks to ensure authService and its methods exist
|
// Defensive checks to ensure authService and its methods exist
|
||||||
if (!authService ||
|
if (
|
||||||
typeof authService.isAuthenticated !== 'function' ||
|
!authService ||
|
||||||
typeof authService.isOwnerOrAdmin !== 'function' ||
|
typeof authService.isAuthenticated !== 'function' ||
|
||||||
typeof authService.getCurrentSession !== 'function') {
|
typeof authService.isOwnerOrAdmin !== 'function' ||
|
||||||
|
typeof authService.getCurrentSession !== 'function'
|
||||||
|
) {
|
||||||
return null; // Don't redirect if auth service is not ready
|
return null; // Don't redirect if auth service is not ready
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -75,7 +77,7 @@ export const AdminGuard = memo(({ children }: GuardProps) => {
|
|||||||
|
|
||||||
const currentSession = authService.getCurrentSession();
|
const currentSession = authService.getCurrentSession();
|
||||||
const isFreePlan = currentSession?.subscription_type === ISUBSCRIPTION_TYPE.FREE;
|
const isFreePlan = currentSession?.subscription_type === ISUBSCRIPTION_TYPE.FREE;
|
||||||
|
|
||||||
if (!authService.isOwnerOrAdmin() || isFreePlan) {
|
if (!authService.isOwnerOrAdmin() || isFreePlan) {
|
||||||
return { redirect: '/worklenz/unauthorized' };
|
return { redirect: '/worklenz/unauthorized' };
|
||||||
}
|
}
|
||||||
@@ -103,9 +105,11 @@ export const LicenseExpiryGuard = memo(({ children }: GuardProps) => {
|
|||||||
const shouldRedirect = useMemo(() => {
|
const shouldRedirect = useMemo(() => {
|
||||||
try {
|
try {
|
||||||
// Defensive checks to ensure authService and its methods exist
|
// Defensive checks to ensure authService and its methods exist
|
||||||
if (!authService ||
|
if (
|
||||||
typeof authService.isAuthenticated !== 'function' ||
|
!authService ||
|
||||||
typeof authService.getCurrentSession !== 'function') {
|
typeof authService.isAuthenticated !== 'function' ||
|
||||||
|
typeof authService.getCurrentSession !== 'function'
|
||||||
|
) {
|
||||||
return false; // Don't redirect if auth service is not ready
|
return false; // Don't redirect if auth service is not ready
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -120,37 +124,40 @@ export const LicenseExpiryGuard = memo(({ children }: GuardProps) => {
|
|||||||
const currentSession = authService.getCurrentSession();
|
const currentSession = authService.getCurrentSession();
|
||||||
|
|
||||||
// Check if trial is expired more than 7 days or if is_expired flag is set
|
// Check if trial is expired more than 7 days or if is_expired flag is set
|
||||||
const isLicenseExpiredMoreThan7Days = () => {
|
const isLicenseExpiredMoreThan7Days = () => {
|
||||||
// Quick bail if no session data is available
|
// Quick bail if no session data is available
|
||||||
if (!currentSession) return false;
|
if (!currentSession) return false;
|
||||||
|
|
||||||
// Check is_expired flag first
|
// Check is_expired flag first
|
||||||
if (currentSession.is_expired) {
|
if (currentSession.is_expired) {
|
||||||
// If no trial_expire_date exists but is_expired is true, defer to backend check
|
// If no trial_expire_date exists but is_expired is true, defer to backend check
|
||||||
if (!currentSession.trial_expire_date) return true;
|
if (!currentSession.trial_expire_date) return true;
|
||||||
|
|
||||||
// If there is a trial_expire_date, check if it's more than 7 days past
|
// If there is a trial_expire_date, check if it's more than 7 days past
|
||||||
const today = new Date();
|
const today = new Date();
|
||||||
const expiryDate = new Date(currentSession.trial_expire_date);
|
const expiryDate = new Date(currentSession.trial_expire_date);
|
||||||
const diffTime = today.getTime() - expiryDate.getTime();
|
const diffTime = today.getTime() - expiryDate.getTime();
|
||||||
const diffDays = Math.ceil(diffTime / (1000 * 60 * 60 * 24));
|
const diffDays = Math.ceil(diffTime / (1000 * 60 * 60 * 24));
|
||||||
|
|
||||||
// Redirect if more than 7 days past expiration
|
// Redirect if more than 7 days past expiration
|
||||||
return diffDays > 7;
|
return diffDays > 7;
|
||||||
}
|
}
|
||||||
|
|
||||||
// If not marked as expired but has trial_expire_date, do a date check
|
// If not marked as expired but has trial_expire_date, do a date check
|
||||||
if (currentSession.subscription_type === ISUBSCRIPTION_TYPE.TRIAL && currentSession.trial_expire_date) {
|
if (
|
||||||
|
currentSession.subscription_type === ISUBSCRIPTION_TYPE.TRIAL &&
|
||||||
|
currentSession.trial_expire_date
|
||||||
|
) {
|
||||||
const today = new Date();
|
const today = new Date();
|
||||||
const expiryDate = new Date(currentSession.trial_expire_date);
|
const expiryDate = new Date(currentSession.trial_expire_date);
|
||||||
|
|
||||||
const diffTime = today.getTime() - expiryDate.getTime();
|
const diffTime = today.getTime() - expiryDate.getTime();
|
||||||
const diffDays = Math.ceil(diffTime / (1000 * 60 * 60 * 24));
|
const diffDays = Math.ceil(diffTime / (1000 * 60 * 60 * 24));
|
||||||
|
|
||||||
// If expired more than 7 days, redirect
|
// If expired more than 7 days, redirect
|
||||||
return diffDays > 7;
|
return diffDays > 7;
|
||||||
}
|
}
|
||||||
|
|
||||||
// No expiration data found
|
// No expiration data found
|
||||||
return false;
|
return false;
|
||||||
};
|
};
|
||||||
@@ -227,30 +234,34 @@ const wrapRoutes = (
|
|||||||
// Optimized static license expired component
|
// Optimized static license expired component
|
||||||
const StaticLicenseExpired = memo(() => {
|
const StaticLicenseExpired = memo(() => {
|
||||||
return (
|
return (
|
||||||
<div style={{
|
<div
|
||||||
marginTop: 65,
|
style={{
|
||||||
minHeight: '90vh',
|
marginTop: 65,
|
||||||
backgroundColor: '#f5f5f5',
|
minHeight: '90vh',
|
||||||
padding: '20px',
|
backgroundColor: '#f5f5f5',
|
||||||
display: 'flex',
|
padding: '20px',
|
||||||
justifyContent: 'center',
|
display: 'flex',
|
||||||
alignItems: 'center'
|
justifyContent: 'center',
|
||||||
}}>
|
alignItems: 'center',
|
||||||
<div style={{
|
}}
|
||||||
background: 'white',
|
>
|
||||||
padding: '30px',
|
<div
|
||||||
borderRadius: '8px',
|
style={{
|
||||||
boxShadow: '0 2px 8px rgba(0,0,0,0.1)',
|
background: 'white',
|
||||||
textAlign: 'center',
|
padding: '30px',
|
||||||
maxWidth: '600px'
|
borderRadius: '8px',
|
||||||
}}>
|
boxShadow: '0 2px 8px rgba(0,0,0,0.1)',
|
||||||
|
textAlign: 'center',
|
||||||
|
maxWidth: '600px',
|
||||||
|
}}
|
||||||
|
>
|
||||||
<h1 style={{ fontSize: '24px', color: '#faad14', marginBottom: '16px' }}>
|
<h1 style={{ fontSize: '24px', color: '#faad14', marginBottom: '16px' }}>
|
||||||
Your Worklenz trial has expired!
|
Your Worklenz trial has expired!
|
||||||
</h1>
|
</h1>
|
||||||
<p style={{ fontSize: '16px', color: '#555', marginBottom: '24px' }}>
|
<p style={{ fontSize: '16px', color: '#555', marginBottom: '24px' }}>
|
||||||
Please upgrade now to continue using Worklenz.
|
Please upgrade now to continue using Worklenz.
|
||||||
</p>
|
</p>
|
||||||
<button
|
<button
|
||||||
style={{
|
style={{
|
||||||
backgroundColor: '#1890ff',
|
backgroundColor: '#1890ff',
|
||||||
color: 'white',
|
color: 'white',
|
||||||
@@ -258,9 +269,9 @@ const StaticLicenseExpired = memo(() => {
|
|||||||
padding: '8px 16px',
|
padding: '8px 16px',
|
||||||
borderRadius: '4px',
|
borderRadius: '4px',
|
||||||
fontSize: '16px',
|
fontSize: '16px',
|
||||||
cursor: 'pointer'
|
cursor: 'pointer',
|
||||||
}}
|
}}
|
||||||
onClick={() => window.location.href = '/worklenz/admin-center/billing'}
|
onClick={() => (window.location.href = '/worklenz/admin-center/billing')}
|
||||||
>
|
>
|
||||||
Upgrade now
|
Upgrade now
|
||||||
</button>
|
</button>
|
||||||
@@ -272,11 +283,7 @@ const StaticLicenseExpired = memo(() => {
|
|||||||
StaticLicenseExpired.displayName = 'StaticLicenseExpired';
|
StaticLicenseExpired.displayName = 'StaticLicenseExpired';
|
||||||
|
|
||||||
// Create route arrays (moved outside of useMemo to avoid hook violations)
|
// Create route arrays (moved outside of useMemo to avoid hook violations)
|
||||||
const publicRoutes = [
|
const publicRoutes = [...rootRoutes, ...authRoutes, notFoundRoute];
|
||||||
...rootRoutes,
|
|
||||||
...authRoutes,
|
|
||||||
notFoundRoute
|
|
||||||
];
|
|
||||||
|
|
||||||
const protectedMainRoutes = wrapRoutes(mainRoutes, AuthGuard);
|
const protectedMainRoutes = wrapRoutes(mainRoutes, AuthGuard);
|
||||||
const adminRoutes = wrapRoutes(reportingRoutes, AdminGuard);
|
const adminRoutes = wrapRoutes(reportingRoutes, AdminGuard);
|
||||||
@@ -305,37 +312,35 @@ const withLicenseExpiryCheck = (routes: RouteObject[]): RouteObject[] => {
|
|||||||
const licenseCheckedMainRoutes = withLicenseExpiryCheck(protectedMainRoutes);
|
const licenseCheckedMainRoutes = withLicenseExpiryCheck(protectedMainRoutes);
|
||||||
|
|
||||||
// Create optimized router with future flags for better performance
|
// Create optimized router with future flags for better performance
|
||||||
const router = createBrowserRouter([
|
const router = createBrowserRouter(
|
||||||
|
[
|
||||||
|
{
|
||||||
|
element: (
|
||||||
|
<ErrorBoundary>
|
||||||
|
<AuthenticatedLayout />
|
||||||
|
</ErrorBoundary>
|
||||||
|
),
|
||||||
|
errorElement: (
|
||||||
|
<ErrorBoundary>
|
||||||
|
<Suspense fallback={<SuspenseFallback />}>
|
||||||
|
<NotFoundPage />
|
||||||
|
</Suspense>
|
||||||
|
</ErrorBoundary>
|
||||||
|
),
|
||||||
|
children: [...licenseCheckedMainRoutes, ...adminRoutes, ...setupRoutes, licenseExpiredRoute],
|
||||||
|
},
|
||||||
|
...publicRoutes,
|
||||||
|
],
|
||||||
{
|
{
|
||||||
element: (
|
// Enable React Router future features for better performance
|
||||||
<ErrorBoundary>
|
future: {
|
||||||
<AuthenticatedLayout />
|
v7_relativeSplatPath: true,
|
||||||
</ErrorBoundary>
|
v7_fetcherPersist: true,
|
||||||
),
|
v7_normalizeFormMethod: true,
|
||||||
errorElement: (
|
v7_partialHydration: true,
|
||||||
<ErrorBoundary>
|
v7_skipActionErrorRevalidation: true,
|
||||||
<Suspense fallback={<SuspenseFallback />}>
|
},
|
||||||
<NotFoundPage />
|
|
||||||
</Suspense>
|
|
||||||
</ErrorBoundary>
|
|
||||||
),
|
|
||||||
children: [
|
|
||||||
...licenseCheckedMainRoutes,
|
|
||||||
...adminRoutes,
|
|
||||||
...setupRoutes,
|
|
||||||
licenseExpiredRoute,
|
|
||||||
],
|
|
||||||
},
|
|
||||||
...publicRoutes,
|
|
||||||
], {
|
|
||||||
// Enable React Router future features for better performance
|
|
||||||
future: {
|
|
||||||
v7_relativeSplatPath: true,
|
|
||||||
v7_fetcherPersist: true,
|
|
||||||
v7_normalizeFormMethod: true,
|
|
||||||
v7_partialHydration: true,
|
|
||||||
v7_skipActionErrorRevalidation: true
|
|
||||||
}
|
}
|
||||||
});
|
);
|
||||||
|
|
||||||
export default router;
|
export default router;
|
||||||
|
|||||||
@@ -11,7 +11,9 @@ import { SuspenseFallback } from '@/components/suspense-fallback/suspense-fallba
|
|||||||
const HomePage = lazy(() => import('@/pages/home/home-page'));
|
const HomePage = lazy(() => import('@/pages/home/home-page'));
|
||||||
const ProjectList = lazy(() => import('@/pages/projects/project-list'));
|
const ProjectList = lazy(() => import('@/pages/projects/project-list'));
|
||||||
const Schedule = lazy(() => import('@/pages/schedule/schedule'));
|
const Schedule = lazy(() => import('@/pages/schedule/schedule'));
|
||||||
const ProjectTemplateEditView = lazy(() => import('@/pages/settings/project-templates/projectTemplateEditView/ProjectTemplateEditView'));
|
const ProjectTemplateEditView = lazy(
|
||||||
|
() => import('@/pages/settings/project-templates/projectTemplateEditView/ProjectTemplateEditView')
|
||||||
|
);
|
||||||
const LicenseExpired = lazy(() => import('@/pages/license-expired/license-expired'));
|
const LicenseExpired = lazy(() => import('@/pages/license-expired/license-expired'));
|
||||||
const ProjectView = lazy(() => import('@/pages/projects/projectView/project-view'));
|
const ProjectView = lazy(() => import('@/pages/projects/projectView/project-view'));
|
||||||
const Unauthorized = lazy(() => import('@/pages/unauthorized/unauthorized'));
|
const Unauthorized = lazy(() => import('@/pages/unauthorized/unauthorized'));
|
||||||
@@ -23,9 +25,11 @@ const AdminGuard = ({ children }: { children: React.ReactNode }) => {
|
|||||||
|
|
||||||
try {
|
try {
|
||||||
// Defensive checks to ensure authService and its methods exist
|
// Defensive checks to ensure authService and its methods exist
|
||||||
if (!authService ||
|
if (
|
||||||
typeof authService.isAuthenticated !== 'function' ||
|
!authService ||
|
||||||
typeof authService.isOwnerOrAdmin !== 'function') {
|
typeof authService.isAuthenticated !== 'function' ||
|
||||||
|
typeof authService.isOwnerOrAdmin !== 'function'
|
||||||
|
) {
|
||||||
// If auth service is not ready, render children (don't block)
|
// If auth service is not ready, render children (don't block)
|
||||||
return <>{children}</>;
|
return <>{children}</>;
|
||||||
}
|
}
|
||||||
@@ -52,21 +56,21 @@ const mainRoutes: RouteObject[] = [
|
|||||||
element: <MainLayout />,
|
element: <MainLayout />,
|
||||||
children: [
|
children: [
|
||||||
{ index: true, element: <Navigate to="home" replace /> },
|
{ index: true, element: <Navigate to="home" replace /> },
|
||||||
{
|
{
|
||||||
path: 'home',
|
path: 'home',
|
||||||
element: (
|
element: (
|
||||||
<Suspense fallback={<SuspenseFallback />}>
|
<Suspense fallback={<SuspenseFallback />}>
|
||||||
<HomePage />
|
<HomePage />
|
||||||
</Suspense>
|
</Suspense>
|
||||||
)
|
),
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
path: 'projects',
|
path: 'projects',
|
||||||
element: (
|
element: (
|
||||||
<Suspense fallback={<SuspenseFallback />}>
|
<Suspense fallback={<SuspenseFallback />}>
|
||||||
<ProjectList />
|
<ProjectList />
|
||||||
</Suspense>
|
</Suspense>
|
||||||
)
|
),
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
path: 'schedule',
|
path: 'schedule',
|
||||||
@@ -76,15 +80,15 @@ const mainRoutes: RouteObject[] = [
|
|||||||
<Schedule />
|
<Schedule />
|
||||||
</AdminGuard>
|
</AdminGuard>
|
||||||
</Suspense>
|
</Suspense>
|
||||||
)
|
),
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
path: `projects/:projectId`,
|
path: `projects/:projectId`,
|
||||||
element: (
|
element: (
|
||||||
<Suspense fallback={<SuspenseFallback />}>
|
<Suspense fallback={<SuspenseFallback />}>
|
||||||
<ProjectView />
|
<ProjectView />
|
||||||
</Suspense>
|
</Suspense>
|
||||||
)
|
),
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
path: `settings/project-templates/edit/:templateId/:templateName`,
|
path: `settings/project-templates/edit/:templateId/:templateName`,
|
||||||
@@ -94,13 +98,13 @@ const mainRoutes: RouteObject[] = [
|
|||||||
</Suspense>
|
</Suspense>
|
||||||
),
|
),
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
path: 'unauthorized',
|
path: 'unauthorized',
|
||||||
element: (
|
element: (
|
||||||
<Suspense fallback={<SuspenseFallback />}>
|
<Suspense fallback={<SuspenseFallback />}>
|
||||||
<Unauthorized />
|
<Unauthorized />
|
||||||
</Suspense>
|
</Suspense>
|
||||||
)
|
),
|
||||||
},
|
},
|
||||||
...settingsRoutes,
|
...settingsRoutes,
|
||||||
...adminCenterRoutes,
|
...adminCenterRoutes,
|
||||||
@@ -113,15 +117,15 @@ export const licenseExpiredRoute: RouteObject = {
|
|||||||
path: '/worklenz',
|
path: '/worklenz',
|
||||||
element: <MainLayout />,
|
element: <MainLayout />,
|
||||||
children: [
|
children: [
|
||||||
{
|
{
|
||||||
path: 'license-expired',
|
path: 'license-expired',
|
||||||
element: (
|
element: (
|
||||||
<Suspense fallback={<SuspenseFallback />}>
|
<Suspense fallback={<SuspenseFallback />}>
|
||||||
<LicenseExpired />
|
<LicenseExpired />
|
||||||
</Suspense>
|
</Suspense>
|
||||||
)
|
),
|
||||||
}
|
},
|
||||||
]
|
],
|
||||||
};
|
};
|
||||||
|
|
||||||
export default mainRoutes;
|
export default mainRoutes;
|
||||||
|
|||||||
@@ -4,7 +4,13 @@ import SettingsLayout from '@/layouts/SettingsLayout';
|
|||||||
import { settingsItems } from '@/lib/settings/settings-constants';
|
import { settingsItems } from '@/lib/settings/settings-constants';
|
||||||
import { useAuthService } from '@/hooks/useAuth';
|
import { useAuthService } from '@/hooks/useAuth';
|
||||||
|
|
||||||
const SettingsGuard = ({ children, adminRequired }: { children: React.ReactNode; adminRequired: boolean }) => {
|
const SettingsGuard = ({
|
||||||
|
children,
|
||||||
|
adminRequired,
|
||||||
|
}: {
|
||||||
|
children: React.ReactNode;
|
||||||
|
adminRequired: boolean;
|
||||||
|
}) => {
|
||||||
const isOwnerOrAdmin = useAuthService().isOwnerOrAdmin();
|
const isOwnerOrAdmin = useAuthService().isOwnerOrAdmin();
|
||||||
|
|
||||||
if (adminRequired && !isOwnerOrAdmin) {
|
if (adminRequired && !isOwnerOrAdmin) {
|
||||||
@@ -20,11 +26,7 @@ const settingsRoutes: RouteObject[] = [
|
|||||||
element: <SettingsLayout />,
|
element: <SettingsLayout />,
|
||||||
children: settingsItems.map(item => ({
|
children: settingsItems.map(item => ({
|
||||||
path: item.endpoint,
|
path: item.endpoint,
|
||||||
element: (
|
element: <SettingsGuard adminRequired={!!item.adminOnly}>{item.element}</SettingsGuard>,
|
||||||
<SettingsGuard adminRequired={!!item.adminOnly}>
|
|
||||||
{item.element}
|
|
||||||
</SettingsGuard>
|
|
||||||
),
|
|
||||||
})),
|
})),
|
||||||
},
|
},
|
||||||
];
|
];
|
||||||
|
|||||||
@@ -7,10 +7,7 @@ import { RootState } from './store';
|
|||||||
// Auth selectors
|
// Auth selectors
|
||||||
export const selectAuth = (state: RootState) => state.auth;
|
export const selectAuth = (state: RootState) => state.auth;
|
||||||
export const selectUser = (state: RootState) => state.userReducer;
|
export const selectUser = (state: RootState) => state.userReducer;
|
||||||
export const selectIsAuthenticated = createSelector(
|
export const selectIsAuthenticated = createSelector([selectAuth], auth => !!auth.user);
|
||||||
[selectAuth],
|
|
||||||
(auth) => !!auth.user
|
|
||||||
);
|
|
||||||
|
|
||||||
// Project selectors
|
// Project selectors
|
||||||
export const selectProjects = (state: RootState) => state.projectsReducer;
|
export const selectProjects = (state: RootState) => state.projectsReducer;
|
||||||
@@ -69,13 +66,10 @@ export const selectGroupByFilter = (state: RootState) => state.groupByFilterDrop
|
|||||||
// Memoized computed selectors for common use cases
|
// Memoized computed selectors for common use cases
|
||||||
export const selectHasActiveProject = createSelector(
|
export const selectHasActiveProject = createSelector(
|
||||||
[selectCurrentProject],
|
[selectCurrentProject],
|
||||||
(project) => !!project && Object.keys(project).length > 0
|
project => !!project && Object.keys(project).length > 0
|
||||||
);
|
);
|
||||||
|
|
||||||
export const selectIsLoading = createSelector(
|
export const selectIsLoading = createSelector([selectTasks, selectProjects], (tasks, projects) => {
|
||||||
[selectTasks, selectProjects],
|
// Check if any major feature is loading
|
||||||
(tasks, projects) => {
|
return (tasks as any)?.loading || (projects as any)?.loading;
|
||||||
// Check if any major feature is loading
|
});
|
||||||
return (tasks as any)?.loading || (projects as any)?.loading;
|
|
||||||
}
|
|
||||||
);
|
|
||||||
|
|||||||
@@ -122,7 +122,7 @@ export const store = configureStore({
|
|||||||
taskListCustomColumnsReducer: taskListCustomColumnsReducer,
|
taskListCustomColumnsReducer: taskListCustomColumnsReducer,
|
||||||
boardReducer: boardReducer,
|
boardReducer: boardReducer,
|
||||||
projectDrawerReducer: projectDrawerReducer,
|
projectDrawerReducer: projectDrawerReducer,
|
||||||
|
|
||||||
projectViewReducer: projectViewReducer,
|
projectViewReducer: projectViewReducer,
|
||||||
|
|
||||||
// Project Lookups
|
// Project Lookups
|
||||||
|
|||||||
@@ -22,10 +22,10 @@ interface AssigneeSelectorProps {
|
|||||||
isDarkMode?: boolean;
|
isDarkMode?: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
const AssigneeSelector: React.FC<AssigneeSelectorProps> = ({
|
const AssigneeSelector: React.FC<AssigneeSelectorProps> = ({
|
||||||
task,
|
task,
|
||||||
groupId = null,
|
groupId = null,
|
||||||
isDarkMode = false
|
isDarkMode = false,
|
||||||
}) => {
|
}) => {
|
||||||
const [isOpen, setIsOpen] = useState(false);
|
const [isOpen, setIsOpen] = useState(false);
|
||||||
const [searchQuery, setSearchQuery] = useState('');
|
const [searchQuery, setSearchQuery] = useState('');
|
||||||
@@ -63,8 +63,12 @@ const AssigneeSelector: React.FC<AssigneeSelectorProps> = ({
|
|||||||
// Close dropdown when clicking outside and handle scroll
|
// Close dropdown when clicking outside and handle scroll
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const handleClickOutside = (event: MouseEvent) => {
|
const handleClickOutside = (event: MouseEvent) => {
|
||||||
if (dropdownRef.current && !dropdownRef.current.contains(event.target as Node) &&
|
if (
|
||||||
buttonRef.current && !buttonRef.current.contains(event.target as Node)) {
|
dropdownRef.current &&
|
||||||
|
!dropdownRef.current.contains(event.target as Node) &&
|
||||||
|
buttonRef.current &&
|
||||||
|
!buttonRef.current.contains(event.target as Node)
|
||||||
|
) {
|
||||||
setIsOpen(false);
|
setIsOpen(false);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
@@ -74,10 +78,12 @@ const AssigneeSelector: React.FC<AssigneeSelectorProps> = ({
|
|||||||
// Check if the button is still visible in the viewport
|
// Check if the button is still visible in the viewport
|
||||||
if (buttonRef.current) {
|
if (buttonRef.current) {
|
||||||
const rect = buttonRef.current.getBoundingClientRect();
|
const rect = buttonRef.current.getBoundingClientRect();
|
||||||
const isVisible = rect.top >= 0 && rect.left >= 0 &&
|
const isVisible =
|
||||||
rect.bottom <= window.innerHeight &&
|
rect.top >= 0 &&
|
||||||
rect.right <= window.innerWidth;
|
rect.left >= 0 &&
|
||||||
|
rect.bottom <= window.innerHeight &&
|
||||||
|
rect.right <= window.innerWidth;
|
||||||
|
|
||||||
if (isVisible) {
|
if (isVisible) {
|
||||||
updateDropdownPosition();
|
updateDropdownPosition();
|
||||||
} else {
|
} else {
|
||||||
@@ -98,7 +104,7 @@ const AssigneeSelector: React.FC<AssigneeSelectorProps> = ({
|
|||||||
document.addEventListener('mousedown', handleClickOutside);
|
document.addEventListener('mousedown', handleClickOutside);
|
||||||
window.addEventListener('scroll', handleScroll, true);
|
window.addEventListener('scroll', handleScroll, true);
|
||||||
window.addEventListener('resize', handleResize);
|
window.addEventListener('resize', handleResize);
|
||||||
|
|
||||||
return () => {
|
return () => {
|
||||||
document.removeEventListener('mousedown', handleClickOutside);
|
document.removeEventListener('mousedown', handleClickOutside);
|
||||||
window.removeEventListener('scroll', handleScroll, true);
|
window.removeEventListener('scroll', handleScroll, true);
|
||||||
@@ -113,10 +119,10 @@ const AssigneeSelector: React.FC<AssigneeSelectorProps> = ({
|
|||||||
const handleDropdownToggle = (e: React.MouseEvent) => {
|
const handleDropdownToggle = (e: React.MouseEvent) => {
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
e.stopPropagation();
|
e.stopPropagation();
|
||||||
|
|
||||||
if (!isOpen) {
|
if (!isOpen) {
|
||||||
updateDropdownPosition();
|
updateDropdownPosition();
|
||||||
|
|
||||||
// Prepare team members data when opening
|
// Prepare team members data when opening
|
||||||
const assignees = task?.assignees?.map(assignee => assignee.team_member_id);
|
const assignees = task?.assignees?.map(assignee => assignee.team_member_id);
|
||||||
const membersData = (members?.data || []).map(member => ({
|
const membersData = (members?.data || []).map(member => ({
|
||||||
@@ -125,7 +131,7 @@ const AssigneeSelector: React.FC<AssigneeSelectorProps> = ({
|
|||||||
}));
|
}));
|
||||||
const sortedMembers = sortTeamMembers(membersData);
|
const sortedMembers = sortTeamMembers(membersData);
|
||||||
setTeamMembers({ data: sortedMembers });
|
setTeamMembers({ data: sortedMembers });
|
||||||
|
|
||||||
setIsOpen(true);
|
setIsOpen(true);
|
||||||
// Focus search input after opening
|
// Focus search input after opening
|
||||||
setTimeout(() => {
|
setTimeout(() => {
|
||||||
@@ -160,11 +166,9 @@ const AssigneeSelector: React.FC<AssigneeSelectorProps> = ({
|
|||||||
// Update local team members state for dropdown UI
|
// Update local team members state for dropdown UI
|
||||||
setTeamMembers(prev => ({
|
setTeamMembers(prev => ({
|
||||||
...prev,
|
...prev,
|
||||||
data: (prev.data || []).map(member =>
|
data: (prev.data || []).map(member =>
|
||||||
member.id === memberId
|
member.id === memberId ? { ...member, selected: checked } : member
|
||||||
? { ...member, selected: checked }
|
),
|
||||||
: member
|
|
||||||
)
|
|
||||||
}));
|
}));
|
||||||
|
|
||||||
const body = {
|
const body = {
|
||||||
@@ -178,12 +182,9 @@ const AssigneeSelector: React.FC<AssigneeSelectorProps> = ({
|
|||||||
|
|
||||||
// Emit socket event - the socket handler will update Redux with proper types
|
// Emit socket event - the socket handler will update Redux with proper types
|
||||||
socket?.emit(SocketEvents.QUICK_ASSIGNEES_UPDATE.toString(), JSON.stringify(body));
|
socket?.emit(SocketEvents.QUICK_ASSIGNEES_UPDATE.toString(), JSON.stringify(body));
|
||||||
socket?.once(
|
socket?.once(SocketEvents.QUICK_ASSIGNEES_UPDATE.toString(), (data: any) => {
|
||||||
SocketEvents.QUICK_ASSIGNEES_UPDATE.toString(),
|
dispatch(updateEnhancedKanbanTaskAssignees(data));
|
||||||
(data: any) => {
|
});
|
||||||
dispatch(updateEnhancedKanbanTaskAssignees(data));
|
|
||||||
}
|
|
||||||
);
|
|
||||||
|
|
||||||
// Remove from pending changes after a short delay (optimistic)
|
// Remove from pending changes after a short delay (optimistic)
|
||||||
setTimeout(() => {
|
setTimeout(() => {
|
||||||
@@ -198,9 +199,10 @@ const AssigneeSelector: React.FC<AssigneeSelectorProps> = ({
|
|||||||
const checkMemberSelected = (memberId: string) => {
|
const checkMemberSelected = (memberId: string) => {
|
||||||
if (!memberId) return false;
|
if (!memberId) return false;
|
||||||
// Use optimistic assignees if available, otherwise fall back to task assignees
|
// Use optimistic assignees if available, otherwise fall back to task assignees
|
||||||
const assignees = optimisticAssignees.length > 0
|
const assignees =
|
||||||
? optimisticAssignees
|
optimisticAssignees.length > 0
|
||||||
: task?.assignees?.map(assignee => assignee.team_member_id) || [];
|
? optimisticAssignees
|
||||||
|
: task?.assignees?.map(assignee => assignee.team_member_id) || [];
|
||||||
return assignees.includes(memberId);
|
return assignees.includes(memberId);
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -217,149 +219,159 @@ const AssigneeSelector: React.FC<AssigneeSelectorProps> = ({
|
|||||||
className={`
|
className={`
|
||||||
w-5 h-5 rounded-full border border-dashed flex items-center justify-center
|
w-5 h-5 rounded-full border border-dashed flex items-center justify-center
|
||||||
transition-colors duration-200
|
transition-colors duration-200
|
||||||
${isOpen
|
${
|
||||||
? isDarkMode
|
isOpen
|
||||||
? 'border-blue-500 bg-blue-900/20 text-blue-400'
|
? isDarkMode
|
||||||
: 'border-blue-500 bg-blue-50 text-blue-600'
|
? 'border-blue-500 bg-blue-900/20 text-blue-400'
|
||||||
: isDarkMode
|
: 'border-blue-500 bg-blue-50 text-blue-600'
|
||||||
? 'border-gray-600 hover:border-gray-500 hover:bg-gray-800 text-gray-400'
|
: isDarkMode
|
||||||
: 'border-gray-300 hover:border-gray-400 hover:bg-gray-100 text-gray-600'
|
? 'border-gray-600 hover:border-gray-500 hover:bg-gray-800 text-gray-400'
|
||||||
|
: 'border-gray-300 hover:border-gray-400 hover:bg-gray-100 text-gray-600'
|
||||||
}
|
}
|
||||||
`}
|
`}
|
||||||
>
|
>
|
||||||
<PlusOutlined className="text-xs" />
|
<PlusOutlined className="text-xs" />
|
||||||
</button>
|
</button>
|
||||||
|
|
||||||
{isOpen && createPortal(
|
{isOpen &&
|
||||||
<div
|
createPortal(
|
||||||
ref={dropdownRef}
|
<div
|
||||||
onClick={e => e.stopPropagation()}
|
ref={dropdownRef}
|
||||||
className={`
|
onClick={e => e.stopPropagation()}
|
||||||
|
className={`
|
||||||
fixed z-9999 w-72 rounded-md shadow-lg border
|
fixed z-9999 w-72 rounded-md shadow-lg border
|
||||||
${isDarkMode
|
${isDarkMode ? 'bg-gray-800 border-gray-600' : 'bg-white border-gray-200'}
|
||||||
? 'bg-gray-800 border-gray-600'
|
|
||||||
: 'bg-white border-gray-200'
|
|
||||||
}
|
|
||||||
`}
|
`}
|
||||||
style={{
|
style={{
|
||||||
top: dropdownPosition.top,
|
top: dropdownPosition.top,
|
||||||
left: dropdownPosition.left,
|
left: dropdownPosition.left,
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
{/* Header */}
|
{/* Header */}
|
||||||
<div className={`p-2 border-b ${isDarkMode ? 'border-gray-600' : 'border-gray-300'}`}>
|
<div className={`p-2 border-b ${isDarkMode ? 'border-gray-600' : 'border-gray-300'}`}>
|
||||||
<input
|
<input
|
||||||
ref={searchInputRef}
|
ref={searchInputRef}
|
||||||
type="text"
|
type="text"
|
||||||
value={searchQuery}
|
value={searchQuery}
|
||||||
onChange={(e) => setSearchQuery(e.target.value)}
|
onChange={e => setSearchQuery(e.target.value)}
|
||||||
placeholder="Search members..."
|
placeholder="Search members..."
|
||||||
className={`
|
className={`
|
||||||
w-full px-2 py-1 text-xs rounded border
|
w-full px-2 py-1 text-xs rounded border
|
||||||
${isDarkMode
|
${
|
||||||
? 'bg-gray-700 border-gray-600 text-gray-100 placeholder-gray-400 focus:border-blue-500'
|
isDarkMode
|
||||||
: 'bg-white border-gray-300 text-gray-900 placeholder-gray-500 focus:border-blue-500'
|
? 'bg-gray-700 border-gray-600 text-gray-100 placeholder-gray-400 focus:border-blue-500'
|
||||||
|
: 'bg-white border-gray-300 text-gray-900 placeholder-gray-500 focus:border-blue-500'
|
||||||
}
|
}
|
||||||
focus:outline-none focus:ring-1 focus:ring-blue-500
|
focus:outline-none focus:ring-1 focus:ring-blue-500
|
||||||
`}
|
`}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Members List */}
|
{/* Members List */}
|
||||||
<div className="max-h-48 overflow-y-auto">
|
<div className="max-h-48 overflow-y-auto">
|
||||||
{filteredMembers && filteredMembers.length > 0 ? (
|
{filteredMembers && filteredMembers.length > 0 ? (
|
||||||
filteredMembers.map((member) => (
|
filteredMembers.map(member => (
|
||||||
<div
|
<div
|
||||||
key={member.id}
|
key={member.id}
|
||||||
className={`
|
className={`
|
||||||
flex items-center gap-2 p-2 cursor-pointer transition-colors
|
flex items-center gap-2 p-2 cursor-pointer transition-colors
|
||||||
${member.pending_invitation
|
${
|
||||||
? 'opacity-50 cursor-not-allowed'
|
member.pending_invitation
|
||||||
: isDarkMode
|
? 'opacity-50 cursor-not-allowed'
|
||||||
? 'hover:bg-gray-700'
|
: isDarkMode
|
||||||
: 'hover:bg-gray-50'
|
? 'hover:bg-gray-700'
|
||||||
|
: 'hover:bg-gray-50'
|
||||||
}
|
}
|
||||||
`}
|
`}
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
if (!member.pending_invitation) {
|
if (!member.pending_invitation) {
|
||||||
const isSelected = checkMemberSelected(member.id || '');
|
const isSelected = checkMemberSelected(member.id || '');
|
||||||
handleMemberToggle(member.id || '', !isSelected);
|
handleMemberToggle(member.id || '', !isSelected);
|
||||||
}
|
}
|
||||||
}}
|
}}
|
||||||
style={{
|
style={{
|
||||||
// Add visual feedback for immediate response
|
// Add visual feedback for immediate response
|
||||||
transition: 'all 0.15s ease-in-out',
|
transition: 'all 0.15s ease-in-out',
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<div className="relative">
|
<div className="relative">
|
||||||
<span onClick={e => e.stopPropagation()}>
|
<span onClick={e => e.stopPropagation()}>
|
||||||
<Checkbox
|
<Checkbox
|
||||||
checked={checkMemberSelected(member.id || '')}
|
checked={checkMemberSelected(member.id || '')}
|
||||||
onChange={(checked) => handleMemberToggle(member.id || '', checked)}
|
onChange={checked => handleMemberToggle(member.id || '', checked)}
|
||||||
disabled={member.pending_invitation || pendingChanges.has(member.id || '')}
|
disabled={
|
||||||
isDarkMode={isDarkMode}
|
member.pending_invitation || pendingChanges.has(member.id || '')
|
||||||
/>
|
}
|
||||||
</span>
|
isDarkMode={isDarkMode}
|
||||||
{pendingChanges.has(member.id || '') && (
|
/>
|
||||||
<div className={`absolute inset-0 flex items-center justify-center ${
|
</span>
|
||||||
isDarkMode ? 'bg-gray-800/50' : 'bg-white/50'
|
{pendingChanges.has(member.id || '') && (
|
||||||
}`}>
|
<div
|
||||||
<div className={`w-3 h-3 border border-t-transparent rounded-full animate-spin ${
|
className={`absolute inset-0 flex items-center justify-center ${
|
||||||
isDarkMode ? 'border-blue-400' : 'border-blue-600'
|
isDarkMode ? 'bg-gray-800/50' : 'bg-white/50'
|
||||||
}`} />
|
}`}
|
||||||
</div>
|
>
|
||||||
)}
|
<div
|
||||||
</div>
|
className={`w-3 h-3 border border-t-transparent rounded-full animate-spin ${
|
||||||
|
isDarkMode ? 'border-blue-400' : 'border-blue-600'
|
||||||
<Avatar
|
}`}
|
||||||
src={member.avatar_url}
|
/>
|
||||||
name={member.name || ''}
|
</div>
|
||||||
size={24}
|
|
||||||
isDarkMode={isDarkMode}
|
|
||||||
/>
|
|
||||||
|
|
||||||
<div className="flex-1 min-w-0">
|
|
||||||
<div className={`text-xs font-medium truncate ${isDarkMode ? 'text-gray-100' : 'text-gray-900'}`}>
|
|
||||||
{member.name}
|
|
||||||
</div>
|
|
||||||
<div className={`text-xs truncate ${isDarkMode ? 'text-gray-400' : 'text-gray-500'}`}>
|
|
||||||
{member.email}
|
|
||||||
{member.pending_invitation && (
|
|
||||||
<span className="text-red-400 ml-1">(Pending)</span>
|
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
))
|
|
||||||
) : (
|
|
||||||
<div className={`p-4 text-center ${isDarkMode ? 'text-gray-400' : 'text-gray-500'}`}>
|
|
||||||
<div className="text-xs">No members found</div>
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{/* Footer */}
|
<Avatar
|
||||||
<div className={`p-2 border-t ${isDarkMode ? 'border-gray-600' : 'border-gray-200'}`}>
|
src={member.avatar_url}
|
||||||
<button
|
name={member.name || ''}
|
||||||
className={`
|
size={24}
|
||||||
|
isDarkMode={isDarkMode}
|
||||||
|
/>
|
||||||
|
|
||||||
|
<div className="flex-1 min-w-0">
|
||||||
|
<div
|
||||||
|
className={`text-xs font-medium truncate ${isDarkMode ? 'text-gray-100' : 'text-gray-900'}`}
|
||||||
|
>
|
||||||
|
{member.name}
|
||||||
|
</div>
|
||||||
|
<div
|
||||||
|
className={`text-xs truncate ${isDarkMode ? 'text-gray-400' : 'text-gray-500'}`}
|
||||||
|
>
|
||||||
|
{member.email}
|
||||||
|
{member.pending_invitation && (
|
||||||
|
<span className="text-red-400 ml-1">(Pending)</span>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
))
|
||||||
|
) : (
|
||||||
|
<div
|
||||||
|
className={`p-4 text-center ${isDarkMode ? 'text-gray-400' : 'text-gray-500'}`}
|
||||||
|
>
|
||||||
|
<div className="text-xs">No members found</div>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Footer */}
|
||||||
|
<div className={`p-2 border-t ${isDarkMode ? 'border-gray-600' : 'border-gray-200'}`}>
|
||||||
|
<button
|
||||||
|
className={`
|
||||||
w-full flex items-center justify-center gap-1 px-2 py-1 text-xs rounded
|
w-full flex items-center justify-center gap-1 px-2 py-1 text-xs rounded
|
||||||
transition-colors
|
transition-colors
|
||||||
${isDarkMode
|
${isDarkMode ? 'text-blue-400 hover:bg-gray-700' : 'text-blue-600 hover:bg-blue-50'}
|
||||||
? 'text-blue-400 hover:bg-gray-700'
|
|
||||||
: 'text-blue-600 hover:bg-blue-50'
|
|
||||||
}
|
|
||||||
`}
|
`}
|
||||||
onClick={handleInviteProjectMemberDrawer}
|
onClick={handleInviteProjectMemberDrawer}
|
||||||
>
|
>
|
||||||
<UserAddOutlined />
|
<UserAddOutlined />
|
||||||
Invite member
|
Invite member
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
</div>,
|
</div>,
|
||||||
document.body
|
document.body
|
||||||
)}
|
)}
|
||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
export default AssigneeSelector;
|
export default AssigneeSelector;
|
||||||
|
|||||||
@@ -11,47 +11,63 @@ interface AvatarProps {
|
|||||||
style?: React.CSSProperties;
|
style?: React.CSSProperties;
|
||||||
}
|
}
|
||||||
|
|
||||||
const Avatar: React.FC<AvatarProps> = ({
|
const Avatar: React.FC<AvatarProps> = ({
|
||||||
name = '',
|
name = '',
|
||||||
size = 'default',
|
size = 'default',
|
||||||
isDarkMode = false,
|
isDarkMode = false,
|
||||||
className = '',
|
className = '',
|
||||||
src,
|
src,
|
||||||
backgroundColor,
|
backgroundColor,
|
||||||
onClick,
|
onClick,
|
||||||
style = {}
|
style = {},
|
||||||
}) => {
|
}) => {
|
||||||
// Handle both numeric and string sizes
|
// Handle both numeric and string sizes
|
||||||
const getSize = () => {
|
const getSize = () => {
|
||||||
if (typeof size === 'number') {
|
if (typeof size === 'number') {
|
||||||
return { width: size, height: size, fontSize: `${size * 0.4}px` };
|
return { width: size, height: size, fontSize: `${size * 0.4}px` };
|
||||||
}
|
}
|
||||||
|
|
||||||
const sizeMap = {
|
const sizeMap = {
|
||||||
small: { width: 24, height: 24, fontSize: '10px' },
|
small: { width: 24, height: 24, fontSize: '10px' },
|
||||||
default: { width: 32, height: 32, fontSize: '14px' },
|
default: { width: 32, height: 32, fontSize: '14px' },
|
||||||
large: { width: 48, height: 48, fontSize: '18px' }
|
large: { width: 48, height: 48, fontSize: '18px' },
|
||||||
};
|
};
|
||||||
|
|
||||||
return sizeMap[size];
|
return sizeMap[size];
|
||||||
};
|
};
|
||||||
|
|
||||||
const sizeStyle = getSize();
|
const sizeStyle = getSize();
|
||||||
|
|
||||||
const lightColors = [
|
const lightColors = [
|
||||||
'#f56565', '#4299e1', '#48bb78', '#ed8936', '#9f7aea',
|
'#f56565',
|
||||||
'#ed64a6', '#667eea', '#38b2ac', '#f6ad55', '#4fd1c7'
|
'#4299e1',
|
||||||
|
'#48bb78',
|
||||||
|
'#ed8936',
|
||||||
|
'#9f7aea',
|
||||||
|
'#ed64a6',
|
||||||
|
'#667eea',
|
||||||
|
'#38b2ac',
|
||||||
|
'#f6ad55',
|
||||||
|
'#4fd1c7',
|
||||||
];
|
];
|
||||||
|
|
||||||
const darkColors = [
|
const darkColors = [
|
||||||
'#e53e3e', '#3182ce', '#38a169', '#dd6b20', '#805ad5',
|
'#e53e3e',
|
||||||
'#d53f8c', '#5a67d8', '#319795', '#d69e2e', '#319795'
|
'#3182ce',
|
||||||
|
'#38a169',
|
||||||
|
'#dd6b20',
|
||||||
|
'#805ad5',
|
||||||
|
'#d53f8c',
|
||||||
|
'#5a67d8',
|
||||||
|
'#319795',
|
||||||
|
'#d69e2e',
|
||||||
|
'#319795',
|
||||||
];
|
];
|
||||||
|
|
||||||
const colors = isDarkMode ? darkColors : lightColors;
|
const colors = isDarkMode ? darkColors : lightColors;
|
||||||
const colorIndex = name.charCodeAt(0) % colors.length;
|
const colorIndex = name.charCodeAt(0) % colors.length;
|
||||||
const defaultBgColor = backgroundColor || colors[colorIndex];
|
const defaultBgColor = backgroundColor || colors[colorIndex];
|
||||||
|
|
||||||
const handleClick = (e: React.MouseEvent) => {
|
const handleClick = (e: React.MouseEvent) => {
|
||||||
e.stopPropagation();
|
e.stopPropagation();
|
||||||
onClick?.(e);
|
onClick?.(e);
|
||||||
@@ -60,7 +76,7 @@ const Avatar: React.FC<AvatarProps> = ({
|
|||||||
const avatarStyle = {
|
const avatarStyle = {
|
||||||
...sizeStyle,
|
...sizeStyle,
|
||||||
backgroundColor: defaultBgColor,
|
backgroundColor: defaultBgColor,
|
||||||
...style
|
...style,
|
||||||
};
|
};
|
||||||
|
|
||||||
if (src) {
|
if (src) {
|
||||||
@@ -74,9 +90,9 @@ const Avatar: React.FC<AvatarProps> = ({
|
|||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div
|
<div
|
||||||
onClick={handleClick}
|
onClick={handleClick}
|
||||||
className={`rounded-full flex items-center justify-center text-white font-medium shadow-sm cursor-pointer ${className}`}
|
className={`rounded-full flex items-center justify-center text-white font-medium shadow-sm cursor-pointer ${className}`}
|
||||||
style={avatarStyle}
|
style={avatarStyle}
|
||||||
@@ -86,4 +102,4 @@ const Avatar: React.FC<AvatarProps> = ({
|
|||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
export default Avatar;
|
export default Avatar;
|
||||||
|
|||||||
@@ -20,42 +20,49 @@ interface AvatarGroupProps {
|
|||||||
onClick?: (e: React.MouseEvent) => void;
|
onClick?: (e: React.MouseEvent) => void;
|
||||||
}
|
}
|
||||||
|
|
||||||
const AvatarGroup: React.FC<AvatarGroupProps> = ({
|
const AvatarGroup: React.FC<AvatarGroupProps> = ({
|
||||||
members,
|
members,
|
||||||
maxCount,
|
maxCount,
|
||||||
size = 28,
|
size = 28,
|
||||||
isDarkMode = false,
|
isDarkMode = false,
|
||||||
className = '',
|
className = '',
|
||||||
onClick
|
onClick,
|
||||||
}) => {
|
}) => {
|
||||||
const stopPropagation = useCallback((e: React.MouseEvent) => {
|
const stopPropagation = useCallback(
|
||||||
e.stopPropagation();
|
(e: React.MouseEvent) => {
|
||||||
onClick?.(e);
|
e.stopPropagation();
|
||||||
}, [onClick]);
|
onClick?.(e);
|
||||||
|
},
|
||||||
|
[onClick]
|
||||||
|
);
|
||||||
|
|
||||||
const renderAvatar = useCallback((member: Member, index: number) => {
|
const renderAvatar = useCallback(
|
||||||
const memberName = member.end && member.names ? member.names.join(', ') : member.name || '';
|
(member: Member, index: number) => {
|
||||||
const displayName = member.end && member.names ? member.name : member.name?.charAt(0).toUpperCase();
|
const memberName = member.end && member.names ? member.names.join(', ') : member.name || '';
|
||||||
|
const displayName =
|
||||||
return (
|
member.end && member.names ? member.name : member.name?.charAt(0).toUpperCase();
|
||||||
<Tooltip
|
|
||||||
key={member.team_member_id || member.id || index}
|
return (
|
||||||
title={memberName}
|
<Tooltip
|
||||||
isDarkMode={isDarkMode}
|
key={member.team_member_id || member.id || index}
|
||||||
>
|
title={memberName}
|
||||||
<Avatar
|
|
||||||
name={member.name || ''}
|
|
||||||
src={member.avatar_url}
|
|
||||||
size={size}
|
|
||||||
isDarkMode={isDarkMode}
|
isDarkMode={isDarkMode}
|
||||||
backgroundColor={member.color_code}
|
>
|
||||||
onClick={stopPropagation}
|
<Avatar
|
||||||
className="border-2 border-white"
|
name={member.name || ''}
|
||||||
style={isDarkMode ? { borderColor: '#374151' } : {}}
|
src={member.avatar_url}
|
||||||
/>
|
size={size}
|
||||||
</Tooltip>
|
isDarkMode={isDarkMode}
|
||||||
);
|
backgroundColor={member.color_code}
|
||||||
}, [stopPropagation, size, isDarkMode]);
|
onClick={stopPropagation}
|
||||||
|
className="border-2 border-white"
|
||||||
|
style={isDarkMode ? { borderColor: '#374151' } : {}}
|
||||||
|
/>
|
||||||
|
</Tooltip>
|
||||||
|
);
|
||||||
|
},
|
||||||
|
[stopPropagation, size, isDarkMode]
|
||||||
|
);
|
||||||
|
|
||||||
const visibleMembers = useMemo(() => {
|
const visibleMembers = useMemo(() => {
|
||||||
return maxCount ? members.slice(0, maxCount) : members;
|
return maxCount ? members.slice(0, maxCount) : members;
|
||||||
@@ -73,13 +80,13 @@ const AvatarGroup: React.FC<AvatarGroupProps> = ({
|
|||||||
if (typeof size === 'number') {
|
if (typeof size === 'number') {
|
||||||
return { width: size, height: size, fontSize: `${size * 0.4}px` };
|
return { width: size, height: size, fontSize: `${size * 0.4}px` };
|
||||||
}
|
}
|
||||||
|
|
||||||
const sizeMap = {
|
const sizeMap = {
|
||||||
small: { width: 24, height: 24, fontSize: '10px' },
|
small: { width: 24, height: 24, fontSize: '10px' },
|
||||||
default: { width: 32, height: 32, fontSize: '14px' },
|
default: { width: 32, height: 32, fontSize: '14px' },
|
||||||
large: { width: 48, height: 48, fontSize: '18px' }
|
large: { width: 48, height: 48, fontSize: '18px' },
|
||||||
};
|
};
|
||||||
|
|
||||||
return sizeMap[size];
|
return sizeMap[size];
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -87,15 +94,10 @@ const AvatarGroup: React.FC<AvatarGroupProps> = ({
|
|||||||
<div onClick={stopPropagation} className={`flex -space-x-1 ${className}`}>
|
<div onClick={stopPropagation} className={`flex -space-x-1 ${className}`}>
|
||||||
{avatarElements}
|
{avatarElements}
|
||||||
{remainingCount > 0 && (
|
{remainingCount > 0 && (
|
||||||
<Tooltip
|
<Tooltip title={`${remainingCount} more`} isDarkMode={isDarkMode}>
|
||||||
title={`${remainingCount} more`}
|
<div
|
||||||
isDarkMode={isDarkMode}
|
|
||||||
>
|
|
||||||
<div
|
|
||||||
className={`rounded-full flex items-center justify-center text-white font-medium shadow-sm border-2 cursor-pointer ${
|
className={`rounded-full flex items-center justify-center text-white font-medium shadow-sm border-2 cursor-pointer ${
|
||||||
isDarkMode
|
isDarkMode ? 'bg-gray-600 border-gray-700' : 'bg-gray-400 border-white'
|
||||||
? 'bg-gray-600 border-gray-700'
|
|
||||||
: 'bg-gray-400 border-white'
|
|
||||||
}`}
|
}`}
|
||||||
style={getSizeStyle()}
|
style={getSizeStyle()}
|
||||||
onClick={stopPropagation}
|
onClick={stopPropagation}
|
||||||
@@ -108,4 +110,4 @@ const AvatarGroup: React.FC<AvatarGroupProps> = ({
|
|||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
export default AvatarGroup;
|
export default AvatarGroup;
|
||||||
|
|||||||
@@ -12,25 +12,25 @@ interface ButtonProps {
|
|||||||
type?: 'button' | 'submit' | 'reset';
|
type?: 'button' | 'submit' | 'reset';
|
||||||
}
|
}
|
||||||
|
|
||||||
const Button: React.FC<ButtonProps & React.ButtonHTMLAttributes<HTMLButtonElement>> = ({
|
const Button: React.FC<ButtonProps & React.ButtonHTMLAttributes<HTMLButtonElement>> = ({
|
||||||
children,
|
children,
|
||||||
onClick,
|
onClick,
|
||||||
variant = 'default',
|
variant = 'default',
|
||||||
size = 'default',
|
size = 'default',
|
||||||
className = '',
|
className = '',
|
||||||
icon,
|
icon,
|
||||||
isDarkMode = false,
|
isDarkMode = false,
|
||||||
disabled = false,
|
disabled = false,
|
||||||
type = 'button',
|
type = 'button',
|
||||||
...props
|
...props
|
||||||
}) => {
|
}) => {
|
||||||
const baseClasses = `inline-flex items-center justify-center font-medium transition-colors duration-200 focus:outline-none focus:ring-2 ${isDarkMode ? 'focus:ring-blue-400' : 'focus:ring-blue-500'} focus:ring-offset-2 ${disabled ? 'opacity-50 cursor-not-allowed' : 'cursor-pointer'}`;
|
const baseClasses = `inline-flex items-center justify-center font-medium transition-colors duration-200 focus:outline-none focus:ring-2 ${isDarkMode ? 'focus:ring-blue-400' : 'focus:ring-blue-500'} focus:ring-offset-2 ${disabled ? 'opacity-50 cursor-not-allowed' : 'cursor-pointer'}`;
|
||||||
|
|
||||||
const variantClasses = {
|
const variantClasses = {
|
||||||
text: isDarkMode
|
text: isDarkMode
|
||||||
? 'text-gray-400 hover:text-gray-200 hover:bg-gray-700/50'
|
? 'text-gray-400 hover:text-gray-200 hover:bg-gray-700/50'
|
||||||
: 'text-gray-600 hover:text-gray-800 hover:bg-gray-100',
|
: 'text-gray-600 hover:text-gray-800 hover:bg-gray-100',
|
||||||
default: isDarkMode
|
default: isDarkMode
|
||||||
? 'bg-gray-800 border border-gray-600 text-gray-200 hover:bg-gray-700'
|
? 'bg-gray-800 border border-gray-600 text-gray-200 hover:bg-gray-700'
|
||||||
: 'bg-white border border-gray-300 text-gray-700 hover:bg-gray-50',
|
: 'bg-white border border-gray-300 text-gray-700 hover:bg-gray-50',
|
||||||
primary: isDarkMode
|
primary: isDarkMode
|
||||||
@@ -38,15 +38,15 @@ const Button: React.FC<ButtonProps & React.ButtonHTMLAttributes<HTMLButtonElemen
|
|||||||
: 'bg-blue-500 text-white hover:bg-blue-600',
|
: 'bg-blue-500 text-white hover:bg-blue-600',
|
||||||
danger: isDarkMode
|
danger: isDarkMode
|
||||||
? 'bg-red-600 text-white hover:bg-red-700'
|
? 'bg-red-600 text-white hover:bg-red-700'
|
||||||
: 'bg-red-500 text-white hover:bg-red-600'
|
: 'bg-red-500 text-white hover:bg-red-600',
|
||||||
};
|
};
|
||||||
|
|
||||||
const sizeClasses = {
|
const sizeClasses = {
|
||||||
small: 'px-2 py-1 text-xs rounded-sm',
|
small: 'px-2 py-1 text-xs rounded-sm',
|
||||||
default: 'px-3 py-2 text-sm rounded-md',
|
default: 'px-3 py-2 text-sm rounded-md',
|
||||||
large: 'px-4 py-3 text-base rounded-lg'
|
large: 'px-4 py-3 text-base rounded-lg',
|
||||||
};
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<button
|
<button
|
||||||
type={type}
|
type={type}
|
||||||
@@ -55,10 +55,10 @@ const Button: React.FC<ButtonProps & React.ButtonHTMLAttributes<HTMLButtonElemen
|
|||||||
className={`${baseClasses} ${variantClasses[variant]} ${sizeClasses[size]} ${className}`}
|
className={`${baseClasses} ${variantClasses[variant]} ${sizeClasses[size]} ${className}`}
|
||||||
{...props}
|
{...props}
|
||||||
>
|
>
|
||||||
{icon && <span className={children ? "mr-1" : ""}>{icon}</span>}
|
{icon && <span className={children ? 'mr-1' : ''}>{icon}</span>}
|
||||||
{children}
|
{children}
|
||||||
</button>
|
</button>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
export default Button;
|
export default Button;
|
||||||
|
|||||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user