File size: 16,312 Bytes
0e36185
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
c1143bd
 
 
 
 
 
 
 
 
 
 
 
 
 
0e36185
 
 
 
 
 
 
c1143bd
0e36185
 
 
c1143bd
 
 
 
0e36185
c1143bd
0e36185
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
c1143bd
0e36185
c1143bd
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
0e36185
 
 
c1143bd
 
 
 
 
 
 
 
 
 
 
0e36185
c1143bd
0e36185
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
a6fc104
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>TaskFlow Visualizer</title>
    <script src="https://cdn.tailwindcss.com"></script>
    <script src="https://unpkg.com/feather-icons"></script>
    <script src="https://cdn.jsdelivr.net/npm/d3@7/dist/d3.min.js"></script>
    <script src="https://cdn.jsdelivr.net/npm/graphlib@2.1.8/dist/graphlib.min.js"></script>
    <script src="https://cdn.jsdelivr.net/npm/dagre-d3@0.6.4/dist/dagre-d3.min.js"></script>
    <style>
        .node rect {
            rx: 8;
            ry: 8;
        }
        .node.doNext rect {
            fill: #0a2a22;
            stroke: #1f6f57;
        }
        .node.blocked rect {
            fill: #2a1212;
            stroke: #8a2b2b;
        }
        .node.normal rect {
            fill: #182235;
            stroke: #2b3b57;
        }
        .node:hover rect {
            stroke-width: 2;
        }
        .edgePath path {
            stroke: #51607a;
            stroke-width: 1.5;
        }
        .edgePath.marker path {
            marker-end: url(#arrow);
        }
        .swatch {
            width: 10px;
            height: 10px;
            border-radius: 2px;
            display: inline-block;
        }
    </style>
</head>
<body class="bg-gray-900 text-gray-100">
    <header class="sticky top-0 z-10 bg-gray-900/90 backdrop-blur-sm border-b border-gray-800 px-6 py-4 flex items-center justify-between">
        <div class="flex items-center gap-4">
            <h1 class="text-xl font-bold">TaskFlow Visualizer</h1>
            <span class="text-sm bg-gray-800 px-3 py-1 rounded-full">v1.0</span>
        </div>
        <div class="flex gap-2">
            <button id="exportBtn" class="flex items-center gap-2 bg-gray-800 hover:bg-gray-700 px-4 py-2 rounded-full text-sm transition-colors">
                <i data-feather="download"></i>
                Export
            </button>
            <button id="helpBtn" class="flex items-center gap-2 bg-blue-600 hover:bg-blue-500 px-4 py-2 rounded-full text-sm transition-colors">
                <i data-feather="help-circle"></i>
                Help
            </button>
        </div>
    </header>

    <div class="flex h-[calc(100vh-68px)]">
        <!-- Sidebar -->
        <aside class="w-80 border-r border-gray-800 p-4 overflow-y-auto">
            <div class="mb-6">
                <div class="flex gap-2 mb-4">
                    <button class="filter-btn active px-4 py-2 rounded-full bg-gray-800 text-sm" data-filter="all">All Tasks</button>
                    <button class="filter-btn px-4 py-2 rounded-full bg-gray-800 text-sm" data-filter="doNext">Do Next</button>
                    <button class="filter-btn px-4 py-2 rounded-full bg-gray-800 text-sm" data-filter="blocked">Blocked</button>
                </div>
                <div class="flex flex-wrap gap-2 mb-6">
                    <span class="chip px-3 py-1 rounded-full text-xs flex items-center gap-2 bg-gray-800">
                        <span class="swatch bg-green-800"></span>
                        Do-Next
                    </span>
                    <span class="chip px-3 py-1 rounded-full text-xs flex items-center gap-2 bg-gray-800">
                        <span class="swatch bg-red-800"></span>
                        Blocked
                    </span>
                    <span class="chip px-3 py-1 rounded-full text-xs flex items-center gap-2 bg-gray-800">
                        <span class="swatch bg-blue-800"></span>
                        Normal
                    </span>
                </div>
                <div class="relative">
                    <i data-feather="search" class="absolute left-3 top-1/2 transform -translate-y-1/2 text-gray-500" width="16"></i>
                    <input type="text" placeholder="Search tasks..." class="w-full bg-gray-800 border border-gray-700 rounded-lg pl-10 pr-4 py-2 text-sm focus:outline-none focus:ring-2 focus:ring-blue-500">
                </div>
            </div>
            <div class="bg-gray-800 rounded-xl p-4 mb-4">
                <h3 class="font-medium mb-2">Task Details</h3>
                <div id="detailBody" class="text-sm text-gray-400">
                    Select a task to view details
                </div>
                <div class="mt-3 text-xs text-gray-500">
                    <div class="flex items-center gap-2 mb-1">
                        <span class="swatch bg-green-800"></span>
                        <span>Do-Next (Priority)</span>
                    </div>
                    <div class="flex items-center gap-2 mb-1">
                        <span class="swatch bg-blue-800"></span>
                        <span>Normal</span>
                    </div>
                    <div class="flex items-center gap-2">
                        <span class="swatch bg-red-800"></span>
                        <span>Blocked</span>
                    </div>
                </div>
            </div>
            <div class="bg-gray-800 rounded-xl p-4">
                <h3 class="font-medium mb-2">Quick Actions</h3>
                <button class="w-full flex items-center gap-2 text-sm bg-gray-700 hover:bg-gray-600 px-3 py-2 rounded-lg mb-2 transition-colors">
                    <i data-feather="plus" width="16"></i>
                    Add New Task
                </button>
                <button class="w-full flex items-center gap-2 text-sm bg-gray-700 hover:bg-gray-600 px-3 py-2 rounded-lg mb-2 transition-colors">
                    <i data-feather="link" width="16"></i>
                    Create Dependency
                </button>
                <button class="w-full flex items-center gap-2 text-sm bg-gray-700 hover:bg-gray-600 px-3 py-2 rounded-lg transition-colors">
                    <i data-feather="filter" width="16"></i>
                    Filter by Location
                </button>
            </div>
</aside>

        <!-- Main Graph Area -->
        <main class="flex-1 relative">
            <svg id="graph" class="w-full h-full">
                <defs>
                    <marker id="arrow" viewBox="0 0 10 10" refX="9" refY="5" markerWidth="6" markerHeight="6" orient="auto-start-reverse">
                        <path d="M 0 0 L 10 5 L 0 10 z" fill="#51607a"></path>
                    </marker>
                </defs>
                <g id="zoom"><g id="g"></g></g>
            </svg>
            <div class="absolute bottom-4 right-4 flex gap-2">
                <button id="zoomIn" class="p-2 bg-gray-800 rounded-lg hover:bg-gray-700">
                    <i data-feather="plus" width="16"></i>
                </button>
                <button id="zoomOut" class="p-2 bg-gray-800 rounded-lg hover:bg-gray-700">
                    <i data-feather="minus" width="16"></i>
                </button>
                <button id="fitView" class="p-2 bg-gray-800 rounded-lg hover:bg-gray-700">
                    <i data-feather="maximize" width="16"></i>
                </button>
            </div>
        </main>
    </div>

    <script>
        // Initialize Feather Icons
        feather.replace();
        // Task data
        const tasks = [
            // Location One - The House
            { id: "L1-EX1", label: "Grease garage door tracks", location: "The House", section: "Exterior Maintenance", status: "doNext", full: "Grease garage door tracks and wheels" },
            { id: "L1-EX2", label: "Mow lawn twice weekly", location: "The House", section: "Exterior Maintenance", status: "doNext", full: "Mow lawn twice weekly until dormant" },
            { id: "L1-EX3", label: "Maintain weed control", location: "The House", section: "Exterior Maintenance", status: "doNext", full: "Maintain weed control around the house" },
            
            { id: "L1-HU1", label: "Refresh cat litter", location: "The House", section: "Household Upkeep", status: "doNext", full: "Refresh cat litter" },
            { id: "L1-HU2", label: "Take out trash", location: "The House", section: "Household Upkeep", status: "doNext", full: "Take out trash" },
            
            { id: "L1-EL1", label: "Map basement electrical", location: "The House", section: "Basement Electrical", status: "doNext", full: "Map electrical plan for basement" },
            { id: "L1-EL2", label: "List basement electrical items", location: "The House", section: "Basement Electrical", status: "doNext", full: "Create list of tools, parts, and materials for basement electric" },
            
            // Location Two - The Lot
            { id: "L2-LM1", label: "Mow lot twice weekly", location: "The Lot", section: "Lawn Maintenance", status: "doNext", full: "Mow lot twice weekly until dormant" },
            { id: "L2-WC1", label: "Maintain weed death", location: "The Lot", section: "Weed Control", status: "doNext", full: "Maintain weed death across the lot" },
            
            // Location Three - The Pad
            { id: "L3-SU1", label: "Take out pad trash", location: "The Pad", section: "Site Upkeep", status: "doNext", full: "Take out trash weekly" },
            { id: "L3-SU2", label: "Maintain pad weed control", location: "The Pad", section: "Site Upkeep", status: "doNext", full: "Maintain weed control on the pad" },
            
            // Blocked tasks
            { id: "L1-PW1", label: "Remove west wall", location: "The House", section: "Structural", status: "blocked", full: "Cannot replace west wall until power is removed" },
            { id: "L1-PW2", label: "Remove power", location: "The House", section: "Structural", status: "blocked", full: "Cannot disconnect power until ready to reconnect power" },
            { id: "L1-FP1", label: "Remove fireplace", location: "The House", section: "Structural", status: "blocked", full: "Cannot remove fireplace until temporary walls are built" }
        ];

        const deps = [
            // Structural dependencies
            ["L1-PW1", "L1-PW2"], // Can't remove west wall until power is removed
            ["L1-FP1", "L1-PW1"], // Can't remove fireplace until west wall is removed
            
            // Location relationships
            ["L1-EX1", "L1-EX2"],  // Exterior maintenance sequence
            ["L1-HU1", "L1-HU2"],  // Household upkeep sequence
            
            // Cross-location relationships
            ["L1-EX2", "L2-LM1"],  // Mowing sequence between locations
            ["L1-EX3", "L2-WC1"]   // Weed control sequence
        ];
// Graph initialization
        const g = new dagreD3.graphlib.Graph()
            .setGraph({ rankdir: "LR", nodesep: 25, ranksep: 55, marginx: 20, marginy: 20 })
            .setDefaultEdgeLabel(() => ({}));

        function nodeStyle(d) { 
            return d.status === "doNext" ? "doNext" : (d.status === "blocked" ? "blocked" : "normal"); 
        }

        function build(filter = "all") {
            g.nodes().forEach(v => g.removeNode(v));
            tasks.forEach(t => {
                if (filter === "doNext" && t.status !== "doNext") return;
                if (filter === "blocked" && t.status !== "blocked") return;
                g.setNode(t.id, {
                    labelType: "html",
                    label: `
                        <div class="font-semibold text-sm">${escapeHTML(t.label)}</div>
                        <div class="text-xs text-gray-400">${escapeHTML(t.location)}${escapeHTML(t.section)}</div>
                    `,
                    class: `node ${nodeStyle(t)}`
                });
            });
            deps.forEach(([a, b]) => {
                if (!g.hasNode(a) || !g.hasNode(b)) return;
                g.setEdge(a, b, { class: "edgePath marker" });
            });
        }

        function escapeHTML(s) { 
            return s.replace(/[&<>'"]/g, c => ({'&':'&amp;','<':'&lt;','>':'&gt;',"'":'&#39;','"':'&quot;'}[c])); 
        }

        const svg = d3.select("#graph");
        const inner = d3.select("#g");
        const render = new dagreD3.render();

        function draw(filter = "all") {
            build(filter);
            inner.selectAll("*").remove();
            render(inner, g);
            
            const graphGroup = d3.select("#zoom");
            const zoom = d3.zoom().on("zoom", (e) => { 
                graphGroup.attr("transform", e.transform); 
            });
            svg.call(zoom);
            
            const { width, height } = svg.node().getBoundingClientRect();
            const graphWidth = g.graph().width || 800;
            const graphHeight = g.graph().height || 600;
            const scale = Math.min(1, Math.max(0.2, Math.min(width / (graphWidth + 80), height / (graphHeight + 80))));
            const tx = (width - graphWidth * scale) / 2 + 20;
            const ty = (height - graphHeight * scale) / 2;
            
            svg.transition().duration(400).call(
                zoom.transform, 
                d3.zoomIdentity.translate(tx, ty).scale(scale)
            );

            inner.selectAll("g.node").on("click", (_, id) => {
                const t = tasks.find(x => x.id === id);
                const prereqs = deps.filter(([a, b]) => b === id)
                    .map(([a]) => tasks.find(x => x.id === a)?.label || a);
                const dependents = deps.filter(([a, b]) => a === id)
                    .map(([, b]) => tasks.find(x => x.id === b)?.label || b);
                
                const body = `
                    <div class="font-semibold mb-2">${escapeHTML(t.label)}</div>
                    <div class="text-sm text-gray-300 mb-2">${escapeHTML(t.full)}</div>
                    <div class="text-xs text-gray-500 mb-3">
                        <span class="inline-block bg-gray-700 rounded px-2 py-1 mr-2">${escapeHTML(t.location)}</span>
                        <span class="inline-block bg-gray-700 rounded px-2 py-1">${escapeHTML(t.section)}</span>
                    </div>
                    <div class="text-xs mb-2"><span class="text-gray-500">Status:</span> <span class="font-medium ${t.status === 'doNext' ? 'text-green-400' : t.status === 'blocked' ? 'text-red-400' : 'text-blue-400'}">${t.status}</span></div>
                    <div class="text-xs mb-1"><span class="text-gray-500">Prerequisites:</span> ${prereqs.length ? prereqs.map(escapeHTML).join(", ") : "None"}</div>
                    <div class="text-xs"><span class="text-gray-500">Unblocks:</span> ${dependents.length ? dependents.map(escapeHTML).join(", ") : "—"}</div>
                `;
                document.querySelector("#detailBody").innerHTML = body;
            });
        }

        // Initial draw
        draw();

        // Filter buttons
        document.querySelectorAll(".filter-btn").forEach(btn => {
            btn.addEventListener("click", () => {
                document.querySelectorAll(".filter-btn").forEach(b => b.classList.remove("active"));
                btn.classList.add("active");
                draw(btn.dataset.filter);
            });
        });

        // Zoom controls
        const zoom = d3.zoom().scaleExtent([0.1, 3]).on("zoom", (event) => {
            d3.select("#zoom").attr("transform", event.transform);
        });
        svg.call(zoom);

        document.getElementById("zoomIn").addEventListener("click", () => {
            svg.transition().call(zoom.scaleBy, 1.2);
        });

        document.getElementById("zoomOut").addEventListener("click", () => {
            svg.transition().call(zoom.scaleBy, 0.8);
        });

        document.getElementById("fitView").addEventListener("click", () => {
            const { width, height } = svg.node().getBoundingClientRect();
            const graphWidth = g.graph().width || 800;
            const graphHeight = g.graph().height || 600;
            const scale = Math.min(1, Math.max(0.2, Math.min(width / (graphWidth + 80), height / (graphHeight + 80))));
            const tx = (width - graphWidth * scale) / 2 + 20;
            const ty = (height - graphHeight * scale) / 2;
            
            svg.transition().duration(400).call(
                zoom.transform, 
                d3.zoomIdentity.translate(tx, ty).scale(scale)
            );
        });
    </script>
</body>
</html>