@fbdo/smart-agentic-calendar 0.1.0
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/README.md +195 -0
- package/dist/analytics/allocation.d.ts +9 -0
- package/dist/analytics/allocation.d.ts.map +1 -0
- package/dist/analytics/allocation.js +27 -0
- package/dist/analytics/allocation.js.map +1 -0
- package/dist/analytics/analytics-engine.d.ts +17 -0
- package/dist/analytics/analytics-engine.d.ts.map +1 -0
- package/dist/analytics/analytics-engine.js +33 -0
- package/dist/analytics/analytics-engine.js.map +1 -0
- package/dist/analytics/estimation.d.ts +9 -0
- package/dist/analytics/estimation.d.ts.map +1 -0
- package/dist/analytics/estimation.js +74 -0
- package/dist/analytics/estimation.js.map +1 -0
- package/dist/analytics/health.d.ts +14 -0
- package/dist/analytics/health.d.ts.map +1 -0
- package/dist/analytics/health.js +146 -0
- package/dist/analytics/health.js.map +1 -0
- package/dist/analytics/period.d.ts +9 -0
- package/dist/analytics/period.d.ts.map +1 -0
- package/dist/analytics/period.js +41 -0
- package/dist/analytics/period.js.map +1 -0
- package/dist/analytics/productivity.d.ts +9 -0
- package/dist/analytics/productivity.d.ts.map +1 -0
- package/dist/analytics/productivity.js +34 -0
- package/dist/analytics/productivity.js.map +1 -0
- package/dist/common/constants.d.ts +13 -0
- package/dist/common/constants.d.ts.map +1 -0
- package/dist/common/constants.js +19 -0
- package/dist/common/constants.js.map +1 -0
- package/dist/common/id.d.ts +2 -0
- package/dist/common/id.d.ts.map +1 -0
- package/dist/common/id.js +5 -0
- package/dist/common/id.js.map +1 -0
- package/dist/common/time.d.ts +11 -0
- package/dist/common/time.d.ts.map +1 -0
- package/dist/common/time.js +67 -0
- package/dist/common/time.js.map +1 -0
- package/dist/engine/conflict-detector.d.ts +22 -0
- package/dist/engine/conflict-detector.d.ts.map +1 -0
- package/dist/engine/conflict-detector.js +194 -0
- package/dist/engine/conflict-detector.js.map +1 -0
- package/dist/engine/dependency-resolver.d.ts +8 -0
- package/dist/engine/dependency-resolver.d.ts.map +1 -0
- package/dist/engine/dependency-resolver.js +160 -0
- package/dist/engine/dependency-resolver.js.map +1 -0
- package/dist/engine/recurrence-manager.d.ts +24 -0
- package/dist/engine/recurrence-manager.d.ts.map +1 -0
- package/dist/engine/recurrence-manager.js +140 -0
- package/dist/engine/recurrence-manager.js.map +1 -0
- package/dist/engine/replan-coordinator.d.ts +24 -0
- package/dist/engine/replan-coordinator.d.ts.map +1 -0
- package/dist/engine/replan-coordinator.js +107 -0
- package/dist/engine/replan-coordinator.js.map +1 -0
- package/dist/engine/scheduler.d.ts +65 -0
- package/dist/engine/scheduler.d.ts.map +1 -0
- package/dist/engine/scheduler.js +368 -0
- package/dist/engine/scheduler.js.map +1 -0
- package/dist/index.d.ts +19 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +69 -0
- package/dist/index.js.map +1 -0
- package/dist/mcp/server.d.ts +25 -0
- package/dist/mcp/server.d.ts.map +1 -0
- package/dist/mcp/server.js +421 -0
- package/dist/mcp/server.js.map +1 -0
- package/dist/mcp/tools/analytics-tools.d.ts +45 -0
- package/dist/mcp/tools/analytics-tools.d.ts.map +1 -0
- package/dist/mcp/tools/analytics-tools.js +27 -0
- package/dist/mcp/tools/analytics-tools.js.map +1 -0
- package/dist/mcp/tools/config-tools.d.ts +55 -0
- package/dist/mcp/tools/config-tools.d.ts.map +1 -0
- package/dist/mcp/tools/config-tools.js +47 -0
- package/dist/mcp/tools/config-tools.js.map +1 -0
- package/dist/mcp/tools/event-tools.d.ts +50 -0
- package/dist/mcp/tools/event-tools.d.ts.map +1 -0
- package/dist/mcp/tools/event-tools.js +48 -0
- package/dist/mcp/tools/event-tools.js.map +1 -0
- package/dist/mcp/tools/schedule-tools.d.ts +80 -0
- package/dist/mcp/tools/schedule-tools.d.ts.map +1 -0
- package/dist/mcp/tools/schedule-tools.js +103 -0
- package/dist/mcp/tools/schedule-tools.js.map +1 -0
- package/dist/mcp/tools/task-tools.d.ts +106 -0
- package/dist/mcp/tools/task-tools.d.ts.map +1 -0
- package/dist/mcp/tools/task-tools.js +161 -0
- package/dist/mcp/tools/task-tools.js.map +1 -0
- package/dist/mcp/validators.d.ts +231 -0
- package/dist/mcp/validators.d.ts.map +1 -0
- package/dist/mcp/validators.js +405 -0
- package/dist/mcp/validators.js.map +1 -0
- package/dist/models/analytics.d.ts +55 -0
- package/dist/models/analytics.d.ts.map +1 -0
- package/dist/models/analytics.js +2 -0
- package/dist/models/analytics.js.map +1 -0
- package/dist/models/config.d.ts +32 -0
- package/dist/models/config.d.ts.map +1 -0
- package/dist/models/config.js +2 -0
- package/dist/models/config.js.map +1 -0
- package/dist/models/conflict.d.ts +21 -0
- package/dist/models/conflict.d.ts.map +1 -0
- package/dist/models/conflict.js +2 -0
- package/dist/models/conflict.js.map +1 -0
- package/dist/models/dependency.d.ts +5 -0
- package/dist/models/dependency.d.ts.map +1 -0
- package/dist/models/dependency.js +2 -0
- package/dist/models/dependency.js.map +1 -0
- package/dist/models/errors.d.ts +19 -0
- package/dist/models/errors.d.ts.map +1 -0
- package/dist/models/errors.js +29 -0
- package/dist/models/errors.js.map +1 -0
- package/dist/models/event.d.ts +11 -0
- package/dist/models/event.d.ts.map +1 -0
- package/dist/models/event.js +2 -0
- package/dist/models/event.js.map +1 -0
- package/dist/models/recurrence.d.ts +22 -0
- package/dist/models/recurrence.d.ts.map +1 -0
- package/dist/models/recurrence.js +2 -0
- package/dist/models/recurrence.js.map +1 -0
- package/dist/models/schedule.d.ts +16 -0
- package/dist/models/schedule.d.ts.map +1 -0
- package/dist/models/schedule.js +2 -0
- package/dist/models/schedule.js.map +1 -0
- package/dist/models/task.d.ts +21 -0
- package/dist/models/task.d.ts.map +1 -0
- package/dist/models/task.js +9 -0
- package/dist/models/task.js.map +1 -0
- package/dist/storage/analytics-repository.d.ts +17 -0
- package/dist/storage/analytics-repository.d.ts.map +1 -0
- package/dist/storage/analytics-repository.js +99 -0
- package/dist/storage/analytics-repository.js.map +1 -0
- package/dist/storage/config-repository.d.ts +20 -0
- package/dist/storage/config-repository.d.ts.map +1 -0
- package/dist/storage/config-repository.js +145 -0
- package/dist/storage/config-repository.js.map +1 -0
- package/dist/storage/database.d.ts +6 -0
- package/dist/storage/database.d.ts.map +1 -0
- package/dist/storage/database.js +160 -0
- package/dist/storage/database.js.map +1 -0
- package/dist/storage/event-repository.d.ts +16 -0
- package/dist/storage/event-repository.d.ts.map +1 -0
- package/dist/storage/event-repository.js +120 -0
- package/dist/storage/event-repository.js.map +1 -0
- package/dist/storage/recurrence-repository.d.ts +18 -0
- package/dist/storage/recurrence-repository.d.ts.map +1 -0
- package/dist/storage/recurrence-repository.js +99 -0
- package/dist/storage/recurrence-repository.js.map +1 -0
- package/dist/storage/schedule-repository.d.ts +13 -0
- package/dist/storage/schedule-repository.d.ts.map +1 -0
- package/dist/storage/schedule-repository.js +53 -0
- package/dist/storage/schedule-repository.js.map +1 -0
- package/dist/storage/task-repository.d.ts +29 -0
- package/dist/storage/task-repository.d.ts.map +1 -0
- package/dist/storage/task-repository.js +235 -0
- package/dist/storage/task-repository.js.map +1 -0
- package/package.json +77 -0
package/README.md
ADDED
|
@@ -0,0 +1,195 @@
|
|
|
1
|
+
# Smart Agentic Calendar
|
|
2
|
+
|
|
3
|
+
A local-first smart calendar MCP server for AI agents. Inspired by [Reclaim.ai](https://reclaim.ai/) and [Motion](https://www.usemotion.com/), it automatically re-plans tasks, recalculates the schedule, and helps AI agents act as personal scheduling assistants.
|
|
4
|
+
|
|
5
|
+
## What It Does
|
|
6
|
+
|
|
7
|
+
- **Time-blocking scheduler** that assigns specific time slots to tasks based on deadlines, durations, and priorities (P1-P4)
|
|
8
|
+
- **Automatic replanning** triggered on any change (create, update, delete, complete) — runs in the background so tool responses are instant
|
|
9
|
+
- **Constraint satisfaction** engine that respects hard constraints (deadlines, availability, events, dependencies) and optimizes soft constraints (priority, focus time, buffer time)
|
|
10
|
+
- **Focus time protection** for deep work blocks with minimum uninterrupted duration enforcement
|
|
11
|
+
- **Task splitting** across multiple time slots when no single slot fits
|
|
12
|
+
- **Recurring tasks** with full RRULE support (daily, weekly, monthly, yearly, custom patterns)
|
|
13
|
+
- **Task dependencies** with cycle detection
|
|
14
|
+
- **Conflict detection** that identifies at-risk tasks and suggests deprioritizations
|
|
15
|
+
- **Analytics** — completion rates, schedule health, estimation accuracy, time allocation by category
|
|
16
|
+
|
|
17
|
+
## Architecture
|
|
18
|
+
|
|
19
|
+
```
|
|
20
|
+
src/
|
|
21
|
+
models/ Domain types (Task, Event, TimeBlock, Config, etc.)
|
|
22
|
+
common/ Shared utilities (ID generation, time functions, constants)
|
|
23
|
+
storage/ SQLite database layer (better-sqlite3)
|
|
24
|
+
engine/ Scheduling engine, replanning, conflict detection, recurrence
|
|
25
|
+
analytics/ Productivity stats, schedule health, estimation accuracy
|
|
26
|
+
mcp/ MCP server and tool handlers
|
|
27
|
+
index.ts Composition root and entry point
|
|
28
|
+
```
|
|
29
|
+
|
|
30
|
+
Single-user, single-process, local-first. All data stored in a local SQLite database with no external service dependencies.
|
|
31
|
+
|
|
32
|
+
## Tech Stack
|
|
33
|
+
|
|
34
|
+
| Component | Choice |
|
|
35
|
+
|-----------|--------|
|
|
36
|
+
| Language | TypeScript (strict mode) |
|
|
37
|
+
| Runtime | Node.js >= 20 |
|
|
38
|
+
| Storage | SQLite via [better-sqlite3](https://github.com/WiseLibs/better-sqlite3) |
|
|
39
|
+
| MCP Transport | stdio |
|
|
40
|
+
| Test Runner | [Vitest](https://vitest.dev/) |
|
|
41
|
+
| Scheduling | Constraint satisfaction algorithm |
|
|
42
|
+
|
|
43
|
+
## Getting Started
|
|
44
|
+
|
|
45
|
+
### Prerequisites
|
|
46
|
+
|
|
47
|
+
- Node.js >= 20
|
|
48
|
+
|
|
49
|
+
### Install and Build
|
|
50
|
+
|
|
51
|
+
```bash
|
|
52
|
+
npm install
|
|
53
|
+
npm run build
|
|
54
|
+
```
|
|
55
|
+
|
|
56
|
+
This compiles TypeScript to `dist/` and produces the runnable server at `dist/index.js`.
|
|
57
|
+
|
|
58
|
+
### Test
|
|
59
|
+
|
|
60
|
+
```bash
|
|
61
|
+
npm test # run all tests (578 tests)
|
|
62
|
+
npm run test:watch # watch mode
|
|
63
|
+
npm run test:coverage # with coverage
|
|
64
|
+
```
|
|
65
|
+
|
|
66
|
+
### Quality Checks
|
|
67
|
+
|
|
68
|
+
```bash
|
|
69
|
+
npm run lint # eslint
|
|
70
|
+
npm run format:check # prettier
|
|
71
|
+
npm run quality # all checks (lint, format, duplication, unused code, dependency rules, security)
|
|
72
|
+
```
|
|
73
|
+
|
|
74
|
+
## Connecting to an MCP-Compatible Agent
|
|
75
|
+
|
|
76
|
+
This server communicates over **stdio** using the [Model Context Protocol](https://modelcontextprotocol.io/). Any MCP-compatible client can connect to it by spawning the server as a subprocess.
|
|
77
|
+
|
|
78
|
+
### Configuration
|
|
79
|
+
|
|
80
|
+
The server accepts one environment variable:
|
|
81
|
+
|
|
82
|
+
| Variable | Default | Description |
|
|
83
|
+
|----------|---------|-------------|
|
|
84
|
+
| `CALENDAR_DB_PATH` | `./calendar.db` | Path to the SQLite database file |
|
|
85
|
+
|
|
86
|
+
### Claude Desktop
|
|
87
|
+
|
|
88
|
+
Add to `~/Library/Application Support/Claude/claude_desktop_config.json` (macOS) or `%APPDATA%\Claude\claude_desktop_config.json` (Windows):
|
|
89
|
+
|
|
90
|
+
```json
|
|
91
|
+
{
|
|
92
|
+
"mcpServers": {
|
|
93
|
+
"smart-agentic-calendar": {
|
|
94
|
+
"command": "npx",
|
|
95
|
+
"args": ["-y", "smart-agentic-calendar"],
|
|
96
|
+
"env": {
|
|
97
|
+
"CALENDAR_DB_PATH": "/absolute/path/to/calendar.db"
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
```
|
|
103
|
+
|
|
104
|
+
### Claude Code (CLI)
|
|
105
|
+
|
|
106
|
+
Add to your project's `.mcp.json` or global `~/.claude/settings.json`:
|
|
107
|
+
|
|
108
|
+
```json
|
|
109
|
+
{
|
|
110
|
+
"mcpServers": {
|
|
111
|
+
"smart-agentic-calendar": {
|
|
112
|
+
"command": "npx",
|
|
113
|
+
"args": ["-y", "smart-agentic-calendar"],
|
|
114
|
+
"env": {
|
|
115
|
+
"CALENDAR_DB_PATH": "/absolute/path/to/calendar.db"
|
|
116
|
+
}
|
|
117
|
+
}
|
|
118
|
+
}
|
|
119
|
+
}
|
|
120
|
+
```
|
|
121
|
+
|
|
122
|
+
### Cursor
|
|
123
|
+
|
|
124
|
+
Add to `.cursor/mcp.json` in your project root:
|
|
125
|
+
|
|
126
|
+
```json
|
|
127
|
+
{
|
|
128
|
+
"mcpServers": {
|
|
129
|
+
"smart-agentic-calendar": {
|
|
130
|
+
"command": "npx",
|
|
131
|
+
"args": ["-y", "smart-agentic-calendar"],
|
|
132
|
+
"env": {
|
|
133
|
+
"CALENDAR_DB_PATH": "/absolute/path/to/calendar.db"
|
|
134
|
+
}
|
|
135
|
+
}
|
|
136
|
+
}
|
|
137
|
+
}
|
|
138
|
+
```
|
|
139
|
+
|
|
140
|
+
### Any MCP Client (Generic)
|
|
141
|
+
|
|
142
|
+
Spawn the server as a child process with stdio transport:
|
|
143
|
+
|
|
144
|
+
```bash
|
|
145
|
+
npx -y smart-agentic-calendar
|
|
146
|
+
```
|
|
147
|
+
|
|
148
|
+
Or with a custom database path:
|
|
149
|
+
|
|
150
|
+
```bash
|
|
151
|
+
CALENDAR_DB_PATH=/path/to/calendar.db npx -y smart-agentic-calendar
|
|
152
|
+
```
|
|
153
|
+
|
|
154
|
+
The server reads JSON-RPC messages from stdin and writes responses to stdout. Diagnostic messages go to stderr.
|
|
155
|
+
|
|
156
|
+
## MCP Tools
|
|
157
|
+
|
|
158
|
+
### Task Management
|
|
159
|
+
- `create_task` — create a task with title, duration, deadline, priority, category, tags
|
|
160
|
+
- `get_task` — retrieve a task by ID
|
|
161
|
+
- `update_task` — update any task field (triggers background replan)
|
|
162
|
+
- `delete_task` — soft-delete a task (triggers background replan)
|
|
163
|
+
- `complete_task` — mark a task done, optionally record actual duration
|
|
164
|
+
- `list_tasks` — query tasks with filters (status, priority, deadline range, category)
|
|
165
|
+
|
|
166
|
+
### Event Management
|
|
167
|
+
- `create_event` — create a fixed calendar event that blocks time
|
|
168
|
+
- `update_event` — modify an event
|
|
169
|
+
- `delete_event` — remove an event
|
|
170
|
+
- `list_events` — query events by date range
|
|
171
|
+
|
|
172
|
+
### Schedule & Conflicts
|
|
173
|
+
- `get_schedule` — view scheduled time blocks for a date range (returns `schedule_status`: "up_to_date" or "replan_in_progress")
|
|
174
|
+
- `replan` — force a synchronous replan (the only blocking replan path)
|
|
175
|
+
- `get_conflicts` — view at-risk tasks and deadline conflicts
|
|
176
|
+
|
|
177
|
+
### Analytics
|
|
178
|
+
- `get_productivity_stats` — completion rates, on-time rates by period
|
|
179
|
+
- `get_schedule_health` — composite health score, utilization, overdue/at-risk counts
|
|
180
|
+
- `get_time_allocation` — hours breakdown by category
|
|
181
|
+
- `get_estimation_accuracy` — estimated vs. actual duration analysis
|
|
182
|
+
|
|
183
|
+
### Configuration
|
|
184
|
+
- `set_availability` — define work hours per day of week
|
|
185
|
+
- `set_focus_time` — define focus/deep-work blocks
|
|
186
|
+
- `set_preferences` — buffer time, default priority, scheduling horizon, minimum block size
|
|
187
|
+
- `get_preferences` — retrieve current configuration
|
|
188
|
+
|
|
189
|
+
## Development
|
|
190
|
+
|
|
191
|
+
This project follows **test-driven development** (red-green-refactor) with a healthy test pyramid and dependency injection for testability. Property-based tests are used for pure scheduling functions and serialization round-trips.
|
|
192
|
+
|
|
193
|
+
## License
|
|
194
|
+
|
|
195
|
+
MIT
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
import type { AnalyticsRepository } from "../storage/analytics-repository.js";
|
|
2
|
+
import type { TimeAllocation } from "../models/analytics.js";
|
|
3
|
+
import type { Period } from "./period.js";
|
|
4
|
+
export declare class AllocationCalculator {
|
|
5
|
+
private readonly analyticsRepo;
|
|
6
|
+
constructor(analyticsRepo: AnalyticsRepository);
|
|
7
|
+
compute(period: Period, referenceDate?: Date): TimeAllocation;
|
|
8
|
+
}
|
|
9
|
+
//# sourceMappingURL=allocation.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"allocation.d.ts","sourceRoot":"","sources":["../../src/analytics/allocation.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,mBAAmB,EAAE,MAAM,oCAAoC,CAAC;AAC9E,OAAO,KAAK,EAAE,cAAc,EAAsB,MAAM,wBAAwB,CAAC;AACjF,OAAO,KAAK,EAAE,MAAM,EAAE,MAAM,aAAa,CAAC;AAG1C,qBAAa,oBAAoB;IAC/B,OAAO,CAAC,QAAQ,CAAC,aAAa,CAAsB;gBAExC,aAAa,EAAE,mBAAmB;IAI9C,OAAO,CAAC,MAAM,EAAE,MAAM,EAAE,aAAa,CAAC,EAAE,IAAI,GAAG,cAAc;CAoB9D"}
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
import { resolvePeriod } from "./period.js";
|
|
2
|
+
export class AllocationCalculator {
|
|
3
|
+
analyticsRepo;
|
|
4
|
+
constructor(analyticsRepo) {
|
|
5
|
+
this.analyticsRepo = analyticsRepo;
|
|
6
|
+
}
|
|
7
|
+
compute(period, referenceDate) {
|
|
8
|
+
const range = resolvePeriod(period, referenceDate);
|
|
9
|
+
const summaries = this.analyticsRepo.getTasksByCategory(range.start, range.end);
|
|
10
|
+
if (summaries.length === 0) {
|
|
11
|
+
return { period, categories: [] };
|
|
12
|
+
}
|
|
13
|
+
const totalMinutes = summaries.reduce((sum, s) => sum + s.totalMinutes, 0);
|
|
14
|
+
const categories = summaries
|
|
15
|
+
.map((s) => ({
|
|
16
|
+
category: s.category,
|
|
17
|
+
hours: round2(s.totalMinutes / 60),
|
|
18
|
+
percentage: totalMinutes === 0 ? 0 : round2((s.totalMinutes / totalMinutes) * 100),
|
|
19
|
+
}))
|
|
20
|
+
.sort((a, b) => b.hours - a.hours);
|
|
21
|
+
return { period, categories };
|
|
22
|
+
}
|
|
23
|
+
}
|
|
24
|
+
function round2(value) {
|
|
25
|
+
return Math.round(value * 100) / 100;
|
|
26
|
+
}
|
|
27
|
+
//# sourceMappingURL=allocation.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"allocation.js","sourceRoot":"","sources":["../../src/analytics/allocation.ts"],"names":[],"mappings":"AAGA,OAAO,EAAE,aAAa,EAAE,MAAM,aAAa,CAAC;AAE5C,MAAM,OAAO,oBAAoB;IACd,aAAa,CAAsB;IAEpD,YAAY,aAAkC;QAC5C,IAAI,CAAC,aAAa,GAAG,aAAa,CAAC;IACrC,CAAC;IAED,OAAO,CAAC,MAAc,EAAE,aAAoB;QAC1C,MAAM,KAAK,GAAG,aAAa,CAAC,MAAM,EAAE,aAAa,CAAC,CAAC;QACnD,MAAM,SAAS,GAAG,IAAI,CAAC,aAAa,CAAC,kBAAkB,CAAC,KAAK,CAAC,KAAK,EAAE,KAAK,CAAC,GAAG,CAAC,CAAC;QAEhF,IAAI,SAAS,CAAC,MAAM,KAAK,CAAC,EAAE,CAAC;YAC3B,OAAO,EAAE,MAAM,EAAE,UAAU,EAAE,EAAE,EAAE,CAAC;QACpC,CAAC;QAED,MAAM,YAAY,GAAG,SAAS,CAAC,MAAM,CAAC,CAAC,GAAG,EAAE,CAAC,EAAE,EAAE,CAAC,GAAG,GAAG,CAAC,CAAC,YAAY,EAAE,CAAC,CAAC,CAAC;QAE3E,MAAM,UAAU,GAAyB,SAAS;aAC/C,GAAG,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC;YACX,QAAQ,EAAE,CAAC,CAAC,QAAQ;YACpB,KAAK,EAAE,MAAM,CAAC,CAAC,CAAC,YAAY,GAAG,EAAE,CAAC;YAClC,UAAU,EAAE,YAAY,KAAK,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,MAAM,CAAC,CAAC,CAAC,CAAC,YAAY,GAAG,YAAY,CAAC,GAAG,GAAG,CAAC;SACnF,CAAC,CAAC;aACF,IAAI,CAAC,CAAC,CAAC,EAAE,CAAC,EAAE,EAAE,CAAC,CAAC,CAAC,KAAK,GAAG,CAAC,CAAC,KAAK,CAAC,CAAC;QAErC,OAAO,EAAE,MAAM,EAAE,UAAU,EAAE,CAAC;IAChC,CAAC;CACF;AAED,SAAS,MAAM,CAAC,KAAa;IAC3B,OAAO,IAAI,CAAC,KAAK,CAAC,KAAK,GAAG,GAAG,CAAC,GAAG,GAAG,CAAC;AACvC,CAAC"}
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
import type { AnalyticsRepository } from "../storage/analytics-repository.js";
|
|
2
|
+
import type { TaskRepository } from "../storage/task-repository.js";
|
|
3
|
+
import type { ScheduleRepository } from "../storage/schedule-repository.js";
|
|
4
|
+
import type { ConfigRepository } from "../storage/config-repository.js";
|
|
5
|
+
import type { ProductivityStats, ScheduleHealth, EstimationAccuracy, TimeAllocation } from "../models/analytics.js";
|
|
6
|
+
export declare class AnalyticsEngine {
|
|
7
|
+
private readonly productivity;
|
|
8
|
+
private readonly health;
|
|
9
|
+
private readonly estimation;
|
|
10
|
+
private readonly allocation;
|
|
11
|
+
constructor(analyticsRepo: AnalyticsRepository, taskRepo: TaskRepository, scheduleRepo: ScheduleRepository, configRepo: ConfigRepository);
|
|
12
|
+
getProductivityStats(period: string, referenceDate?: Date): ProductivityStats;
|
|
13
|
+
getScheduleHealth(referenceDate?: Date): ScheduleHealth;
|
|
14
|
+
getEstimationAccuracy(period: string, referenceDate?: Date): EstimationAccuracy;
|
|
15
|
+
getTimeAllocation(period: string, referenceDate?: Date): TimeAllocation;
|
|
16
|
+
}
|
|
17
|
+
//# sourceMappingURL=analytics-engine.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"analytics-engine.d.ts","sourceRoot":"","sources":["../../src/analytics/analytics-engine.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,mBAAmB,EAAE,MAAM,oCAAoC,CAAC;AAC9E,OAAO,KAAK,EAAE,cAAc,EAAE,MAAM,+BAA+B,CAAC;AACpE,OAAO,KAAK,EAAE,kBAAkB,EAAE,MAAM,mCAAmC,CAAC;AAC5E,OAAO,KAAK,EAAE,gBAAgB,EAAE,MAAM,iCAAiC,CAAC;AACxE,OAAO,KAAK,EACV,iBAAiB,EACjB,cAAc,EACd,kBAAkB,EAClB,cAAc,EACf,MAAM,wBAAwB,CAAC;AAOhC,qBAAa,eAAe;IAC1B,OAAO,CAAC,QAAQ,CAAC,YAAY,CAAyB;IACtD,OAAO,CAAC,QAAQ,CAAC,MAAM,CAAmB;IAC1C,OAAO,CAAC,QAAQ,CAAC,UAAU,CAAuB;IAClD,OAAO,CAAC,QAAQ,CAAC,UAAU,CAAuB;gBAGhD,aAAa,EAAE,mBAAmB,EAClC,QAAQ,EAAE,cAAc,EACxB,YAAY,EAAE,kBAAkB,EAChC,UAAU,EAAE,gBAAgB;IAQ9B,oBAAoB,CAAC,MAAM,EAAE,MAAM,EAAE,aAAa,CAAC,EAAE,IAAI,GAAG,iBAAiB;IAK7E,iBAAiB,CAAC,aAAa,CAAC,EAAE,IAAI,GAAG,cAAc;IAIvD,qBAAqB,CAAC,MAAM,EAAE,MAAM,EAAE,aAAa,CAAC,EAAE,IAAI,GAAG,kBAAkB;IAK/E,iBAAiB,CAAC,MAAM,EAAE,MAAM,EAAE,aAAa,CAAC,EAAE,IAAI,GAAG,cAAc;CAIxE"}
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
import { validatePeriod } from "./period.js";
|
|
2
|
+
import { ProductivityCalculator } from "./productivity.js";
|
|
3
|
+
import { HealthCalculator } from "./health.js";
|
|
4
|
+
import { EstimationCalculator } from "./estimation.js";
|
|
5
|
+
import { AllocationCalculator } from "./allocation.js";
|
|
6
|
+
export class AnalyticsEngine {
|
|
7
|
+
productivity;
|
|
8
|
+
health;
|
|
9
|
+
estimation;
|
|
10
|
+
allocation;
|
|
11
|
+
constructor(analyticsRepo, taskRepo, scheduleRepo, configRepo) {
|
|
12
|
+
this.productivity = new ProductivityCalculator(analyticsRepo);
|
|
13
|
+
this.health = new HealthCalculator(analyticsRepo, taskRepo, scheduleRepo, configRepo);
|
|
14
|
+
this.estimation = new EstimationCalculator(analyticsRepo);
|
|
15
|
+
this.allocation = new AllocationCalculator(analyticsRepo);
|
|
16
|
+
}
|
|
17
|
+
getProductivityStats(period, referenceDate) {
|
|
18
|
+
validatePeriod(period);
|
|
19
|
+
return this.productivity.compute(period, referenceDate);
|
|
20
|
+
}
|
|
21
|
+
getScheduleHealth(referenceDate) {
|
|
22
|
+
return this.health.compute(referenceDate);
|
|
23
|
+
}
|
|
24
|
+
getEstimationAccuracy(period, referenceDate) {
|
|
25
|
+
validatePeriod(period);
|
|
26
|
+
return this.estimation.compute(period, referenceDate);
|
|
27
|
+
}
|
|
28
|
+
getTimeAllocation(period, referenceDate) {
|
|
29
|
+
validatePeriod(period);
|
|
30
|
+
return this.allocation.compute(period, referenceDate);
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
//# sourceMappingURL=analytics-engine.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"analytics-engine.js","sourceRoot":"","sources":["../../src/analytics/analytics-engine.ts"],"names":[],"mappings":"AAUA,OAAO,EAAE,cAAc,EAAE,MAAM,aAAa,CAAC;AAC7C,OAAO,EAAE,sBAAsB,EAAE,MAAM,mBAAmB,CAAC;AAC3D,OAAO,EAAE,gBAAgB,EAAE,MAAM,aAAa,CAAC;AAC/C,OAAO,EAAE,oBAAoB,EAAE,MAAM,iBAAiB,CAAC;AACvD,OAAO,EAAE,oBAAoB,EAAE,MAAM,iBAAiB,CAAC;AAEvD,MAAM,OAAO,eAAe;IACT,YAAY,CAAyB;IACrC,MAAM,CAAmB;IACzB,UAAU,CAAuB;IACjC,UAAU,CAAuB;IAElD,YACE,aAAkC,EAClC,QAAwB,EACxB,YAAgC,EAChC,UAA4B;QAE5B,IAAI,CAAC,YAAY,GAAG,IAAI,sBAAsB,CAAC,aAAa,CAAC,CAAC;QAC9D,IAAI,CAAC,MAAM,GAAG,IAAI,gBAAgB,CAAC,aAAa,EAAE,QAAQ,EAAE,YAAY,EAAE,UAAU,CAAC,CAAC;QACtF,IAAI,CAAC,UAAU,GAAG,IAAI,oBAAoB,CAAC,aAAa,CAAC,CAAC;QAC1D,IAAI,CAAC,UAAU,GAAG,IAAI,oBAAoB,CAAC,aAAa,CAAC,CAAC;IAC5D,CAAC;IAED,oBAAoB,CAAC,MAAc,EAAE,aAAoB;QACvD,cAAc,CAAC,MAAM,CAAC,CAAC;QACvB,OAAO,IAAI,CAAC,YAAY,CAAC,OAAO,CAAC,MAAM,EAAE,aAAa,CAAC,CAAC;IAC1D,CAAC;IAED,iBAAiB,CAAC,aAAoB;QACpC,OAAO,IAAI,CAAC,MAAM,CAAC,OAAO,CAAC,aAAa,CAAC,CAAC;IAC5C,CAAC;IAED,qBAAqB,CAAC,MAAc,EAAE,aAAoB;QACxD,cAAc,CAAC,MAAM,CAAC,CAAC;QACvB,OAAO,IAAI,CAAC,UAAU,CAAC,OAAO,CAAC,MAAM,EAAE,aAAa,CAAC,CAAC;IACxD,CAAC;IAED,iBAAiB,CAAC,MAAc,EAAE,aAAoB;QACpD,cAAc,CAAC,MAAM,CAAC,CAAC;QACvB,OAAO,IAAI,CAAC,UAAU,CAAC,OAAO,CAAC,MAAM,EAAE,aAAa,CAAC,CAAC;IACxD,CAAC;CACF"}
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
import type { AnalyticsRepository } from "../storage/analytics-repository.js";
|
|
2
|
+
import type { EstimationAccuracy } from "../models/analytics.js";
|
|
3
|
+
import type { Period } from "./period.js";
|
|
4
|
+
export declare class EstimationCalculator {
|
|
5
|
+
private readonly analyticsRepo;
|
|
6
|
+
constructor(analyticsRepo: AnalyticsRepository);
|
|
7
|
+
compute(period: Period, referenceDate?: Date): EstimationAccuracy;
|
|
8
|
+
}
|
|
9
|
+
//# sourceMappingURL=estimation.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"estimation.d.ts","sourceRoot":"","sources":["../../src/analytics/estimation.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,mBAAmB,EAAE,MAAM,oCAAoC,CAAC;AAC9E,OAAO,KAAK,EAAE,kBAAkB,EAAkB,MAAM,wBAAwB,CAAC;AACjF,OAAO,KAAK,EAAE,MAAM,EAAE,MAAM,aAAa,CAAC;AAG1C,qBAAa,oBAAoB;IAC/B,OAAO,CAAC,QAAQ,CAAC,aAAa,CAAsB;gBAExC,aAAa,EAAE,mBAAmB;IAI9C,OAAO,CAAC,MAAM,EAAE,MAAM,EAAE,aAAa,CAAC,EAAE,IAAI,GAAG,kBAAkB;CAkDlE"}
|
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
import { resolvePeriod } from "./period.js";
|
|
2
|
+
export class EstimationCalculator {
|
|
3
|
+
analyticsRepo;
|
|
4
|
+
constructor(analyticsRepo) {
|
|
5
|
+
this.analyticsRepo = analyticsRepo;
|
|
6
|
+
}
|
|
7
|
+
compute(period, referenceDate) {
|
|
8
|
+
const range = resolvePeriod(period, referenceDate);
|
|
9
|
+
const records = this.analyticsRepo.getDurationRecords(range.start, range.end);
|
|
10
|
+
if (records.length === 0) {
|
|
11
|
+
return {
|
|
12
|
+
averageAccuracyPercentage: null,
|
|
13
|
+
overestimateCount: 0,
|
|
14
|
+
underestimateCount: 0,
|
|
15
|
+
averageOverestimateMinutes: null,
|
|
16
|
+
averageUnderestimateMinutes: null,
|
|
17
|
+
accuracyByCategory: null,
|
|
18
|
+
};
|
|
19
|
+
}
|
|
20
|
+
const accuracies = records.map((r) => perTaskAccuracy(r.estimatedMinutes, r.actualMinutes));
|
|
21
|
+
const averageAccuracyPercentage = round2(accuracies.reduce((sum, a) => sum + a, 0) / accuracies.length);
|
|
22
|
+
const overestimates = records.filter((r) => r.actualMinutes < r.estimatedMinutes);
|
|
23
|
+
const underestimates = records.filter((r) => r.actualMinutes > r.estimatedMinutes);
|
|
24
|
+
const averageOverestimateMinutes = overestimates.length === 0
|
|
25
|
+
? null
|
|
26
|
+
: round1(overestimates.reduce((sum, r) => sum + (r.estimatedMinutes - r.actualMinutes), 0) /
|
|
27
|
+
overestimates.length);
|
|
28
|
+
const averageUnderestimateMinutes = underestimates.length === 0
|
|
29
|
+
? null
|
|
30
|
+
: round1(underestimates.reduce((sum, r) => sum + (r.actualMinutes - r.estimatedMinutes), 0) /
|
|
31
|
+
underestimates.length);
|
|
32
|
+
const accuracyByCategory = computeCategoryAccuracy(records);
|
|
33
|
+
return {
|
|
34
|
+
averageAccuracyPercentage,
|
|
35
|
+
overestimateCount: overestimates.length,
|
|
36
|
+
underestimateCount: underestimates.length,
|
|
37
|
+
averageOverestimateMinutes,
|
|
38
|
+
averageUnderestimateMinutes,
|
|
39
|
+
accuracyByCategory,
|
|
40
|
+
};
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
function perTaskAccuracy(estimated, actual) {
|
|
44
|
+
if (estimated === 0)
|
|
45
|
+
return 0;
|
|
46
|
+
const deviation = (Math.abs(actual - estimated) / estimated) * 100;
|
|
47
|
+
return Math.max(0, 100 - deviation);
|
|
48
|
+
}
|
|
49
|
+
function computeCategoryAccuracy(records) {
|
|
50
|
+
const groups = new Map();
|
|
51
|
+
for (const record of records) {
|
|
52
|
+
const category = record.category ?? "uncategorized";
|
|
53
|
+
const accuracy = perTaskAccuracy(record.estimatedMinutes, record.actualMinutes);
|
|
54
|
+
if (!groups.has(category)) {
|
|
55
|
+
groups.set(category, []);
|
|
56
|
+
}
|
|
57
|
+
const group = groups.get(category);
|
|
58
|
+
if (group) {
|
|
59
|
+
group.push(accuracy);
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
const result = {};
|
|
63
|
+
for (const [category, accuracies] of groups) {
|
|
64
|
+
result[category] = round2(accuracies.reduce((sum, a) => sum + a, 0) / accuracies.length);
|
|
65
|
+
}
|
|
66
|
+
return result;
|
|
67
|
+
}
|
|
68
|
+
function round2(value) {
|
|
69
|
+
return Math.round(value * 100) / 100;
|
|
70
|
+
}
|
|
71
|
+
function round1(value) {
|
|
72
|
+
return Math.round(value * 10) / 10;
|
|
73
|
+
}
|
|
74
|
+
//# sourceMappingURL=estimation.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"estimation.js","sourceRoot":"","sources":["../../src/analytics/estimation.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
import type { AnalyticsRepository } from "../storage/analytics-repository.js";
|
|
2
|
+
import type { TaskRepository } from "../storage/task-repository.js";
|
|
3
|
+
import type { ScheduleRepository } from "../storage/schedule-repository.js";
|
|
4
|
+
import type { ConfigRepository } from "../storage/config-repository.js";
|
|
5
|
+
import type { ScheduleHealth } from "../models/analytics.js";
|
|
6
|
+
export declare class HealthCalculator {
|
|
7
|
+
private readonly analyticsRepo;
|
|
8
|
+
private readonly taskRepo;
|
|
9
|
+
private readonly scheduleRepo;
|
|
10
|
+
private readonly configRepo;
|
|
11
|
+
constructor(analyticsRepo: AnalyticsRepository, taskRepo: TaskRepository, scheduleRepo: ScheduleRepository, configRepo: ConfigRepository);
|
|
12
|
+
compute(referenceDate?: Date): ScheduleHealth;
|
|
13
|
+
}
|
|
14
|
+
//# sourceMappingURL=health.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"health.d.ts","sourceRoot":"","sources":["../../src/analytics/health.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,mBAAmB,EAAE,MAAM,oCAAoC,CAAC;AAC9E,OAAO,KAAK,EAAE,cAAc,EAAE,MAAM,+BAA+B,CAAC;AACpE,OAAO,KAAK,EAAE,kBAAkB,EAAE,MAAM,mCAAmC,CAAC;AAC5E,OAAO,KAAK,EAAE,gBAAgB,EAAE,MAAM,iCAAiC,CAAC;AACxE,OAAO,KAAK,EAAE,cAAc,EAAE,MAAM,wBAAwB,CAAC;AAe7D,qBAAa,gBAAgB;IAC3B,OAAO,CAAC,QAAQ,CAAC,aAAa,CAAsB;IACpD,OAAO,CAAC,QAAQ,CAAC,QAAQ,CAAiB;IAC1C,OAAO,CAAC,QAAQ,CAAC,YAAY,CAAqB;IAClD,OAAO,CAAC,QAAQ,CAAC,UAAU,CAAmB;gBAG5C,aAAa,EAAE,mBAAmB,EAClC,QAAQ,EAAE,cAAc,EACxB,YAAY,EAAE,kBAAkB,EAChC,UAAU,EAAE,gBAAgB;IAQ9B,OAAO,CAAC,aAAa,CAAC,EAAE,IAAI,GAAG,cAAc;CAyC9C"}
|
|
@@ -0,0 +1,146 @@
|
|
|
1
|
+
import { resolvePeriod } from "./period.js";
|
|
2
|
+
import { diffMinutes } from "../common/time.js";
|
|
3
|
+
const DAY_NAMES = ["Sunday", "Monday", "Tuesday", "Wednesday", "Thursday", "Friday", "Saturday"];
|
|
4
|
+
const OVERDUE_WEIGHT = 15;
|
|
5
|
+
const AT_RISK_WEIGHT = 10;
|
|
6
|
+
const OVER_UTILIZATION_THRESHOLD = 90;
|
|
7
|
+
const OVER_UTILIZATION_MULTIPLIER = 2;
|
|
8
|
+
const UNDER_UTILIZATION_THRESHOLD = 20;
|
|
9
|
+
const UNDER_UTILIZATION_MULTIPLIER = 1;
|
|
10
|
+
export class HealthCalculator {
|
|
11
|
+
analyticsRepo;
|
|
12
|
+
taskRepo;
|
|
13
|
+
scheduleRepo;
|
|
14
|
+
configRepo;
|
|
15
|
+
constructor(analyticsRepo, taskRepo, scheduleRepo, configRepo) {
|
|
16
|
+
this.analyticsRepo = analyticsRepo;
|
|
17
|
+
this.taskRepo = taskRepo;
|
|
18
|
+
this.scheduleRepo = scheduleRepo;
|
|
19
|
+
this.configRepo = configRepo;
|
|
20
|
+
}
|
|
21
|
+
compute(referenceDate) {
|
|
22
|
+
const ref = referenceDate ?? new Date();
|
|
23
|
+
const weekRange = resolvePeriod("week", ref);
|
|
24
|
+
const config = this.configRepo.getFullConfig();
|
|
25
|
+
const timeBlocks = this.scheduleRepo.getSchedule(weekRange.start, weekRange.end);
|
|
26
|
+
const overdueTasks = this.analyticsRepo.getOverdueTasks(ref.toISOString());
|
|
27
|
+
const atRiskTasks = this.taskRepo.findAll({ status: "at_risk" });
|
|
28
|
+
const overdueCount = overdueTasks.length;
|
|
29
|
+
const atRiskCount = atRiskTasks.length;
|
|
30
|
+
const availablePerDay = computeAvailableMinutesPerDay(config.availability, weekRange.start);
|
|
31
|
+
const scheduledPerDay = computeScheduledMinutesPerDay(timeBlocks, weekRange.start);
|
|
32
|
+
const totalAvailable = sumValues(availablePerDay);
|
|
33
|
+
const totalScheduled = sumValues(scheduledPerDay);
|
|
34
|
+
const utilizationPercentage = totalAvailable === 0 ? 0 : round2((totalScheduled / totalAvailable) * 100);
|
|
35
|
+
const freeHoursThisWeek = totalAvailable === 0 ? 0 : round1(Math.max(0, (totalAvailable - totalScheduled) / 60));
|
|
36
|
+
const { busiestDay, lightestDay } = findBusiestLightestDay(scheduledPerDay, availablePerDay, weekRange.start);
|
|
37
|
+
const healthScore = computeHealthScore(overdueCount, atRiskCount, utilizationPercentage);
|
|
38
|
+
return {
|
|
39
|
+
healthScore,
|
|
40
|
+
utilizationPercentage,
|
|
41
|
+
overdueCount,
|
|
42
|
+
atRiskCount,
|
|
43
|
+
freeHoursThisWeek,
|
|
44
|
+
busiestDay,
|
|
45
|
+
lightestDay,
|
|
46
|
+
};
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
function computeHealthScore(overdueCount, atRiskCount, utilizationPercentage) {
|
|
50
|
+
let score = 100;
|
|
51
|
+
score -= overdueCount * OVERDUE_WEIGHT;
|
|
52
|
+
score -= atRiskCount * AT_RISK_WEIGHT;
|
|
53
|
+
if (utilizationPercentage > OVER_UTILIZATION_THRESHOLD) {
|
|
54
|
+
score -= (utilizationPercentage - OVER_UTILIZATION_THRESHOLD) * OVER_UTILIZATION_MULTIPLIER;
|
|
55
|
+
}
|
|
56
|
+
else if (utilizationPercentage < UNDER_UTILIZATION_THRESHOLD) {
|
|
57
|
+
score -= (UNDER_UTILIZATION_THRESHOLD - utilizationPercentage) * UNDER_UTILIZATION_MULTIPLIER;
|
|
58
|
+
}
|
|
59
|
+
return Math.round(Math.max(0, Math.min(100, score)));
|
|
60
|
+
}
|
|
61
|
+
function computeAvailableMinutesPerDay(availability, weekStart) {
|
|
62
|
+
const result = new Map();
|
|
63
|
+
const startDate = new Date(weekStart);
|
|
64
|
+
for (let dayOffset = 0; dayOffset < 7; dayOffset++) {
|
|
65
|
+
const date = new Date(startDate);
|
|
66
|
+
date.setUTCDate(date.getUTCDate() + dayOffset);
|
|
67
|
+
const dayOfWeek = date.getUTCDay(); // 0=Sun..6=Sat
|
|
68
|
+
let minutes = 0;
|
|
69
|
+
for (const window of availability.windows) {
|
|
70
|
+
if (window.day === dayOfWeek) {
|
|
71
|
+
const [startH, startM] = window.startTime.split(":").map(Number);
|
|
72
|
+
const [endH, endM] = window.endTime.split(":").map(Number);
|
|
73
|
+
minutes += endH * 60 + endM - (startH * 60 + startM);
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
result.set(dayOffset, minutes);
|
|
77
|
+
}
|
|
78
|
+
return result;
|
|
79
|
+
}
|
|
80
|
+
function computeScheduledMinutesPerDay(timeBlocks, weekStart) {
|
|
81
|
+
const result = new Map();
|
|
82
|
+
const startMs = new Date(weekStart).getTime();
|
|
83
|
+
for (const block of timeBlocks) {
|
|
84
|
+
const blockDate = new Date(block.startTime);
|
|
85
|
+
const dayOffset = Math.floor((blockDate.getTime() - startMs) / (24 * 60 * 60 * 1000));
|
|
86
|
+
if (dayOffset >= 0 && dayOffset < 7) {
|
|
87
|
+
const minutes = diffMinutes(block.startTime, block.endTime);
|
|
88
|
+
result.set(dayOffset, (result.get(dayOffset) ?? 0) + minutes);
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
return result;
|
|
92
|
+
}
|
|
93
|
+
function findBusiestLightestDay(scheduledPerDay, availablePerDay, weekStart) {
|
|
94
|
+
// Check if any time blocks exist
|
|
95
|
+
let hasScheduled = false;
|
|
96
|
+
for (const minutes of scheduledPerDay.values()) {
|
|
97
|
+
if (minutes > 0) {
|
|
98
|
+
hasScheduled = true;
|
|
99
|
+
break;
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
if (!hasScheduled) {
|
|
103
|
+
return { busiestDay: null, lightestDay: null };
|
|
104
|
+
}
|
|
105
|
+
const startDate = new Date(weekStart);
|
|
106
|
+
let busiestOffset = -1;
|
|
107
|
+
let busiestMinutes = -1;
|
|
108
|
+
let lightestOffset = -1;
|
|
109
|
+
let lightestMinutes = Infinity;
|
|
110
|
+
for (let dayOffset = 0; dayOffset < 7; dayOffset++) {
|
|
111
|
+
const scheduled = scheduledPerDay.get(dayOffset) ?? 0;
|
|
112
|
+
const available = availablePerDay.get(dayOffset) ?? 0;
|
|
113
|
+
// Busiest: any day with scheduled time
|
|
114
|
+
if (scheduled > busiestMinutes) {
|
|
115
|
+
busiestMinutes = scheduled;
|
|
116
|
+
busiestOffset = dayOffset;
|
|
117
|
+
}
|
|
118
|
+
// Lightest: only days with availability > 0
|
|
119
|
+
if (available > 0 && scheduled < lightestMinutes) {
|
|
120
|
+
lightestMinutes = scheduled;
|
|
121
|
+
lightestOffset = dayOffset;
|
|
122
|
+
}
|
|
123
|
+
}
|
|
124
|
+
const busiestDay = busiestOffset >= 0 ? getDayName(startDate, busiestOffset) : null;
|
|
125
|
+
const lightestDay = lightestOffset >= 0 ? getDayName(startDate, lightestOffset) : null;
|
|
126
|
+
return { busiestDay, lightestDay };
|
|
127
|
+
}
|
|
128
|
+
function getDayName(weekStart, dayOffset) {
|
|
129
|
+
const date = new Date(weekStart);
|
|
130
|
+
date.setUTCDate(date.getUTCDate() + dayOffset);
|
|
131
|
+
return DAY_NAMES[date.getUTCDay()];
|
|
132
|
+
}
|
|
133
|
+
function sumValues(map) {
|
|
134
|
+
let sum = 0;
|
|
135
|
+
for (const v of map.values()) {
|
|
136
|
+
sum += v;
|
|
137
|
+
}
|
|
138
|
+
return sum;
|
|
139
|
+
}
|
|
140
|
+
function round2(value) {
|
|
141
|
+
return Math.round(value * 100) / 100;
|
|
142
|
+
}
|
|
143
|
+
function round1(value) {
|
|
144
|
+
return Math.round(value * 10) / 10;
|
|
145
|
+
}
|
|
146
|
+
//# sourceMappingURL=health.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"health.js","sourceRoot":"","sources":["../../src/analytics/health.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
export type Period = "day" | "week" | "month";
|
|
2
|
+
interface DateRange {
|
|
3
|
+
start: string;
|
|
4
|
+
end: string;
|
|
5
|
+
}
|
|
6
|
+
export declare function validatePeriod(period: string): asserts period is Period;
|
|
7
|
+
export declare function resolvePeriod(period: Period, referenceDate?: Date): DateRange;
|
|
8
|
+
export {};
|
|
9
|
+
//# sourceMappingURL=period.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"period.d.ts","sourceRoot":"","sources":["../../src/analytics/period.ts"],"names":[],"mappings":"AAEA,MAAM,MAAM,MAAM,GAAG,KAAK,GAAG,MAAM,GAAG,OAAO,CAAC;AAE9C,UAAU,SAAS;IACjB,KAAK,EAAE,MAAM,CAAC;IACd,GAAG,EAAE,MAAM,CAAC;CACb;AAID,wBAAgB,cAAc,CAAC,MAAM,EAAE,MAAM,GAAG,OAAO,CAAC,MAAM,IAAI,MAAM,CAIvE;AAED,wBAAgB,aAAa,CAAC,MAAM,EAAE,MAAM,EAAE,aAAa,CAAC,EAAE,IAAI,GAAG,SAAS,CAW7E"}
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
import { ValidationError } from "../models/errors.js";
|
|
2
|
+
const VALID_PERIODS = new Set(["day", "week", "month"]);
|
|
3
|
+
export function validatePeriod(period) {
|
|
4
|
+
if (!VALID_PERIODS.has(period)) {
|
|
5
|
+
throw new ValidationError("invalid period: must be day, week, or month");
|
|
6
|
+
}
|
|
7
|
+
}
|
|
8
|
+
export function resolvePeriod(period, referenceDate) {
|
|
9
|
+
const ref = referenceDate ?? new Date();
|
|
10
|
+
switch (period) {
|
|
11
|
+
case "day":
|
|
12
|
+
return resolveDay(ref);
|
|
13
|
+
case "week":
|
|
14
|
+
return resolveWeek(ref);
|
|
15
|
+
case "month":
|
|
16
|
+
return resolveMonth(ref);
|
|
17
|
+
}
|
|
18
|
+
}
|
|
19
|
+
function resolveDay(ref) {
|
|
20
|
+
const start = new Date(Date.UTC(ref.getUTCFullYear(), ref.getUTCMonth(), ref.getUTCDate()));
|
|
21
|
+
const end = new Date(start);
|
|
22
|
+
end.setUTCDate(end.getUTCDate() + 1);
|
|
23
|
+
return { start: start.toISOString(), end: end.toISOString() };
|
|
24
|
+
}
|
|
25
|
+
function resolveWeek(ref) {
|
|
26
|
+
const start = new Date(Date.UTC(ref.getUTCFullYear(), ref.getUTCMonth(), ref.getUTCDate()));
|
|
27
|
+
// getUTCDay(): 0=Sunday, 1=Monday, ..., 6=Saturday
|
|
28
|
+
// We want Monday as start of week
|
|
29
|
+
const dayOfWeek = start.getUTCDay();
|
|
30
|
+
const daysFromMonday = dayOfWeek === 0 ? 6 : dayOfWeek - 1;
|
|
31
|
+
start.setUTCDate(start.getUTCDate() - daysFromMonday);
|
|
32
|
+
const end = new Date(start);
|
|
33
|
+
end.setUTCDate(end.getUTCDate() + 7);
|
|
34
|
+
return { start: start.toISOString(), end: end.toISOString() };
|
|
35
|
+
}
|
|
36
|
+
function resolveMonth(ref) {
|
|
37
|
+
const start = new Date(Date.UTC(ref.getUTCFullYear(), ref.getUTCMonth(), 1));
|
|
38
|
+
const end = new Date(Date.UTC(ref.getUTCFullYear(), ref.getUTCMonth() + 1, 1));
|
|
39
|
+
return { start: start.toISOString(), end: end.toISOString() };
|
|
40
|
+
}
|
|
41
|
+
//# sourceMappingURL=period.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"period.js","sourceRoot":"","sources":["../../src/analytics/period.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,eAAe,EAAE,MAAM,qBAAqB,CAAC;AAStD,MAAM,aAAa,GAAwB,IAAI,GAAG,CAAC,CAAC,KAAK,EAAE,MAAM,EAAE,OAAO,CAAC,CAAC,CAAC;AAE7E,MAAM,UAAU,cAAc,CAAC,MAAc;IAC3C,IAAI,CAAC,aAAa,CAAC,GAAG,CAAC,MAAM,CAAC,EAAE,CAAC;QAC/B,MAAM,IAAI,eAAe,CAAC,6CAA6C,CAAC,CAAC;IAC3E,CAAC;AACH,CAAC;AAED,MAAM,UAAU,aAAa,CAAC,MAAc,EAAE,aAAoB;IAChE,MAAM,GAAG,GAAG,aAAa,IAAI,IAAI,IAAI,EAAE,CAAC;IAExC,QAAQ,MAAM,EAAE,CAAC;QACf,KAAK,KAAK;YACR,OAAO,UAAU,CAAC,GAAG,CAAC,CAAC;QACzB,KAAK,MAAM;YACT,OAAO,WAAW,CAAC,GAAG,CAAC,CAAC;QAC1B,KAAK,OAAO;YACV,OAAO,YAAY,CAAC,GAAG,CAAC,CAAC;IAC7B,CAAC;AACH,CAAC;AAED,SAAS,UAAU,CAAC,GAAS;IAC3B,MAAM,KAAK,GAAG,IAAI,IAAI,CAAC,IAAI,CAAC,GAAG,CAAC,GAAG,CAAC,cAAc,EAAE,EAAE,GAAG,CAAC,WAAW,EAAE,EAAE,GAAG,CAAC,UAAU,EAAE,CAAC,CAAC,CAAC;IAC5F,MAAM,GAAG,GAAG,IAAI,IAAI,CAAC,KAAK,CAAC,CAAC;IAC5B,GAAG,CAAC,UAAU,CAAC,GAAG,CAAC,UAAU,EAAE,GAAG,CAAC,CAAC,CAAC;IACrC,OAAO,EAAE,KAAK,EAAE,KAAK,CAAC,WAAW,EAAE,EAAE,GAAG,EAAE,GAAG,CAAC,WAAW,EAAE,EAAE,CAAC;AAChE,CAAC;AAED,SAAS,WAAW,CAAC,GAAS;IAC5B,MAAM,KAAK,GAAG,IAAI,IAAI,CAAC,IAAI,CAAC,GAAG,CAAC,GAAG,CAAC,cAAc,EAAE,EAAE,GAAG,CAAC,WAAW,EAAE,EAAE,GAAG,CAAC,UAAU,EAAE,CAAC,CAAC,CAAC;IAC5F,mDAAmD;IACnD,kCAAkC;IAClC,MAAM,SAAS,GAAG,KAAK,CAAC,SAAS,EAAE,CAAC;IACpC,MAAM,cAAc,GAAG,SAAS,KAAK,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,SAAS,GAAG,CAAC,CAAC;IAC3D,KAAK,CAAC,UAAU,CAAC,KAAK,CAAC,UAAU,EAAE,GAAG,cAAc,CAAC,CAAC;IAEtD,MAAM,GAAG,GAAG,IAAI,IAAI,CAAC,KAAK,CAAC,CAAC;IAC5B,GAAG,CAAC,UAAU,CAAC,GAAG,CAAC,UAAU,EAAE,GAAG,CAAC,CAAC,CAAC;IACrC,OAAO,EAAE,KAAK,EAAE,KAAK,CAAC,WAAW,EAAE,EAAE,GAAG,EAAE,GAAG,CAAC,WAAW,EAAE,EAAE,CAAC;AAChE,CAAC;AAED,SAAS,YAAY,CAAC,GAAS;IAC7B,MAAM,KAAK,GAAG,IAAI,IAAI,CAAC,IAAI,CAAC,GAAG,CAAC,GAAG,CAAC,cAAc,EAAE,EAAE,GAAG,CAAC,WAAW,EAAE,EAAE,CAAC,CAAC,CAAC,CAAC;IAC7E,MAAM,GAAG,GAAG,IAAI,IAAI,CAAC,IAAI,CAAC,GAAG,CAAC,GAAG,CAAC,cAAc,EAAE,EAAE,GAAG,CAAC,WAAW,EAAE,GAAG,CAAC,EAAE,CAAC,CAAC,CAAC,CAAC;IAC/E,OAAO,EAAE,KAAK,EAAE,KAAK,CAAC,WAAW,EAAE,EAAE,GAAG,EAAE,GAAG,CAAC,WAAW,EAAE,EAAE,CAAC;AAChE,CAAC"}
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
import type { AnalyticsRepository } from "../storage/analytics-repository.js";
|
|
2
|
+
import type { ProductivityStats } from "../models/analytics.js";
|
|
3
|
+
import type { Period } from "./period.js";
|
|
4
|
+
export declare class ProductivityCalculator {
|
|
5
|
+
private readonly analyticsRepo;
|
|
6
|
+
constructor(analyticsRepo: AnalyticsRepository);
|
|
7
|
+
compute(period: Period, referenceDate?: Date): ProductivityStats;
|
|
8
|
+
}
|
|
9
|
+
//# sourceMappingURL=productivity.d.ts.map
|