@imj_media/tareas 0.0.7
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.env.template +8 -0
- package/.storybook/main.ts +26 -0
- package/.storybook/preview.ts +15 -0
- package/LICENSE.md +21 -0
- package/README.md +50 -0
- package/core/actions/get_all_users.action.ts +16 -0
- package/core/actions/get_salesman_response.action.ts +16 -0
- package/core/actions/get_tasks_project.action.ts +62 -0
- package/core/actions/get_tasks_response.action.ts +56 -0
- package/eslint.config.js +28 -0
- package/global.d.ts +3 -0
- package/infraestructure/interfaces/salesmans-obp-response.ts +43 -0
- package/infraestructure/interfaces/salesmans-obp.ts +4 -0
- package/infraestructure/interfaces/tasks-campania-response.ts +118 -0
- package/infraestructure/interfaces/tasks-campania.ts +26 -0
- package/infraestructure/interfaces/tasks-kanban-general.ts +25 -0
- package/infraestructure/interfaces/tasks-reponse.ts +111 -0
- package/infraestructure/interfaces/teams-response.ts +7 -0
- package/infraestructure/interfaces/teams.ts +5 -0
- package/infraestructure/interfaces/users-obp-response.ts +52 -0
- package/infraestructure/interfaces/users.ts +5 -0
- package/infraestructure/mappers/all-users-obp.ts +12 -0
- package/infraestructure/mappers/campaign-tasks.ts +35 -0
- package/infraestructure/mappers/kanban-tasks.ts +35 -0
- package/infraestructure/mappers/salesmans-obp.ts +11 -0
- package/infraestructure/mappers/teams.ts +12 -0
- package/package.json +61 -0
- package/postcss.config.js +6 -0
- package/src/components/atoms/Avatar.tsx +14 -0
- package/src/components/atoms/Comment.tsx +33 -0
- package/src/components/atoms/InputSearch.tsx +40 -0
- package/src/components/atoms/SkeletonCard.tsx +17 -0
- package/src/components/atoms/TabButton.tsx +16 -0
- package/src/components/atoms/TooltipUser.tsx +26 -0
- package/src/components/atoms/index.ts +2 -0
- package/src/components/index.ts +3 -0
- package/src/components/kanban-campania/DoneBoard.tsx +12 -0
- package/src/components/kanban-campania/KanbanCampania.tsx +39 -0
- package/src/components/kanban-campania/ToDoBoard.tsx +12 -0
- package/src/components/kanban-campania/WorkingBoard.tsx +13 -0
- package/src/components/kanban-campania/filters.ts +46 -0
- package/src/components/kanban-campania/index.ts +3 -0
- package/src/components/kanban-general/DoneBoard.tsx +12 -0
- package/src/components/kanban-general/KanbanGeneral.tsx +40 -0
- package/src/components/kanban-general/ToDoBoard.tsx +12 -0
- package/src/components/kanban-general/WorkingBoard.tsx +13 -0
- package/src/components/kanban-general/filters.ts +58 -0
- package/src/components/kanban-general/index.ts +3 -0
- package/src/components/layout/FilterButton.tsx +50 -0
- package/src/components/layout/FilterContent.tsx +70 -0
- package/src/components/layout/IndexComponents.tsx +32 -0
- package/src/components/lista-campania/ChildTask.tsx +22 -0
- package/src/components/lista-campania/Date.tsx +30 -0
- package/src/components/lista-campania/ListaCampania.tsx +21 -0
- package/src/components/lista-campania/ParentTask.tsx +57 -0
- package/src/components/molecules/AllComments.tsx +78 -0
- package/src/components/molecules/ButtonAssignUsers.tsx +175 -0
- package/src/components/molecules/DependentTasks.tsx +64 -0
- package/src/components/molecules/Tabs.tsx +39 -0
- package/src/components/molecules/index.ts +1 -0
- package/src/components/organisms/Board.tsx +87 -0
- package/src/components/organisms/Checkbox.tsx +45 -0
- package/src/components/organisms/DetailsTask.tsx +286 -0
- package/src/components/organisms/TabDetailsTask.tsx +39 -0
- package/src/components/organisms/Task.tsx +176 -0
- package/src/components/organisms/index.ts +2 -0
- package/src/components/tasks/PriorityButton.tsx +79 -0
- package/src/components/templates/Layout.tsx +84 -0
- package/src/components/templates/TableList/components/TableList.scss +270 -0
- package/src/components/templates/TableList/components/TableList.tsx +239 -0
- package/src/components/templates/TableList/components/index.tsx +1 -0
- package/src/constants/colors.ts +64 -0
- package/src/constants/gaps.ts +8 -0
- package/src/constants/paddings.ts +8 -0
- package/src/constants/shadows.ts +5 -0
- package/src/context/filtersLayout.context.tsx +118 -0
- package/src/context/kanbanCampania.context.tsx +50 -0
- package/src/context/useApis.context.tsx +47 -0
- package/src/context/userLog.context.tsx +50 -0
- package/src/env.d.ts +10 -0
- package/src/functions/taskCalculations.tsx +818 -0
- package/src/hooks/useAllUsers.ts +18 -0
- package/src/hooks/useCheckTask.tsx +15 -0
- package/src/hooks/useComerciales.ts +58 -0
- package/src/hooks/useDoneTasks.ts +90 -0
- package/src/hooks/useElementPosition.ts +34 -0
- package/src/hooks/useFunctionsTasks.ts +57 -0
- package/src/hooks/useNormalizedData.ts +36 -0
- package/src/hooks/useTeams.ts +19 -0
- package/src/hooks/useToDoTasks.ts +89 -0
- package/src/hooks/useWorkingTasks.ts +85 -0
- package/src/index.css +55 -0
- package/src/index.ts +2 -0
- package/src/index.tsx +1 -0
- package/src/pages/App.tsx +42 -0
- package/src/pages/NoAccessToken.tsx +20 -0
- package/src/pages/NoUser.tsx +20 -0
- package/src/stories/AppTasks.stories.tsx +36 -0
- package/src/stories/Table.stories.tsx +160 -0
- package/src/types/index.ts +107 -0
- package/src/types/interfaces.ts +67 -0
- package/src/types/layout.types.ts +30 -0
- package/src/utils/filters.functions.ts +17 -0
- package/src/utils/formats.ts +33 -0
- package/src/utils/functionsStorybook.tsx +0 -0
- package/src/utils/inputs.functions.ts +25 -0
- package/src/utils/tanstack.functions.ts +19 -0
- package/src/utils/utils.ts +12 -0
- package/src/vite-env.d.ts +1 -0
- package/tailwind.config.js +31 -0
- package/tsconfig.app.json +26 -0
- package/tsconfig.json +7 -0
- package/tsconfig.node.json +24 -0
- package/vite.config.ts +16 -0
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
export const filtersToShow = [
|
|
2
|
+
{
|
|
3
|
+
label: "Tipo de dependencia",
|
|
4
|
+
type: "select",
|
|
5
|
+
id: "dependency",
|
|
6
|
+
options: [{
|
|
7
|
+
id:"con",
|
|
8
|
+
name: "Con dependencia"
|
|
9
|
+
},{
|
|
10
|
+
id:"sin",
|
|
11
|
+
name: "Sin dependencia"
|
|
12
|
+
}
|
|
13
|
+
]
|
|
14
|
+
},
|
|
15
|
+
{
|
|
16
|
+
label: "Por proyecto",
|
|
17
|
+
type: "select",
|
|
18
|
+
id: "project.id",
|
|
19
|
+
options: []
|
|
20
|
+
},
|
|
21
|
+
{
|
|
22
|
+
label: "Por responsable",
|
|
23
|
+
type: "select",
|
|
24
|
+
id: "owner.config",
|
|
25
|
+
options: [
|
|
26
|
+
{id:"yo", name:"Mis tareas"},
|
|
27
|
+
{id:"yo_y_mis_equipos", name:"Tareas mías y de mis equipos"},
|
|
28
|
+
{id:"mis_equipos", name:"Tareas de mis equipos"}
|
|
29
|
+
]
|
|
30
|
+
},
|
|
31
|
+
{
|
|
32
|
+
label: "Por ejecutivo",
|
|
33
|
+
type: "select",
|
|
34
|
+
id: "salesman.name",
|
|
35
|
+
options: []
|
|
36
|
+
},
|
|
37
|
+
{
|
|
38
|
+
label: "Por dificultad",
|
|
39
|
+
type: "select",
|
|
40
|
+
id: "difficulty",
|
|
41
|
+
options: [{
|
|
42
|
+
id:0,
|
|
43
|
+
name: "Muy facil"
|
|
44
|
+
},{
|
|
45
|
+
id:1,
|
|
46
|
+
name: "Facil"
|
|
47
|
+
},{
|
|
48
|
+
id:2,
|
|
49
|
+
name: "Media"
|
|
50
|
+
},{
|
|
51
|
+
id:3,
|
|
52
|
+
name: "Dificil"
|
|
53
|
+
},{
|
|
54
|
+
id:4,
|
|
55
|
+
name: "Muy dificil"
|
|
56
|
+
}]
|
|
57
|
+
}
|
|
58
|
+
]
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
import { Icons } from "@imj_media/tasks-modules"
|
|
2
|
+
import { COLORS } from "../../constants/colors"
|
|
3
|
+
import ReactDOM from "react-dom";
|
|
4
|
+
import useElementPosition from "../../hooks/useElementPosition";
|
|
5
|
+
import { useEffect, useRef, useState } from "react";
|
|
6
|
+
|
|
7
|
+
const FilterButton = ({children, filterChildren}:{children:React.ReactNode, filterChildren:React.MutableRefObject<HTMLDivElement>}) => {
|
|
8
|
+
const {position, setElementPosition} = useElementPosition();
|
|
9
|
+
const ref = useRef<HTMLElement>(null);
|
|
10
|
+
|
|
11
|
+
const FiltersModal = ({children}:{children:React.ReactNode})=>{
|
|
12
|
+
const [left, setLeft] = useState<number>();
|
|
13
|
+
|
|
14
|
+
useEffect(()=>{
|
|
15
|
+
if(filterChildren.current){
|
|
16
|
+
setLeft((position?.x ?? 0 ) - (filterChildren.current?.getBoundingClientRect()?.width ?? 0));
|
|
17
|
+
}
|
|
18
|
+
},[filterChildren?.current, position?.x])
|
|
19
|
+
|
|
20
|
+
return position && ReactDOM.createPortal(
|
|
21
|
+
<div className={`absolute w-fit h-fit ${!left ? "hidden" :"block"}`} style={{top: `${position?.y}px`, left: `${left}px`}}>
|
|
22
|
+
{children}
|
|
23
|
+
</div>,
|
|
24
|
+
document.body
|
|
25
|
+
)
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
return (
|
|
29
|
+
<>
|
|
30
|
+
<label
|
|
31
|
+
ref={ref as React.LegacyRef<HTMLLabelElement>}
|
|
32
|
+
htmlFor="filter-button"
|
|
33
|
+
className="flex cursor-pointer items-center gap-m p-s text-texts-enfasis fill-texts-enfasis bg-bg rounded-lg px-l h-[30px] shadow-button-primary hover:shadow-button-primary-hover "
|
|
34
|
+
>
|
|
35
|
+
<Icons icon="filter" size="xs" strokeWidth={5} color={COLORS.primary.regular}/>
|
|
36
|
+
Filtrar
|
|
37
|
+
</label>
|
|
38
|
+
{/* this button is used to trigger the filter modal */}
|
|
39
|
+
<button className="hidden" id="filter-button" onClick={()=>{
|
|
40
|
+
setElementPosition(ref as React.MutableRefObject<HTMLElement>)
|
|
41
|
+
}}/>
|
|
42
|
+
|
|
43
|
+
<FiltersModal>
|
|
44
|
+
{children}
|
|
45
|
+
</FiltersModal>
|
|
46
|
+
</>
|
|
47
|
+
)
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
export default FilterButton
|
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
import { useFiltersLayoutContext, IFieldFilter } from '../../context/filtersLayout.context';
|
|
2
|
+
import { debounce } from '../../utils/inputs.functions';
|
|
3
|
+
|
|
4
|
+
const FilterContent = () => {
|
|
5
|
+
const {
|
|
6
|
+
fields,
|
|
7
|
+
updateFilters,
|
|
8
|
+
cleanFilters,
|
|
9
|
+
applyFilters,
|
|
10
|
+
initialFilters
|
|
11
|
+
} = useFiltersLayoutContext();
|
|
12
|
+
|
|
13
|
+
return (
|
|
14
|
+
<div className='flex flex-col gap-l min-w-[250px]'>
|
|
15
|
+
<div className='flex justify-between items-center'>
|
|
16
|
+
<p className='text-texts text-m'>Filtros</p>
|
|
17
|
+
<button className='text-xs font-semibold flex cursor-pointer items-center gap-m p-s text-texts-enfasis fill-texts-enfasis bg-bg rounded-lg px-l h-fit shadow-button-primary hover:shadow-button-primary-hover ' onClick={cleanFilters}>Limpiar</button>
|
|
18
|
+
</div>
|
|
19
|
+
<div className='flex flex-col gap-m'>
|
|
20
|
+
{fields?.map((field: IFieldFilter, index: number) => {
|
|
21
|
+
return (
|
|
22
|
+
<div key={index}>
|
|
23
|
+
<p className='text-sm'>{field.label}</p>
|
|
24
|
+
{field?.type === "select" ? (
|
|
25
|
+
<select
|
|
26
|
+
onChange={(e)=>
|
|
27
|
+
updateFilters({
|
|
28
|
+
name: e?.target?.name,
|
|
29
|
+
value: e?.target?.value
|
|
30
|
+
})
|
|
31
|
+
}
|
|
32
|
+
value={initialFilters?.[field?.id]}
|
|
33
|
+
className='w-full h-[30px] shadow-input rounded-lg p-s'
|
|
34
|
+
name={field.id}
|
|
35
|
+
id={field.id}
|
|
36
|
+
>
|
|
37
|
+
<option key={index} value={""}>Selecciona una opción</option>
|
|
38
|
+
{field?.options?.map((option: {id: string | number, name: string}, index: number)=>(
|
|
39
|
+
<option
|
|
40
|
+
key={index}
|
|
41
|
+
value={option.id}
|
|
42
|
+
>
|
|
43
|
+
{option.name}
|
|
44
|
+
</option>
|
|
45
|
+
))}
|
|
46
|
+
</select>
|
|
47
|
+
): (
|
|
48
|
+
<input
|
|
49
|
+
className='w-full h-[30px] shadow-input rounded-lg p-s'
|
|
50
|
+
type={field.type}
|
|
51
|
+
value={initialFilters?.[field?.id]}
|
|
52
|
+
name={field.label.toLowerCase().replace(/ /g, "_")}
|
|
53
|
+
onChange={debounce((e:React.ChangeEvent<HTMLInputElement>)=>updateFilters({
|
|
54
|
+
name: e?.target?.name,
|
|
55
|
+
value: e?.target?.value
|
|
56
|
+
}), 1000)}
|
|
57
|
+
/>
|
|
58
|
+
)}
|
|
59
|
+
|
|
60
|
+
|
|
61
|
+
</div>
|
|
62
|
+
)
|
|
63
|
+
})}
|
|
64
|
+
</div>
|
|
65
|
+
<button className='btn-primary rounded-md py-s' onClick={applyFilters}>Filtrar</button>
|
|
66
|
+
</div>
|
|
67
|
+
)
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
export default FilterContent
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
import { InfoCampaniaProvider } from "../../context/kanbanCampania.context";
|
|
2
|
+
import { IComponents, IIndexComponents, IViews } from "../../types/layout.types";
|
|
3
|
+
import KanbanCampania from "../kanban-campania/KanbanCampania";
|
|
4
|
+
import KanbanGeneral from "../kanban-general/KanbanGeneral";
|
|
5
|
+
import ListaCampania from "../lista-campania/ListaCampania";
|
|
6
|
+
|
|
7
|
+
const IndexComponents = ({path, tab}: IIndexComponents) => {
|
|
8
|
+
const components: IComponents = {
|
|
9
|
+
"kanban-general": { "kanban": <KanbanGeneral/> },
|
|
10
|
+
"lista-campanias": {
|
|
11
|
+
"lista": <div>lista-campanias</div>,
|
|
12
|
+
"gantt": <div>gantt-campanias</div>
|
|
13
|
+
},
|
|
14
|
+
"kanban-campania": {
|
|
15
|
+
"kanban": <KanbanCampania />,
|
|
16
|
+
"lista": <ListaCampania />,
|
|
17
|
+
"gantt": <div>kanban-campania-gantt</div>
|
|
18
|
+
}
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
if(path === "kanban-campania"){
|
|
22
|
+
return (
|
|
23
|
+
<InfoCampaniaProvider project={1839}>
|
|
24
|
+
{ (components[path] as IViews)[tab as keyof IViews] as React.ReactNode}
|
|
25
|
+
</InfoCampaniaProvider>
|
|
26
|
+
)
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
return (components[path] as IViews)[tab as keyof IViews] as React.ReactNode
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
export default IndexComponents
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
import { ITasksCampaign } from "../../../infraestructure/interfaces/tasks-campania"
|
|
2
|
+
|
|
3
|
+
interface ChildTaskProps extends ITasksCampaign {
|
|
4
|
+
|
|
5
|
+
}
|
|
6
|
+
|
|
7
|
+
const ChildTask = ({
|
|
8
|
+
id,
|
|
9
|
+
task,
|
|
10
|
+
priority,
|
|
11
|
+
startDate,
|
|
12
|
+
endDate,
|
|
13
|
+
status
|
|
14
|
+
}: ChildTaskProps) => {
|
|
15
|
+
return (
|
|
16
|
+
<div className="ml-[30px] mb-3 border-b border-gray-200 pb-3">
|
|
17
|
+
{task}
|
|
18
|
+
</div>
|
|
19
|
+
)
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
export default ChildTask
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
import { Icons } from "@imj_media/tasks-modules"
|
|
2
|
+
import { formatDate } from "../../utils/formats"
|
|
3
|
+
import { COLORS } from "../../constants/colors"
|
|
4
|
+
|
|
5
|
+
interface DateViewProps {
|
|
6
|
+
startDate: Date,
|
|
7
|
+
endDate: Date
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
const DateView = ({
|
|
11
|
+
startDate,
|
|
12
|
+
endDate
|
|
13
|
+
}: DateViewProps) => {
|
|
14
|
+
|
|
15
|
+
return (
|
|
16
|
+
<>
|
|
17
|
+
<div className="gap-s bg-gray-100 rounded-md h-[30px] w-fit flex justify-center items-center px-xxxl">
|
|
18
|
+
<Icons icon="calendar" size="xs" strokeWidth={4} color={COLORS.texts.subtext}/>
|
|
19
|
+
<p className="text-sm"> {formatDate(startDate)}</p>
|
|
20
|
+
</div>
|
|
21
|
+
<Icons icon="arrow_right" size="xs" strokeWidth={4} color={COLORS.texts.DEFAULT}/>
|
|
22
|
+
<div className="gap-s bg-gray-100 rounded-md h-[30px] w-fit flex justify-center items-center px-xxxl">
|
|
23
|
+
<Icons icon="calendar" size="xs" strokeWidth={4} color={COLORS.texts.subtext}/>
|
|
24
|
+
<p className="text-sm"> {formatDate(endDate)}</p>
|
|
25
|
+
</div>
|
|
26
|
+
</>
|
|
27
|
+
)
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
export default DateView
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
import { useInfoCampania } from "../../context/kanbanCampania.context";
|
|
2
|
+
import ParentTask from "./ParentTask";
|
|
3
|
+
|
|
4
|
+
const ListaCampania = () => {
|
|
5
|
+
const {tasksProject} = useInfoCampania();
|
|
6
|
+
let parentTasks = [...tasksProject?.data?.filter((task: any) => task.tasks.length > 0)];
|
|
7
|
+
|
|
8
|
+
return (
|
|
9
|
+
<div className="flex flex-col gap-m">
|
|
10
|
+
{parentTasks.map((task: any) => {
|
|
11
|
+
return (
|
|
12
|
+
<div className="flex flex-col gap-m border-b border-gray-200 pb-m">
|
|
13
|
+
<ParentTask id={task.id} />
|
|
14
|
+
</div>
|
|
15
|
+
)
|
|
16
|
+
})}
|
|
17
|
+
</div>
|
|
18
|
+
)
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
export default ListaCampania
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
import { Icons, Tooltip } from "@imj_media/tasks-modules";
|
|
2
|
+
import { COLORS } from "../../constants/colors";
|
|
3
|
+
import PriorityButton from "../tasks/PriorityButton";
|
|
4
|
+
import { useInfoCampania } from "../../context/kanbanCampania.context";
|
|
5
|
+
import DateView from "./Date";
|
|
6
|
+
import ChildTask from "./ChildTask";
|
|
7
|
+
|
|
8
|
+
const ParentTask = ({ id }: { id: number }) => {
|
|
9
|
+
const { tasksProject } = useInfoCampania();
|
|
10
|
+
const task = tasksProject?.data?.find((task: any) => task.id === id);
|
|
11
|
+
|
|
12
|
+
return (
|
|
13
|
+
<div className="flex flex-col gap-m ">
|
|
14
|
+
<div className="flex items-center gap-m flex-wrap hover:bg-gray-100">
|
|
15
|
+
<button className="-rotate-90">
|
|
16
|
+
<Icons icon="angle_down_outline" color={COLORS.texts.subtext}/>
|
|
17
|
+
</button>
|
|
18
|
+
<div className="flex items-center justify-center min-w-[20px] min-h-[20px] w-fit h-[20px] mx-1 bg-primary-medium rounded-full">
|
|
19
|
+
<p className="text-sm text-primary">
|
|
20
|
+
{task?.tasks?.length === 0 ? 1 : task?.tasks?.length}
|
|
21
|
+
</p>
|
|
22
|
+
</div>
|
|
23
|
+
<Tooltip
|
|
24
|
+
dispatch={
|
|
25
|
+
<p className="text-sm text-texts-subtext max-w-[400px] truncate">{task?.task}</p>
|
|
26
|
+
}
|
|
27
|
+
>
|
|
28
|
+
<p className="text-sm text-texts-subtext p-2 max-w-[400px]">{task?.task}</p>
|
|
29
|
+
</Tooltip>
|
|
30
|
+
<div className="gap-s bg-gray-100 rounded-md h-[30px] w-[30px] flex justify-center items-center">
|
|
31
|
+
<Icons icon="align_center" size="xs" strokeWidth={4} color={COLORS.texts.subtext}/>
|
|
32
|
+
</div>
|
|
33
|
+
<div className="gap-s bg-success-medium border border-success-pastel rounded-md h-[30px] w-[30px] flex justify-center items-center">
|
|
34
|
+
<PriorityButton id={id} priority={task?.priority}/>
|
|
35
|
+
</div>
|
|
36
|
+
<div className="gap-s bg-danger-light border border-danger-regular rounded-md h-[30px] w-[30px] flex justify-center items-center">
|
|
37
|
+
<Icons icon="at" size="xs" strokeWidth={4} color={COLORS.danger.regular}/>
|
|
38
|
+
</div>
|
|
39
|
+
<DateView startDate={task?.startDate} endDate={task?.endDate}/>
|
|
40
|
+
<div className="gap-s bg-gray-100 rounded-md h-[30px] flex justify-center items-center w-[30px]">
|
|
41
|
+
<Icons icon="trash" size="xs" strokeWidth={5} color={COLORS.texts.subtext}/>
|
|
42
|
+
</div>
|
|
43
|
+
</div>
|
|
44
|
+
<div className="ml-[30px]">
|
|
45
|
+
{task?.tasks?.length > 0 ? (
|
|
46
|
+
task?.tasks?.map((childTask: any) => (
|
|
47
|
+
<ParentTask id={childTask?.id} />
|
|
48
|
+
))
|
|
49
|
+
) : (
|
|
50
|
+
<ChildTask {...task}/>
|
|
51
|
+
)}
|
|
52
|
+
</div>
|
|
53
|
+
</div>
|
|
54
|
+
)
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
export default ParentTask
|
|
@@ -0,0 +1,78 @@
|
|
|
1
|
+
import { CommentsProps } from '../../types';
|
|
2
|
+
import Comment from '../atoms/Comment';
|
|
3
|
+
import { getInitials, URL_API } from '../../utils/utils';
|
|
4
|
+
import { useForm } from 'react-hook-form';
|
|
5
|
+
import { useApis } from '../../context/useApis.context';
|
|
6
|
+
import { useState, useEffect } from 'react';
|
|
7
|
+
|
|
8
|
+
// @ts-ignore
|
|
9
|
+
import { Avatar, AvatarFallback, AvatarImage, Input } from '@imj_media/imj-ui';
|
|
10
|
+
|
|
11
|
+
export default function AllComments({ currentUser, data, taskId, onNewComment }: CommentsProps) {
|
|
12
|
+
const { tasks_api } = useApis();
|
|
13
|
+
const { register, handleSubmit, reset } = useForm();
|
|
14
|
+
const [comments, setComments] = useState<any[]>([]);
|
|
15
|
+
|
|
16
|
+
useEffect(() => {
|
|
17
|
+
if (data?.comentarios) {
|
|
18
|
+
const sortedComments = [...data.comentarios].sort(
|
|
19
|
+
(a, b) => new Date(b.createdAt).getTime() - new Date(a.createdAt).getTime()
|
|
20
|
+
);
|
|
21
|
+
setComments(sortedComments);
|
|
22
|
+
}
|
|
23
|
+
}, [data]);
|
|
24
|
+
|
|
25
|
+
const onSubmit = async (comment: any) => {
|
|
26
|
+
try {
|
|
27
|
+
const newComment = {
|
|
28
|
+
autor: {
|
|
29
|
+
id: currentUser?.id,
|
|
30
|
+
email: currentUser?.email,
|
|
31
|
+
nombre: currentUser?.nombre,
|
|
32
|
+
imagen: currentUser?.imagen,
|
|
33
|
+
},
|
|
34
|
+
comentario: comment.comment,
|
|
35
|
+
createdAt: new Date().toISOString(),
|
|
36
|
+
};
|
|
37
|
+
|
|
38
|
+
await tasks_api.post(`/api/comentarios`, {
|
|
39
|
+
data: {
|
|
40
|
+
autor: currentUser?.id,
|
|
41
|
+
comentario: comment.comment,
|
|
42
|
+
tarea: taskId,
|
|
43
|
+
},
|
|
44
|
+
});
|
|
45
|
+
|
|
46
|
+
onNewComment(true);
|
|
47
|
+
setComments((prevComments) => [newComment, ...prevComments]);
|
|
48
|
+
reset();
|
|
49
|
+
} catch (error) {
|
|
50
|
+
console.error('Error al agregar comentario:', error);
|
|
51
|
+
}
|
|
52
|
+
};
|
|
53
|
+
|
|
54
|
+
return (
|
|
55
|
+
<div className="flex flex-col h-full gap-4">
|
|
56
|
+
<form onSubmit={handleSubmit(onSubmit)} className="flex flex-row items-center gap-3">
|
|
57
|
+
<Avatar className="w-8 h-8">
|
|
58
|
+
<AvatarImage
|
|
59
|
+
src={`${URL_API}${currentUser?.imagen.formats.thumbnail?.url}`}
|
|
60
|
+
alt="@shadcn"
|
|
61
|
+
/>
|
|
62
|
+
<AvatarFallback className="border border-x-primary-pastel">
|
|
63
|
+
{getInitials(currentUser?.nombre)}
|
|
64
|
+
</AvatarFallback>
|
|
65
|
+
</Avatar>
|
|
66
|
+
<Input
|
|
67
|
+
{...register('comment', { required: true })}
|
|
68
|
+
autoComplete="off"
|
|
69
|
+
placeholder="Escribe un comentario"
|
|
70
|
+
className="w-full bg-white border border-neutral-200"
|
|
71
|
+
/>
|
|
72
|
+
</form>
|
|
73
|
+
{comments.map((comment, index) => (
|
|
74
|
+
<Comment key={index} comment={comment} />
|
|
75
|
+
))}
|
|
76
|
+
</div>
|
|
77
|
+
);
|
|
78
|
+
}
|
|
@@ -0,0 +1,175 @@
|
|
|
1
|
+
import { useEffect, useRef, useState } from 'react';
|
|
2
|
+
import Avatar from '../atoms/Avatar';
|
|
3
|
+
import { Icons, Tooltip } from '@imj_media/tasks-modules';
|
|
4
|
+
import TooltipUser from '../atoms/TooltipUser';
|
|
5
|
+
import { IUsers } from '../../../infraestructure/interfaces/users';
|
|
6
|
+
import useElementPosition from '../../hooks/useElementPosition';
|
|
7
|
+
import ReactDOM from 'react-dom';
|
|
8
|
+
import InputSearch from '../atoms/InputSearch';
|
|
9
|
+
import useTeams from '../../hooks/useTeams';
|
|
10
|
+
import { ITeam } from '../../../infraestructure/interfaces/teams';
|
|
11
|
+
import useAllUsers from '../../hooks/useAllUsers';
|
|
12
|
+
import { AddOBPUrl } from '../../utils/formats';
|
|
13
|
+
|
|
14
|
+
const ButtonAssignUsers = ({
|
|
15
|
+
users,
|
|
16
|
+
onClick,
|
|
17
|
+
}: {
|
|
18
|
+
users: IUsers[] | [];
|
|
19
|
+
onClick: (option: any) => void;
|
|
20
|
+
}) => {
|
|
21
|
+
let buttonId = `button-assign-users-${Math.random().toString(36).substring(2, 15)}`;
|
|
22
|
+
const { setElementPosition, position, clearPosition } = useElementPosition();
|
|
23
|
+
const ref = useRef<HTMLParagraphElement>(null);
|
|
24
|
+
|
|
25
|
+
/**
|
|
26
|
+
* Get avatar more than three
|
|
27
|
+
* @returns a div with the number of users more than three
|
|
28
|
+
*/
|
|
29
|
+
const getAvatarMoreThanThree = () => {
|
|
30
|
+
//TODO: Implement the logic to show the users in the hover
|
|
31
|
+
return (
|
|
32
|
+
users.length > 3 && (
|
|
33
|
+
<Tooltip
|
|
34
|
+
dispatch={
|
|
35
|
+
<div className="w-[28px] h-[28px] rounded-full btn-primary flex items-center justify-center border-2 border-white ml-[-8px]">
|
|
36
|
+
<p className="text-xs font-bold text-white ">{`+${
|
|
37
|
+
users.length - 3
|
|
38
|
+
}`}</p>
|
|
39
|
+
</div>
|
|
40
|
+
}
|
|
41
|
+
>
|
|
42
|
+
<div className="bg-containers w-fit h-fit p-2 rounded-lg border-containers">
|
|
43
|
+
{users?.slice(3).map((user, index) => (
|
|
44
|
+
<div key={index} className="flex items-center gap-2 ">
|
|
45
|
+
<Avatar key={index} imageUrl={user.image ?? ''} alt={user.name} />
|
|
46
|
+
<p className="text-xs font-bold content-text">{user.name}</p>
|
|
47
|
+
</div>
|
|
48
|
+
))}
|
|
49
|
+
</div>
|
|
50
|
+
</Tooltip>
|
|
51
|
+
)
|
|
52
|
+
);
|
|
53
|
+
};
|
|
54
|
+
|
|
55
|
+
/**
|
|
56
|
+
* Map users
|
|
57
|
+
* @returns a div with the users
|
|
58
|
+
*/
|
|
59
|
+
const mapUsers = () =>
|
|
60
|
+
users?.length > 0 &&
|
|
61
|
+
users.slice(0, 3).map((user, index) => (
|
|
62
|
+
<div className={`${index === 0 ? '' : 'ml-[-8px]'}`}>
|
|
63
|
+
<TooltipUser
|
|
64
|
+
trigger={<Avatar key={index} imageUrl={user.image} alt={user.name} />}
|
|
65
|
+
user={user}
|
|
66
|
+
showUser={!position?.x ? true : false}
|
|
67
|
+
/>
|
|
68
|
+
</div>
|
|
69
|
+
));
|
|
70
|
+
|
|
71
|
+
/**
|
|
72
|
+
* Label to active the button
|
|
73
|
+
* @returns a label with the children
|
|
74
|
+
*/
|
|
75
|
+
const LabelToActiveTheButton = ({ children }: { children: React.ReactNode }) => (
|
|
76
|
+
<label ref={ref as any} htmlFor={buttonId} className="cursor-pointer flex items-center">
|
|
77
|
+
{children}
|
|
78
|
+
</label>
|
|
79
|
+
);
|
|
80
|
+
|
|
81
|
+
/**
|
|
82
|
+
* If the users length is 0, return a label with the icon
|
|
83
|
+
*/
|
|
84
|
+
if (users.length === 0) {
|
|
85
|
+
return (
|
|
86
|
+
<LabelToActiveTheButton>
|
|
87
|
+
<Icons icon="user" size="xs" strokeWidth={3} />
|
|
88
|
+
</LabelToActiveTheButton>
|
|
89
|
+
);
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
const UsersModal = () => {
|
|
93
|
+
const [left, setLeft] = useState<number>();
|
|
94
|
+
const [selected, setSelected] = useState<'responsible' | 'team'>('team');
|
|
95
|
+
const { teams } = useTeams();
|
|
96
|
+
const { users: allUsers } = useAllUsers();
|
|
97
|
+
|
|
98
|
+
useEffect(() => {
|
|
99
|
+
if (ref.current) {
|
|
100
|
+
setLeft((position?.x ?? 0) - (ref.current?.getBoundingClientRect()?.width ?? 0));
|
|
101
|
+
}
|
|
102
|
+
}, [ref?.current, position?.x]);
|
|
103
|
+
|
|
104
|
+
const Tabs = () => {
|
|
105
|
+
const tabs = [
|
|
106
|
+
{ id: 'team', label: 'Equipo' },
|
|
107
|
+
{ id: 'responsible', label: 'Responsable' },
|
|
108
|
+
];
|
|
109
|
+
return (
|
|
110
|
+
<div className="w-full flex justify-center gap-l text-texts-placeholder font-bold">
|
|
111
|
+
{tabs.map((tab, index) => (
|
|
112
|
+
<button
|
|
113
|
+
className={`w-full text-sm border-texts-placeholder text-center ${
|
|
114
|
+
selected === tab.id ? 'text-primary-pastel' : ''
|
|
115
|
+
} ${index === tabs.length - 1 ? 'border-r-0' : ' border-r-2'}`}
|
|
116
|
+
onClick={() => setSelected(tab.id as 'responsible' | 'team')}
|
|
117
|
+
>
|
|
118
|
+
{tab.label}
|
|
119
|
+
</button>
|
|
120
|
+
))}
|
|
121
|
+
</div>
|
|
122
|
+
);
|
|
123
|
+
};
|
|
124
|
+
|
|
125
|
+
return (
|
|
126
|
+
position?.x &&
|
|
127
|
+
ReactDOM.createPortal(
|
|
128
|
+
<div
|
|
129
|
+
className={`absolute w-[300px] h-fit ${!left ? 'hidden' : 'block'}`}
|
|
130
|
+
style={{ top: `${position?.y}px`, left: `${left}px` }}
|
|
131
|
+
>
|
|
132
|
+
<div className="flex flex-col gap-m bg-bg-card border-cards rounded-lg p-2 max-h-[200px] overflow-y-hidden">
|
|
133
|
+
<Tabs />
|
|
134
|
+
<InputSearch
|
|
135
|
+
options={
|
|
136
|
+
selected === 'team'
|
|
137
|
+
? teams.data.map((team: ITeam) => ({
|
|
138
|
+
id: team.id,
|
|
139
|
+
name: team.name,
|
|
140
|
+
image: '',
|
|
141
|
+
}))
|
|
142
|
+
: allUsers.data?.map(AddOBPUrl)
|
|
143
|
+
}
|
|
144
|
+
onselect={(option: any) => {
|
|
145
|
+
clearPosition();
|
|
146
|
+
if (onClick) onClick(option);
|
|
147
|
+
}}
|
|
148
|
+
/>
|
|
149
|
+
</div>
|
|
150
|
+
</div>,
|
|
151
|
+
document.body
|
|
152
|
+
)
|
|
153
|
+
);
|
|
154
|
+
};
|
|
155
|
+
return (
|
|
156
|
+
<>
|
|
157
|
+
<LabelToActiveTheButton>
|
|
158
|
+
{mapUsers()}
|
|
159
|
+
{getAvatarMoreThanThree()}
|
|
160
|
+
</LabelToActiveTheButton>
|
|
161
|
+
|
|
162
|
+
{/* Hidden button to active the modal */}
|
|
163
|
+
<button
|
|
164
|
+
className="hidden"
|
|
165
|
+
id={buttonId}
|
|
166
|
+
onClick={() => setElementPosition(ref as React.MutableRefObject<HTMLElement>)}
|
|
167
|
+
/>
|
|
168
|
+
|
|
169
|
+
{/* {getModalOfUsers()} */}
|
|
170
|
+
<UsersModal />
|
|
171
|
+
</>
|
|
172
|
+
);
|
|
173
|
+
};
|
|
174
|
+
|
|
175
|
+
export default ButtonAssignUsers;
|
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
import { format } from 'date-fns';
|
|
2
|
+
import { es } from 'date-fns/locale';
|
|
3
|
+
import { Avatar } from '../atoms';
|
|
4
|
+
// @ts-ignore
|
|
5
|
+
import { Label } from '@imj_media/imj-ui';
|
|
6
|
+
|
|
7
|
+
export default function DependentTasks({ data }: any) {
|
|
8
|
+
const statusMap: any = {
|
|
9
|
+
0: {
|
|
10
|
+
color: 'bg-gray-400',
|
|
11
|
+
label: 'Sin estado',
|
|
12
|
+
},
|
|
13
|
+
1: {
|
|
14
|
+
color: 'bg-primary-regular',
|
|
15
|
+
label: 'En curso',
|
|
16
|
+
},
|
|
17
|
+
2: {
|
|
18
|
+
color: 'bg-success-regular',
|
|
19
|
+
label: 'Finalizada',
|
|
20
|
+
},
|
|
21
|
+
3: {
|
|
22
|
+
color: 'bg-success-regular',
|
|
23
|
+
label: 'Finalizada',
|
|
24
|
+
},
|
|
25
|
+
};
|
|
26
|
+
|
|
27
|
+
const dateFormated = (date: any) => {
|
|
28
|
+
if (!date) return '';
|
|
29
|
+
return format(new Date(date), 'd MMM', { locale: es });
|
|
30
|
+
};
|
|
31
|
+
return (
|
|
32
|
+
<div>
|
|
33
|
+
<div className="flex flex-row items-center gap-4">
|
|
34
|
+
<Label className="text-lg font-medium text-neutral-900">Tareas dependientes</Label>
|
|
35
|
+
<span className="text-sm font-normal text-neutral-600 bg-[#E1EBF9] w-6 h-6 rounded-full flex items-center justify-center text-primary">
|
|
36
|
+
{data?.length}
|
|
37
|
+
</span>
|
|
38
|
+
</div>
|
|
39
|
+
<div className="flex flex-col gap-2 border border-neutral-200 rounded-lg mt-3">
|
|
40
|
+
{data?.map((task: any) => (
|
|
41
|
+
<div
|
|
42
|
+
key={task.id}
|
|
43
|
+
className="flex flex-row w-full items-center justify-between gap-s border-b border-neutral-200 py-1 px-2"
|
|
44
|
+
>
|
|
45
|
+
<div className="flex items-center gap-2">
|
|
46
|
+
<Avatar imageUrl={task.responsable?.image?.formats?.thumbnail?.url} />
|
|
47
|
+
<p className="text-sm font-normal w-[400px] text-nowrap text-ellipsis overflow-hidden">
|
|
48
|
+
{task?.texto_corto}
|
|
49
|
+
</p>
|
|
50
|
+
</div>
|
|
51
|
+
<div className="flex items-center gap-1">
|
|
52
|
+
<p className="text-sm font-normal">{dateFormated(task?.createdAt)}</p>
|
|
53
|
+
<div
|
|
54
|
+
className={`w-4 h-4 bg-gray-400 block rounded-full ${
|
|
55
|
+
statusMap[task?.estatus as any]?.color ?? 'bg-gray-400'
|
|
56
|
+
}`}
|
|
57
|
+
/>
|
|
58
|
+
</div>
|
|
59
|
+
</div>
|
|
60
|
+
))}
|
|
61
|
+
</div>
|
|
62
|
+
</div>
|
|
63
|
+
);
|
|
64
|
+
}
|