@flowtomic/logic 0.1.7 → 0.1.8
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/dist/hooks/useDashboardLayout/index.d.ts +2 -2
- package/dist/hooks/useDashboardLayout/index.d.ts.map +1 -1
- package/dist/hooks/useDashboardLayout/useDashboardLayout.d.ts +21 -0
- package/dist/hooks/useDashboardLayout/useDashboardLayout.d.ts.map +1 -1
- package/dist/hooks/useResizable/index.d.ts +1 -1
- package/dist/hooks/useResizable/index.d.ts.map +1 -1
- package/dist/hooks/useResizable/useResizable.d.ts +5 -0
- package/dist/hooks/useResizable/useResizable.d.ts.map +1 -1
- package/dist/index.js +2 -2
- package/package.json +3 -3
- package/dist/hooks/useAnimatedIndicator/index.js +0 -1
- package/dist/hooks/useAnimatedIndicator/useAnimatedIndicator.js +0 -242
- package/dist/hooks/useAutocomplete/index.js +0 -1
- package/dist/hooks/useAutocomplete/useAutocomplete.js +0 -380
- package/dist/hooks/useDashboardLayout/index.js +0 -1
- package/dist/hooks/useDashboardLayout/useDashboardLayout.js +0 -175
- package/dist/hooks/useGenealogy/index.js +0 -1
- package/dist/hooks/useGenealogy/useGenealogy.js +0 -341
- package/dist/hooks/useMoblile/index.js +0 -1
- package/dist/hooks/useMoblile/useMobile.js +0 -15
- package/dist/hooks/useProjectProgress/index.js +0 -1
- package/dist/hooks/useProjectProgress/useProjectProgress.js +0 -124
- package/dist/hooks/useProjectStats/index.js +0 -1
- package/dist/hooks/useProjectStats/useProjectStats.js +0 -87
- package/dist/hooks/useReactTableBack/index.js +0 -1
- package/dist/hooks/useReactTableBack/useReactTableBack.js +0 -124
- package/dist/hooks/useReactTableFront/index.js +0 -1
- package/dist/hooks/useReactTableFront/useReactTableFront.js +0 -112
- package/dist/hooks/useResizable/index.js +0 -1
- package/dist/hooks/useResizable/useResizable.js +0 -129
- package/dist/hooks/useScriptEditor/index.js +0 -1
- package/dist/hooks/useScriptEditor/useScriptEditor.js +0 -247
- package/dist/hooks/useStatCard/index.js +0 -1
- package/dist/hooks/useStatCard/useStatCard.js +0 -187
- package/dist/hooks/useThemeTransition/index.js +0 -1
- package/dist/hooks/useThemeTransition/useThemeTransition.js +0 -35
- package/dist/hooks/useTimeTracker/index.js +0 -1
- package/dist/hooks/useTimeTracker/useTimeTracker.js +0 -143
|
@@ -1,175 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* useDashboardLayout - Headless UI Hook
|
|
3
|
-
*
|
|
4
|
-
* Fornece apenas: lógica, estado, processamento e API
|
|
5
|
-
* NÃO fornece: markup, styles ou implementações pré-construídas
|
|
6
|
-
*
|
|
7
|
-
* Padrão Headless UI: você controla o markup e styles
|
|
8
|
-
*
|
|
9
|
-
* @example
|
|
10
|
-
* ```tsx
|
|
11
|
-
* function MyDashboard() {
|
|
12
|
-
* const [widgets, setWidgets] = useState<WidgetLayout[]>([]);
|
|
13
|
-
* const gridConfig: GridConfig = { columns: 12, cellSize: 50, gap: 16 };
|
|
14
|
-
*
|
|
15
|
-
* const {
|
|
16
|
-
* getWidgetGridPosition,
|
|
17
|
-
* moveWidget,
|
|
18
|
-
* resizeWidget,
|
|
19
|
-
* findNextAvailablePosition,
|
|
20
|
-
* } = useDashboardLayout({
|
|
21
|
-
* widgetsLayout: widgets,
|
|
22
|
-
* gridConfig,
|
|
23
|
-
* onUpdateLayout: setWidgets,
|
|
24
|
-
* });
|
|
25
|
-
*
|
|
26
|
-
* return (
|
|
27
|
-
* <div>
|
|
28
|
-
* {widgets.map(widget => (
|
|
29
|
-
* <div style={getWidgetGridPosition(widget)}>
|
|
30
|
-
* Widget Content
|
|
31
|
-
* </div>
|
|
32
|
-
* ))}
|
|
33
|
-
* </div>
|
|
34
|
-
* );
|
|
35
|
-
* }
|
|
36
|
-
* ```
|
|
37
|
-
*/
|
|
38
|
-
import { useCallback, useMemo } from "react";
|
|
39
|
-
const DEFAULT_GRID_CONFIG = {
|
|
40
|
-
columns: 12,
|
|
41
|
-
cellSize: 50,
|
|
42
|
-
gap: 16,
|
|
43
|
-
};
|
|
44
|
-
/**
|
|
45
|
-
* Hook para gerenciar layout do dashboard
|
|
46
|
-
*
|
|
47
|
-
* Hook headless que fornece apenas lógica de layout, sem UI.
|
|
48
|
-
* Você controla o markup e styles.
|
|
49
|
-
*/
|
|
50
|
-
export function useDashboardLayout({ widgetsLayout, gridConfig, onUpdateLayout, }) {
|
|
51
|
-
// Normaliza gridConfig com defaults
|
|
52
|
-
const normalizedGridConfig = useMemo(() => {
|
|
53
|
-
return {
|
|
54
|
-
...DEFAULT_GRID_CONFIG,
|
|
55
|
-
...gridConfig,
|
|
56
|
-
};
|
|
57
|
-
}, [gridConfig]);
|
|
58
|
-
/**
|
|
59
|
-
* Calcula posição CSS Grid para um widget
|
|
60
|
-
*/
|
|
61
|
-
const getWidgetGridPosition = useCallback((widget) => {
|
|
62
|
-
return {
|
|
63
|
-
gridColumnStart: widget.x + 1, // CSS Grid é 1-indexed
|
|
64
|
-
gridColumnEnd: widget.x + widget.w + 1,
|
|
65
|
-
gridRowStart: widget.y + 1,
|
|
66
|
-
gridRowEnd: widget.y + widget.h + 1,
|
|
67
|
-
};
|
|
68
|
-
}, []);
|
|
69
|
-
/**
|
|
70
|
-
* Verifica se uma posição está ocupada
|
|
71
|
-
*/
|
|
72
|
-
const isPositionOccupied = useCallback((x, y, w, h, excludeWidgetId) => {
|
|
73
|
-
return widgetsLayout.some((widget) => {
|
|
74
|
-
if (excludeWidgetId && widget.id === excludeWidgetId) {
|
|
75
|
-
return false;
|
|
76
|
-
}
|
|
77
|
-
// Verifica sobreposição
|
|
78
|
-
const widgetRight = widget.x + widget.w;
|
|
79
|
-
const widgetBottom = widget.y + widget.h;
|
|
80
|
-
const newRight = x + w;
|
|
81
|
-
const newBottom = y + h;
|
|
82
|
-
return !(widgetRight <= x ||
|
|
83
|
-
widget.x >= newRight ||
|
|
84
|
-
widgetBottom <= y ||
|
|
85
|
-
widget.y >= newBottom);
|
|
86
|
-
});
|
|
87
|
-
}, [widgetsLayout]);
|
|
88
|
-
/**
|
|
89
|
-
* Encontra próxima posição disponível
|
|
90
|
-
*/
|
|
91
|
-
const findNextAvailablePosition = useCallback((w, h) => {
|
|
92
|
-
const maxColumns = normalizedGridConfig.columns;
|
|
93
|
-
const maxRows = 100; // Limite razoável
|
|
94
|
-
for (let y = 0; y < maxRows; y++) {
|
|
95
|
-
for (let x = 0; x <= maxColumns - w; x++) {
|
|
96
|
-
if (!isPositionOccupied(x, y, w, h)) {
|
|
97
|
-
return { x, y };
|
|
98
|
-
}
|
|
99
|
-
}
|
|
100
|
-
}
|
|
101
|
-
return null;
|
|
102
|
-
}, [normalizedGridConfig.columns, isPositionOccupied]);
|
|
103
|
-
/**
|
|
104
|
-
* Valida se um widget pode ser movido para uma posição
|
|
105
|
-
*/
|
|
106
|
-
const canMoveToPosition = useCallback((widgetId, x, y) => {
|
|
107
|
-
const widget = widgetsLayout.find((w) => w.id === widgetId);
|
|
108
|
-
if (!widget)
|
|
109
|
-
return false;
|
|
110
|
-
// Valida limites do grid
|
|
111
|
-
if (x < 0 || y < 0)
|
|
112
|
-
return false;
|
|
113
|
-
if (x + widget.w > normalizedGridConfig.columns)
|
|
114
|
-
return false;
|
|
115
|
-
// Verifica colisões
|
|
116
|
-
return !isPositionOccupied(x, y, widget.w, widget.h, widgetId);
|
|
117
|
-
}, [widgetsLayout, normalizedGridConfig.columns, isPositionOccupied]);
|
|
118
|
-
/**
|
|
119
|
-
* Valida se um widget pode ser redimensionado
|
|
120
|
-
*/
|
|
121
|
-
const canResize = useCallback((widgetId, newW, newH) => {
|
|
122
|
-
const widget = widgetsLayout.find((w) => w.id === widgetId);
|
|
123
|
-
if (!widget)
|
|
124
|
-
return false;
|
|
125
|
-
// Valida tamanhos mínimos e máximos
|
|
126
|
-
if (newW < 2 || newH < 2)
|
|
127
|
-
return false; // Mínimo 2x2
|
|
128
|
-
if (newW > normalizedGridConfig.columns)
|
|
129
|
-
return false;
|
|
130
|
-
if (newH > 20)
|
|
131
|
-
return false; // Máximo 20 linhas
|
|
132
|
-
// Valida limites do grid
|
|
133
|
-
if (widget.x + newW > normalizedGridConfig.columns)
|
|
134
|
-
return false;
|
|
135
|
-
// Verifica colisões
|
|
136
|
-
return !isPositionOccupied(widget.x, widget.y, newW, newH, widgetId);
|
|
137
|
-
}, [widgetsLayout, normalizedGridConfig.columns, isPositionOccupied]);
|
|
138
|
-
/**
|
|
139
|
-
* Move widget para nova posição
|
|
140
|
-
*/
|
|
141
|
-
const moveWidget = useCallback((widgetId, x, y) => {
|
|
142
|
-
if (!canMoveToPosition(widgetId, x, y)) {
|
|
143
|
-
return;
|
|
144
|
-
}
|
|
145
|
-
const updatedLayout = widgetsLayout.map((widget) => widget.id === widgetId ? { ...widget, x, y } : widget);
|
|
146
|
-
onUpdateLayout(updatedLayout);
|
|
147
|
-
}, [widgetsLayout, canMoveToPosition, onUpdateLayout]);
|
|
148
|
-
/**
|
|
149
|
-
* Redimensiona widget
|
|
150
|
-
*/
|
|
151
|
-
const resizeWidget = useCallback((widgetId, w, h) => {
|
|
152
|
-
if (!canResize(widgetId, w, h)) {
|
|
153
|
-
return;
|
|
154
|
-
}
|
|
155
|
-
const updatedLayout = widgetsLayout.map((widget) => widget.id === widgetId ? { ...widget, w, h } : widget);
|
|
156
|
-
onUpdateLayout(updatedLayout);
|
|
157
|
-
}, [widgetsLayout, canResize, onUpdateLayout]);
|
|
158
|
-
/**
|
|
159
|
-
* Atualiza layout completo
|
|
160
|
-
*/
|
|
161
|
-
const updateLayout = useCallback((layout) => {
|
|
162
|
-
onUpdateLayout(layout);
|
|
163
|
-
}, [onUpdateLayout]);
|
|
164
|
-
return {
|
|
165
|
-
gridConfig: normalizedGridConfig,
|
|
166
|
-
getWidgetGridPosition,
|
|
167
|
-
isPositionOccupied,
|
|
168
|
-
findNextAvailablePosition,
|
|
169
|
-
canMoveToPosition,
|
|
170
|
-
canResize,
|
|
171
|
-
moveWidget,
|
|
172
|
-
resizeWidget,
|
|
173
|
-
updateLayout,
|
|
174
|
-
};
|
|
175
|
-
}
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
export { useGenealogy } from "./useGenealogy";
|
|
@@ -1,341 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* useGenealogy - Headless UI Hook
|
|
3
|
-
*
|
|
4
|
-
* Fornece apenas: lógica, estado, processamento e API para árvores genealógicas
|
|
5
|
-
* NÃO fornece: markup, styles ou implementações pré-construídas
|
|
6
|
-
*
|
|
7
|
-
* Padrão Headless UI: você controla o markup e styles
|
|
8
|
-
*/
|
|
9
|
-
import { useCallback, useMemo, useState } from "react";
|
|
10
|
-
/**
|
|
11
|
-
* Calcula o nível (geração) de cada entidade na hierarquia
|
|
12
|
-
* Retorna um Map com entityId -> nível (0 = raiz, 1 = filhos da raiz, etc)
|
|
13
|
-
*/
|
|
14
|
-
function calculateLevels(hierarchy) {
|
|
15
|
-
const levels = new Map();
|
|
16
|
-
const visited = new Set();
|
|
17
|
-
// Função recursiva para calcular nível
|
|
18
|
-
const calculateLevel = (entityId) => {
|
|
19
|
-
const cachedLevel = levels.get(entityId);
|
|
20
|
-
if (cachedLevel !== undefined) {
|
|
21
|
-
return cachedLevel;
|
|
22
|
-
}
|
|
23
|
-
if (visited.has(entityId)) {
|
|
24
|
-
// Ciclo detectado, retornar 0 como fallback
|
|
25
|
-
return 0;
|
|
26
|
-
}
|
|
27
|
-
visited.add(entityId);
|
|
28
|
-
const entityHierarchy = hierarchy.get(entityId);
|
|
29
|
-
if (!entityHierarchy || entityHierarchy.parents.length === 0) {
|
|
30
|
-
// Sem pais = raiz (nível 0)
|
|
31
|
-
levels.set(entityId, 0);
|
|
32
|
-
return 0;
|
|
33
|
-
}
|
|
34
|
-
// Nível = nível do pai mais alto + 1
|
|
35
|
-
const parentLevels = entityHierarchy.parents.map((parent) => calculateLevel(parent.id));
|
|
36
|
-
const maxParentLevel = Math.max(...parentLevels);
|
|
37
|
-
const level = maxParentLevel + 1;
|
|
38
|
-
levels.set(entityId, level);
|
|
39
|
-
return level;
|
|
40
|
-
};
|
|
41
|
-
// Calcular nível para todas as entidades
|
|
42
|
-
hierarchy.forEach((_, entityId) => {
|
|
43
|
-
calculateLevel(entityId);
|
|
44
|
-
});
|
|
45
|
-
return levels;
|
|
46
|
-
}
|
|
47
|
-
/**
|
|
48
|
-
* Organiza entidades por nível (geração) e calcula posições hierárquicas
|
|
49
|
-
*/
|
|
50
|
-
function calculateHierarchicalPositions(_data, hierarchy, nodeWidth, nodeHeight, horizontalSpacing, verticalSpacing) {
|
|
51
|
-
const positions = new Map();
|
|
52
|
-
const levels = calculateLevels(hierarchy);
|
|
53
|
-
// Agrupar entidades por nível
|
|
54
|
-
const entitiesByLevel = new Map();
|
|
55
|
-
levels.forEach((level, entityId) => {
|
|
56
|
-
if (!entitiesByLevel.has(level)) {
|
|
57
|
-
entitiesByLevel.set(level, []);
|
|
58
|
-
}
|
|
59
|
-
entitiesByLevel.get(level)?.push(entityId);
|
|
60
|
-
});
|
|
61
|
-
// Calcular posições para cada nível, começando do nível 0 (raiz)
|
|
62
|
-
const sortedLevels = Array.from(entitiesByLevel.keys()).sort((a, b) => a - b);
|
|
63
|
-
sortedLevels.forEach((level) => {
|
|
64
|
-
const entityIds = entitiesByLevel.get(level);
|
|
65
|
-
if (!entityIds)
|
|
66
|
-
return;
|
|
67
|
-
// Ordenar entidades do mesmo nível (por ID para consistência)
|
|
68
|
-
entityIds.sort();
|
|
69
|
-
// Calcular posição Y baseada no nível (pais acima, filhos abaixo)
|
|
70
|
-
const y = level * (nodeHeight + verticalSpacing);
|
|
71
|
-
// Para cada entidade, tentar centralizar abaixo dos pais
|
|
72
|
-
entityIds.forEach((entityId) => {
|
|
73
|
-
const entityHierarchy = hierarchy.get(entityId);
|
|
74
|
-
let x = 0;
|
|
75
|
-
if (entityHierarchy && entityHierarchy.parents.length > 0) {
|
|
76
|
-
// Calcular posição média dos pais
|
|
77
|
-
const parentPositions = entityHierarchy.parents
|
|
78
|
-
.map((parent) => positions.get(parent.id))
|
|
79
|
-
.filter((pos) => pos !== undefined);
|
|
80
|
-
if (parentPositions.length > 0) {
|
|
81
|
-
const avgX = parentPositions.reduce((sum, pos) => sum + pos.x, 0) / parentPositions.length;
|
|
82
|
-
x = avgX;
|
|
83
|
-
}
|
|
84
|
-
}
|
|
85
|
-
// Se não tem pais ou não conseguiu calcular, distribuir horizontalmente
|
|
86
|
-
if (x === 0 && entityIds.length > 1) {
|
|
87
|
-
const index = entityIds.indexOf(entityId);
|
|
88
|
-
const totalWidth = entityIds.length * nodeWidth + (entityIds.length - 1) * horizontalSpacing;
|
|
89
|
-
const startX = -totalWidth / 2 + nodeWidth / 2;
|
|
90
|
-
x = startX + index * (nodeWidth + horizontalSpacing);
|
|
91
|
-
}
|
|
92
|
-
positions.set(entityId, { x, y });
|
|
93
|
-
});
|
|
94
|
-
// Ajustar posições para evitar sobreposição
|
|
95
|
-
entityIds.forEach((entityId, index) => {
|
|
96
|
-
const currentPos = positions.get(entityId);
|
|
97
|
-
if (!currentPos)
|
|
98
|
-
return;
|
|
99
|
-
// Verificar se há sobreposição com outros nós do mesmo nível
|
|
100
|
-
entityIds.slice(0, index).forEach((otherId) => {
|
|
101
|
-
const otherPos = positions.get(otherId);
|
|
102
|
-
if (!otherPos)
|
|
103
|
-
return;
|
|
104
|
-
if (Math.abs(currentPos.x - otherPos.x) < nodeWidth + horizontalSpacing) {
|
|
105
|
-
// Ajustar para evitar sobreposição
|
|
106
|
-
currentPos.x = otherPos.x + nodeWidth + horizontalSpacing;
|
|
107
|
-
}
|
|
108
|
-
});
|
|
109
|
-
});
|
|
110
|
-
});
|
|
111
|
-
return positions;
|
|
112
|
-
}
|
|
113
|
-
/**
|
|
114
|
-
* Gera nodes do ReactFlow a partir dos dados de genealogia
|
|
115
|
-
*/
|
|
116
|
-
function generateNodes(data, hierarchy, expansionState, nodeWidth, nodeHeight, horizontalSpacing, verticalSpacing) {
|
|
117
|
-
const nodes = [];
|
|
118
|
-
// Calcular posições hierárquicas
|
|
119
|
-
const positions = calculateHierarchicalPositions(data, hierarchy, nodeWidth, nodeHeight, horizontalSpacing, verticalSpacing);
|
|
120
|
-
// Criar nodes com posições calculadas
|
|
121
|
-
data.people.forEach((entity) => {
|
|
122
|
-
const position = positions.get(entity.id) || { x: 0, y: 0 };
|
|
123
|
-
const entityHierarchy = hierarchy.get(entity.id);
|
|
124
|
-
nodes.push({
|
|
125
|
-
id: entity.id,
|
|
126
|
-
type: "genealogy",
|
|
127
|
-
position,
|
|
128
|
-
data: {
|
|
129
|
-
person: entity,
|
|
130
|
-
hierarchy: entityHierarchy,
|
|
131
|
-
isExpanded: expansionState[entity.id] ?? false,
|
|
132
|
-
},
|
|
133
|
-
});
|
|
134
|
-
});
|
|
135
|
-
return nodes;
|
|
136
|
-
}
|
|
137
|
-
/**
|
|
138
|
-
* Gera edges do ReactFlow a partir dos relacionamentos
|
|
139
|
-
* Para hierarquia vertical: source (pai) -> target (filho)
|
|
140
|
-
*/
|
|
141
|
-
function generateEdges(data, hierarchy, expansionState) {
|
|
142
|
-
const edges = [];
|
|
143
|
-
const edgeIds = new Set();
|
|
144
|
-
data.relationships.forEach((relationship) => {
|
|
145
|
-
// Determinar source e target baseado no tipo de relacionamento
|
|
146
|
-
let source;
|
|
147
|
-
let target;
|
|
148
|
-
switch (relationship.type) {
|
|
149
|
-
case "father":
|
|
150
|
-
case "mother": {
|
|
151
|
-
// from é pai/mãe de to
|
|
152
|
-
// source = pai (from), target = filho (to)
|
|
153
|
-
source = relationship.from;
|
|
154
|
-
target = relationship.to;
|
|
155
|
-
break;
|
|
156
|
-
}
|
|
157
|
-
case "child": {
|
|
158
|
-
// from é filho de to
|
|
159
|
-
// source = pai (to), target = filho (from)
|
|
160
|
-
source = relationship.to;
|
|
161
|
-
target = relationship.from;
|
|
162
|
-
break;
|
|
163
|
-
}
|
|
164
|
-
default: {
|
|
165
|
-
source = relationship.from;
|
|
166
|
-
target = relationship.to;
|
|
167
|
-
}
|
|
168
|
-
}
|
|
169
|
-
// Criar edge ID único
|
|
170
|
-
const edgeId = `e-${source}-${target}-${relationship.type}`;
|
|
171
|
-
// Evitar duplicatas
|
|
172
|
-
if (edgeIds.has(edgeId)) {
|
|
173
|
-
return;
|
|
174
|
-
}
|
|
175
|
-
edgeIds.add(edgeId);
|
|
176
|
-
// Verificar se ambos os nós existem
|
|
177
|
-
const sourceHierarchy = hierarchy.get(source);
|
|
178
|
-
const targetHierarchy = hierarchy.get(target);
|
|
179
|
-
if (!sourceHierarchy || !targetHierarchy) {
|
|
180
|
-
return;
|
|
181
|
-
}
|
|
182
|
-
// Só criar edge se ambos os nós estiverem visíveis (expandidos ou raiz)
|
|
183
|
-
const sourceExpanded = expansionState[source] ?? true;
|
|
184
|
-
const targetExpanded = expansionState[target] ?? true;
|
|
185
|
-
// Para relacionamentos pai/filho, só mostrar se o nó pai estiver expandido
|
|
186
|
-
if (relationship.type === "father" ||
|
|
187
|
-
relationship.type === "mother" ||
|
|
188
|
-
relationship.type === "child") {
|
|
189
|
-
if (!sourceExpanded || !targetExpanded)
|
|
190
|
-
return;
|
|
191
|
-
}
|
|
192
|
-
edges.push({
|
|
193
|
-
id: edgeId,
|
|
194
|
-
source,
|
|
195
|
-
target,
|
|
196
|
-
type: "animated",
|
|
197
|
-
data: {
|
|
198
|
-
relationship,
|
|
199
|
-
},
|
|
200
|
-
});
|
|
201
|
-
});
|
|
202
|
-
return edges;
|
|
203
|
-
}
|
|
204
|
-
/**
|
|
205
|
-
* Constrói hierarquia de entidades a partir dos dados
|
|
206
|
-
*/
|
|
207
|
-
function buildHierarchy(data) {
|
|
208
|
-
const hierarchy = new Map();
|
|
209
|
-
// Inicializar todas as entidades
|
|
210
|
-
data.people.forEach((entity) => {
|
|
211
|
-
hierarchy.set(entity.id, {
|
|
212
|
-
person: entity,
|
|
213
|
-
parents: [],
|
|
214
|
-
children: [],
|
|
215
|
-
adoptions: [],
|
|
216
|
-
siblings: [],
|
|
217
|
-
});
|
|
218
|
-
});
|
|
219
|
-
// Processar relacionamentos
|
|
220
|
-
data.relationships.forEach((relationship) => {
|
|
221
|
-
const fromHierarchy = hierarchy.get(relationship.from);
|
|
222
|
-
const toHierarchy = hierarchy.get(relationship.to);
|
|
223
|
-
if (!fromHierarchy || !toHierarchy)
|
|
224
|
-
return;
|
|
225
|
-
switch (relationship.type) {
|
|
226
|
-
case "father":
|
|
227
|
-
case "mother": {
|
|
228
|
-
// from é pai/mãe de to
|
|
229
|
-
if (!toHierarchy.parents.find((p) => p.id === relationship.from)) {
|
|
230
|
-
toHierarchy.parents.push(fromHierarchy.person);
|
|
231
|
-
}
|
|
232
|
-
if (relationship.type === "father") {
|
|
233
|
-
toHierarchy.father = fromHierarchy.person;
|
|
234
|
-
}
|
|
235
|
-
else if (relationship.type === "mother") {
|
|
236
|
-
toHierarchy.mother = fromHierarchy.person;
|
|
237
|
-
}
|
|
238
|
-
if (!fromHierarchy.children.find((c) => c.id === relationship.to)) {
|
|
239
|
-
fromHierarchy.children.push(toHierarchy.person);
|
|
240
|
-
}
|
|
241
|
-
break;
|
|
242
|
-
}
|
|
243
|
-
case "child": {
|
|
244
|
-
// from é filho de to
|
|
245
|
-
if (!toHierarchy.children.find((c) => c.id === relationship.from)) {
|
|
246
|
-
toHierarchy.children.push(fromHierarchy.person);
|
|
247
|
-
}
|
|
248
|
-
if (!fromHierarchy.parents.find((p) => p.id === relationship.to)) {
|
|
249
|
-
fromHierarchy.parents.push(toHierarchy.person);
|
|
250
|
-
}
|
|
251
|
-
break;
|
|
252
|
-
}
|
|
253
|
-
}
|
|
254
|
-
});
|
|
255
|
-
// Calcular irmãos (pessoas com os mesmos pais)
|
|
256
|
-
hierarchy.forEach((personHierarchy) => {
|
|
257
|
-
if (personHierarchy.parents.length > 0) {
|
|
258
|
-
const parentIds = personHierarchy.parents.map((p) => p.id);
|
|
259
|
-
hierarchy.forEach((otherHierarchy, otherId) => {
|
|
260
|
-
if (otherId !== personHierarchy.person.id) {
|
|
261
|
-
const hasCommonParent = otherHierarchy.parents.some((p) => parentIds.includes(p.id));
|
|
262
|
-
if (hasCommonParent && !personHierarchy.siblings.find((s) => s.id === otherId)) {
|
|
263
|
-
personHierarchy.siblings.push(otherHierarchy.person);
|
|
264
|
-
}
|
|
265
|
-
}
|
|
266
|
-
});
|
|
267
|
-
}
|
|
268
|
-
});
|
|
269
|
-
return hierarchy;
|
|
270
|
-
}
|
|
271
|
-
/**
|
|
272
|
-
* Hook headless para gerenciar árvores genealógicas
|
|
273
|
-
*/
|
|
274
|
-
export function useGenealogy({ data, initialExpanded = [], nodeWidth = 200, nodeHeight = 100, horizontalSpacing = 250, verticalSpacing = 150, onNodeSelect, onNodeExpand, onAddRelation, }) {
|
|
275
|
-
// Estado de expansão dos nós
|
|
276
|
-
const [expansionState, setExpansionState] = useState(() => {
|
|
277
|
-
const state = {};
|
|
278
|
-
initialExpanded.forEach((id) => {
|
|
279
|
-
state[id] = true;
|
|
280
|
-
});
|
|
281
|
-
return state;
|
|
282
|
-
});
|
|
283
|
-
// Estado de seleção
|
|
284
|
-
const [selectedNodeId, setSelectedNodeId] = useState(null);
|
|
285
|
-
// Construir hierarquia
|
|
286
|
-
const hierarchy = useMemo(() => buildHierarchy(data), [data]);
|
|
287
|
-
// Gerar nodes e edges
|
|
288
|
-
const nodes = useMemo(() => generateNodes(data, hierarchy, expansionState, nodeWidth, nodeHeight, horizontalSpacing, verticalSpacing), [data, hierarchy, expansionState, nodeWidth, nodeHeight, horizontalSpacing, verticalSpacing]);
|
|
289
|
-
const edges = useMemo(() => generateEdges(data, hierarchy, expansionState), [data, hierarchy, expansionState]);
|
|
290
|
-
// Função para selecionar um nó
|
|
291
|
-
const selectNode = useCallback((nodeId) => {
|
|
292
|
-
setSelectedNodeId(nodeId);
|
|
293
|
-
const entity = data.people.find((p) => p.id === nodeId);
|
|
294
|
-
if (entity && onNodeSelect) {
|
|
295
|
-
onNodeSelect(nodeId, entity);
|
|
296
|
-
}
|
|
297
|
-
}, [data.people, onNodeSelect]);
|
|
298
|
-
// Função para expandir/colapsar um nó
|
|
299
|
-
const expandNode = useCallback((nodeId, expanded) => {
|
|
300
|
-
setExpansionState((prev) => {
|
|
301
|
-
const newState = { ...prev };
|
|
302
|
-
const isCurrentlyExpanded = prev[nodeId] ?? false;
|
|
303
|
-
newState[nodeId] = expanded ?? !isCurrentlyExpanded;
|
|
304
|
-
if (onNodeExpand) {
|
|
305
|
-
onNodeExpand(nodeId, newState[nodeId]);
|
|
306
|
-
}
|
|
307
|
-
return newState;
|
|
308
|
-
});
|
|
309
|
-
}, [onNodeExpand]);
|
|
310
|
-
// Função para colapsar um nó
|
|
311
|
-
const collapseNode = useCallback((nodeId) => {
|
|
312
|
-
expandNode(nodeId, false);
|
|
313
|
-
}, [expandNode]);
|
|
314
|
-
// Função para adicionar relacionamento
|
|
315
|
-
const addRelation = useCallback((fromId, toId, type) => {
|
|
316
|
-
if (onAddRelation) {
|
|
317
|
-
onAddRelation(fromId, toId, type);
|
|
318
|
-
}
|
|
319
|
-
}, [onAddRelation]);
|
|
320
|
-
// Função para obter hierarquia de uma pessoa
|
|
321
|
-
const getPersonHierarchy = useCallback((personId) => {
|
|
322
|
-
return hierarchy.get(personId);
|
|
323
|
-
}, [hierarchy]);
|
|
324
|
-
// Função para obter relacionamentos de uma pessoa
|
|
325
|
-
const getPersonRelations = useCallback((personId) => {
|
|
326
|
-
return data.relationships.filter((r) => r.from === personId || r.to === personId);
|
|
327
|
-
}, [data.relationships]);
|
|
328
|
-
return {
|
|
329
|
-
nodes,
|
|
330
|
-
edges,
|
|
331
|
-
hierarchy,
|
|
332
|
-
selectedNodeId,
|
|
333
|
-
expansionState,
|
|
334
|
-
selectNode,
|
|
335
|
-
expandNode,
|
|
336
|
-
collapseNode,
|
|
337
|
-
addRelation,
|
|
338
|
-
getPersonHierarchy,
|
|
339
|
-
getPersonRelations,
|
|
340
|
-
};
|
|
341
|
-
}
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
export { useIsMobile } from "./useMobile";
|
|
@@ -1,15 +0,0 @@
|
|
|
1
|
-
import * as React from "react";
|
|
2
|
-
const MOBILE_BREAKPOINT = 768;
|
|
3
|
-
export function useIsMobile() {
|
|
4
|
-
const [isMobile, setIsMobile] = React.useState(undefined);
|
|
5
|
-
React.useEffect(() => {
|
|
6
|
-
const mql = window.matchMedia(`(max-width: ${MOBILE_BREAKPOINT - 1}px)`);
|
|
7
|
-
const onChange = () => {
|
|
8
|
-
setIsMobile(window.innerWidth < MOBILE_BREAKPOINT);
|
|
9
|
-
};
|
|
10
|
-
mql.addEventListener("change", onChange);
|
|
11
|
-
setIsMobile(window.innerWidth < MOBILE_BREAKPOINT);
|
|
12
|
-
return () => mql.removeEventListener("change", onChange);
|
|
13
|
-
}, []);
|
|
14
|
-
return !!isMobile;
|
|
15
|
-
}
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
export { useProjectProgress } from "./useProjectProgress";
|
|
@@ -1,124 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* useProjectProgress - Headless UI Hook
|
|
3
|
-
*
|
|
4
|
-
* Fornece apenas: lógica, estado, processamento e API
|
|
5
|
-
* NÃO fornece: markup, styles ou implementações pré-construídas
|
|
6
|
-
*
|
|
7
|
-
* Padrão Headless UI: você controla o markup e styles
|
|
8
|
-
*
|
|
9
|
-
* @example
|
|
10
|
-
* ```tsx
|
|
11
|
-
* function MyProjectProgress() {
|
|
12
|
-
* const { percentage, status, distribution } = useProjectProgress({
|
|
13
|
-
* projects: [
|
|
14
|
-
* { id: '1', status: 'completed', ... },
|
|
15
|
-
* { id: '2', status: 'in-progress', ... },
|
|
16
|
-
* { id: '3', status: 'pending', ... },
|
|
17
|
-
* ],
|
|
18
|
-
* })
|
|
19
|
-
*
|
|
20
|
-
* return (
|
|
21
|
-
* <div>
|
|
22
|
-
* <div>Progress: {percentage}%</div>
|
|
23
|
-
* <div>Status: {status}</div>
|
|
24
|
-
* </div>
|
|
25
|
-
* )
|
|
26
|
-
* }
|
|
27
|
-
* ```
|
|
28
|
-
*/
|
|
29
|
-
import { useMemo } from "react";
|
|
30
|
-
/**
|
|
31
|
-
* Calcula progresso padrão baseado no status
|
|
32
|
-
*/
|
|
33
|
-
function getDefaultProgress(project) {
|
|
34
|
-
if (project.progress !== undefined) {
|
|
35
|
-
return project.progress;
|
|
36
|
-
}
|
|
37
|
-
switch (project.status) {
|
|
38
|
-
case "completed":
|
|
39
|
-
return 100;
|
|
40
|
-
case "in-progress":
|
|
41
|
-
return 50; // Valor padrão para projetos em progresso
|
|
42
|
-
case "pending":
|
|
43
|
-
return 0;
|
|
44
|
-
case "on-hold":
|
|
45
|
-
return 0;
|
|
46
|
-
default:
|
|
47
|
-
return 0;
|
|
48
|
-
}
|
|
49
|
-
}
|
|
50
|
-
/**
|
|
51
|
-
* Hook headless para calcular progresso de projetos
|
|
52
|
-
*/
|
|
53
|
-
export function useProjectProgress(options) {
|
|
54
|
-
const { projects, calculateProgress = getDefaultProgress } = options;
|
|
55
|
-
const result = useMemo(() => {
|
|
56
|
-
if (projects.length === 0) {
|
|
57
|
-
return {
|
|
58
|
-
percentage: 0,
|
|
59
|
-
status: "pending",
|
|
60
|
-
distribution: {
|
|
61
|
-
completed: 0,
|
|
62
|
-
inProgress: 0,
|
|
63
|
-
pending: 0,
|
|
64
|
-
onHold: 0,
|
|
65
|
-
},
|
|
66
|
-
total: 0,
|
|
67
|
-
completed: 0,
|
|
68
|
-
inProgress: 0,
|
|
69
|
-
pending: 0,
|
|
70
|
-
onHold: 0,
|
|
71
|
-
};
|
|
72
|
-
}
|
|
73
|
-
// Calcular distribuição
|
|
74
|
-
const distribution = {
|
|
75
|
-
completed: 0,
|
|
76
|
-
inProgress: 0,
|
|
77
|
-
pending: 0,
|
|
78
|
-
onHold: 0,
|
|
79
|
-
};
|
|
80
|
-
let totalProgress = 0;
|
|
81
|
-
projects.forEach((project) => {
|
|
82
|
-
const progress = calculateProgress(project);
|
|
83
|
-
totalProgress += progress;
|
|
84
|
-
switch (project.status) {
|
|
85
|
-
case "completed":
|
|
86
|
-
distribution.completed++;
|
|
87
|
-
break;
|
|
88
|
-
case "in-progress":
|
|
89
|
-
distribution.inProgress++;
|
|
90
|
-
break;
|
|
91
|
-
case "pending":
|
|
92
|
-
distribution.pending++;
|
|
93
|
-
break;
|
|
94
|
-
case "on-hold":
|
|
95
|
-
distribution.onHold++;
|
|
96
|
-
break;
|
|
97
|
-
}
|
|
98
|
-
});
|
|
99
|
-
// Calcular porcentagem média
|
|
100
|
-
const percentage = Math.round(totalProgress / projects.length);
|
|
101
|
-
// Determinar status geral
|
|
102
|
-
let status = "pending";
|
|
103
|
-
if (distribution.completed === projects.length) {
|
|
104
|
-
status = "completed";
|
|
105
|
-
}
|
|
106
|
-
else if (distribution.inProgress > 0 || distribution.completed > 0) {
|
|
107
|
-
status = "in-progress";
|
|
108
|
-
}
|
|
109
|
-
else if (distribution.onHold > 0) {
|
|
110
|
-
status = "on-hold";
|
|
111
|
-
}
|
|
112
|
-
return {
|
|
113
|
-
percentage,
|
|
114
|
-
status,
|
|
115
|
-
distribution,
|
|
116
|
-
total: projects.length,
|
|
117
|
-
completed: distribution.completed,
|
|
118
|
-
inProgress: distribution.inProgress,
|
|
119
|
-
pending: distribution.pending,
|
|
120
|
-
onHold: distribution.onHold,
|
|
121
|
-
};
|
|
122
|
-
}, [projects, calculateProgress]);
|
|
123
|
-
return result;
|
|
124
|
-
}
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
export { useProjectStats } from "./useProjectStats";
|