@creezio/observability 0.22.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 +328 -0
- package/dist/.creezio-src-hash.json +4 -0
- package/dist/api-endpoints/http-routes.d.ts +11 -0
- package/dist/api-endpoints/http-routes.d.ts.map +1 -0
- package/dist/api-endpoints/http-routes.js +11 -0
- package/dist/api-endpoints/http-routes.js.map +1 -0
- package/dist/api-endpoints/index.d.ts +4 -0
- package/dist/api-endpoints/index.d.ts.map +1 -0
- package/dist/api-endpoints/index.js +3 -0
- package/dist/api-endpoints/index.js.map +1 -0
- package/dist/api-endpoints/registry.d.ts +66 -0
- package/dist/api-endpoints/registry.d.ts.map +1 -0
- package/dist/api-endpoints/registry.js +102 -0
- package/dist/api-endpoints/registry.js.map +1 -0
- package/dist/api-mount.d.ts +7 -0
- package/dist/api-mount.d.ts.map +1 -0
- package/dist/api-mount.js +114 -0
- package/dist/api-mount.js.map +1 -0
- package/dist/helpers.d.ts +23 -0
- package/dist/helpers.d.ts.map +1 -0
- package/dist/helpers.js +36 -0
- package/dist/helpers.js.map +1 -0
- package/dist/index.d.ts +35 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +24 -0
- package/dist/index.js.map +1 -0
- package/dist/memory-store.d.ts +3 -0
- package/dist/memory-store.d.ts.map +1 -0
- package/dist/memory-store.js +113 -0
- package/dist/memory-store.js.map +1 -0
- package/dist/ops/emit.d.ts +8 -0
- package/dist/ops/emit.d.ts.map +1 -0
- package/dist/ops/emit.js +15 -0
- package/dist/ops/emit.js.map +1 -0
- package/dist/ops/fleet-activity.d.ts +50 -0
- package/dist/ops/fleet-activity.d.ts.map +1 -0
- package/dist/ops/fleet-activity.js +123 -0
- package/dist/ops/fleet-activity.js.map +1 -0
- package/dist/ops/fleet-agent.d.ts +102 -0
- package/dist/ops/fleet-agent.d.ts.map +1 -0
- package/dist/ops/fleet-agent.js +352 -0
- package/dist/ops/fleet-agent.js.map +1 -0
- package/dist/ops/fleet-samples.d.ts +33 -0
- package/dist/ops/fleet-samples.d.ts.map +1 -0
- package/dist/ops/fleet-samples.js +223 -0
- package/dist/ops/fleet-samples.js.map +1 -0
- package/dist/ops/journal.d.ts +56 -0
- package/dist/ops/journal.d.ts.map +1 -0
- package/dist/ops/journal.js +307 -0
- package/dist/ops/journal.js.map +1 -0
- package/dist/ops/rules.d.ts +16 -0
- package/dist/ops/rules.d.ts.map +1 -0
- package/dist/ops/rules.js +98 -0
- package/dist/ops/rules.js.map +1 -0
- package/dist/ops/types.d.ts +69 -0
- package/dist/ops/types.d.ts.map +1 -0
- package/dist/ops/types.js +119 -0
- package/dist/ops/types.js.map +1 -0
- package/dist/request-logs/config.d.ts +19 -0
- package/dist/request-logs/config.d.ts.map +1 -0
- package/dist/request-logs/config.js +36 -0
- package/dist/request-logs/config.js.map +1 -0
- package/dist/request-logs/http-routes.d.ts +8 -0
- package/dist/request-logs/http-routes.d.ts.map +1 -0
- package/dist/request-logs/http-routes.js +29 -0
- package/dist/request-logs/http-routes.js.map +1 -0
- package/dist/request-logs/index.d.ts +6 -0
- package/dist/request-logs/index.d.ts.map +1 -0
- package/dist/request-logs/index.js +5 -0
- package/dist/request-logs/index.js.map +1 -0
- package/dist/request-logs/middleware.d.ts +11 -0
- package/dist/request-logs/middleware.d.ts.map +1 -0
- package/dist/request-logs/middleware.js +198 -0
- package/dist/request-logs/middleware.js.map +1 -0
- package/dist/request-logs/request-logs.d.ts +60 -0
- package/dist/request-logs/request-logs.d.ts.map +1 -0
- package/dist/request-logs/request-logs.js +260 -0
- package/dist/request-logs/request-logs.js.map +1 -0
- package/dist/schema.d.ts +3 -0
- package/dist/schema.d.ts.map +1 -0
- package/dist/schema.js +21 -0
- package/dist/schema.js.map +1 -0
- package/dist/sqlite-driver.d.ts +16 -0
- package/dist/sqlite-driver.d.ts.map +1 -0
- package/dist/sqlite-driver.js +15 -0
- package/dist/sqlite-driver.js.map +1 -0
- package/dist/sqlite-store.d.ts +12 -0
- package/dist/sqlite-store.d.ts.map +1 -0
- package/dist/sqlite-store.js +176 -0
- package/dist/sqlite-store.js.map +1 -0
- package/dist/types.d.ts +67 -0
- package/dist/types.d.ts.map +1 -0
- package/dist/types.js +9 -0
- package/dist/types.js.map +1 -0
- package/dist/usage/adapters.d.ts +31 -0
- package/dist/usage/adapters.d.ts.map +1 -0
- package/dist/usage/adapters.js +37 -0
- package/dist/usage/adapters.js.map +1 -0
- package/dist/usage/http-routes.d.ts +23 -0
- package/dist/usage/http-routes.d.ts.map +1 -0
- package/dist/usage/http-routes.js +150 -0
- package/dist/usage/http-routes.js.map +1 -0
- package/dist/usage/index.d.ts +11 -0
- package/dist/usage/index.d.ts.map +1 -0
- package/dist/usage/index.js +7 -0
- package/dist/usage/index.js.map +1 -0
- package/dist/usage/ui-brand.d.ts +23 -0
- package/dist/usage/ui-brand.d.ts.map +1 -0
- package/dist/usage/ui-brand.js +18 -0
- package/dist/usage/ui-brand.js.map +1 -0
- package/dist/usage/usage-analytics-productivity.d.ts +73 -0
- package/dist/usage/usage-analytics-productivity.d.ts.map +1 -0
- package/dist/usage/usage-analytics-productivity.js +317 -0
- package/dist/usage/usage-analytics-productivity.js.map +1 -0
- package/dist/usage/usage-analytics-shared.d.ts +5 -0
- package/dist/usage/usage-analytics-shared.d.ts.map +1 -0
- package/dist/usage/usage-analytics-shared.js +20 -0
- package/dist/usage/usage-analytics-shared.js.map +1 -0
- package/dist/usage/usage-analytics.d.ts +127 -0
- package/dist/usage/usage-analytics.d.ts.map +1 -0
- package/dist/usage/usage-analytics.js +338 -0
- package/dist/usage/usage-analytics.js.map +1 -0
- package/dist-cjs/api-endpoints/http-routes.js +14 -0
- package/dist-cjs/api-endpoints/http-routes.js.map +1 -0
- package/dist-cjs/api-endpoints/index.js +10 -0
- package/dist-cjs/api-endpoints/index.js.map +1 -0
- package/dist-cjs/api-endpoints/registry.js +107 -0
- package/dist-cjs/api-endpoints/registry.js.map +1 -0
- package/dist-cjs/api-mount.js +117 -0
- package/dist-cjs/api-mount.js.map +1 -0
- package/dist-cjs/helpers.js +41 -0
- package/dist-cjs/helpers.js.map +1 -0
- package/dist-cjs/index.js +118 -0
- package/dist-cjs/index.js.map +1 -0
- package/dist-cjs/memory-store.js +119 -0
- package/dist-cjs/memory-store.js.map +1 -0
- package/dist-cjs/ops/emit.js +18 -0
- package/dist-cjs/ops/emit.js.map +1 -0
- package/dist-cjs/ops/fleet-activity.js +131 -0
- package/dist-cjs/ops/fleet-activity.js.map +1 -0
- package/dist-cjs/ops/fleet-agent.js +358 -0
- package/dist-cjs/ops/fleet-agent.js.map +1 -0
- package/dist-cjs/ops/fleet-samples.js +229 -0
- package/dist-cjs/ops/fleet-samples.js.map +1 -0
- package/dist-cjs/ops/journal.js +326 -0
- package/dist-cjs/ops/journal.js.map +1 -0
- package/dist-cjs/ops/rules.js +102 -0
- package/dist-cjs/ops/rules.js.map +1 -0
- package/dist-cjs/ops/types.js +126 -0
- package/dist-cjs/ops/types.js.map +1 -0
- package/dist-cjs/package.json +3 -0
- package/dist-cjs/request-logs/config.js +42 -0
- package/dist-cjs/request-logs/config.js.map +1 -0
- package/dist-cjs/request-logs/http-routes.js +32 -0
- package/dist-cjs/request-logs/http-routes.js.map +1 -0
- package/dist-cjs/request-logs/index.js +27 -0
- package/dist-cjs/request-logs/index.js.map +1 -0
- package/dist-cjs/request-logs/middleware.js +203 -0
- package/dist-cjs/request-logs/middleware.js.map +1 -0
- package/dist-cjs/request-logs/request-logs.js +277 -0
- package/dist-cjs/request-logs/request-logs.js.map +1 -0
- package/dist-cjs/schema.js +24 -0
- package/dist-cjs/schema.js.map +1 -0
- package/dist-cjs/sqlite-driver.js +18 -0
- package/dist-cjs/sqlite-driver.js.map +1 -0
- package/dist-cjs/sqlite-store.js +182 -0
- package/dist-cjs/sqlite-store.js.map +1 -0
- package/dist-cjs/types.js +12 -0
- package/dist-cjs/types.js.map +1 -0
- package/dist-cjs/usage/adapters.js +47 -0
- package/dist-cjs/usage/adapters.js.map +1 -0
- package/dist-cjs/usage/http-routes.js +154 -0
- package/dist-cjs/usage/http-routes.js.map +1 -0
- package/dist-cjs/usage/index.js +34 -0
- package/dist-cjs/usage/index.js.map +1 -0
- package/dist-cjs/usage/ui-brand.js +23 -0
- package/dist-cjs/usage/ui-brand.js.map +1 -0
- package/dist-cjs/usage/usage-analytics-productivity.js +321 -0
- package/dist-cjs/usage/usage-analytics-productivity.js.map +1 -0
- package/dist-cjs/usage/usage-analytics-shared.js +23 -0
- package/dist-cjs/usage/usage-analytics-shared.js.map +1 -0
- package/dist-cjs/usage/usage-analytics.js +353 -0
- package/dist-cjs/usage/usage-analytics.js.map +1 -0
- package/fleet-collector/AGENTS.md +7 -0
- package/fleet-collector/README.md +83 -0
- package/fleet-collector/configure-fleet-npm.sh +108 -0
- package/fleet-collector/env.mjs +174 -0
- package/fleet-collector/ops-api.mjs +455 -0
- package/fleet-collector/public/admin.html +696 -0
- package/fleet-collector/public/index.html +941 -0
- package/fleet-collector/server.mjs +564 -0
- package/fleet-collector/test-fleet-collector.mjs +413 -0
- package/fleet-collector/test-server-admin.mjs +475 -0
- package/package.json +109 -0
- package/scripts/test-api-endpoints-registry.mjs +60 -0
- package/ui/analytics-client.tsx +834 -0
- package/ui/analytics-productivity-panel.tsx +513 -0
- package/ui/api-endpoints-client.tsx +228 -0
- package/ui/index.ts +39 -0
- package/ui/primitives/badge.tsx +37 -0
- package/ui/primitives/button.tsx +44 -0
- package/ui/primitives/card.tsx +44 -0
- package/ui/primitives/cn.ts +6 -0
- package/ui/primitives/input.tsx +19 -0
- package/ui/primitives/tabs.tsx +51 -0
- package/ui/request-logs-client.tsx +327 -0
- package/ui/session-usage-analytics-provider.tsx +28 -0
- package/ui/usage-analytics-client.ts +479 -0
- package/ui/usage-analytics-provider.tsx +84 -0
|
@@ -0,0 +1,513 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
|
|
3
|
+
import { useMemo } from "react";
|
|
4
|
+
import {
|
|
5
|
+
Bar,
|
|
6
|
+
BarChart,
|
|
7
|
+
CartesianGrid,
|
|
8
|
+
ResponsiveContainer,
|
|
9
|
+
Tooltip,
|
|
10
|
+
XAxis,
|
|
11
|
+
YAxis,
|
|
12
|
+
} from "recharts";
|
|
13
|
+
import { Coffee, Flame, Gauge, Timer } from "lucide-react";
|
|
14
|
+
import { Badge } from "./primitives/badge";
|
|
15
|
+
import { cn } from "./primitives/cn";
|
|
16
|
+
import { formatDuration } from "../dist/usage/usage-analytics-shared.js";
|
|
17
|
+
|
|
18
|
+
export type ProductivityPayload = {
|
|
19
|
+
summary: {
|
|
20
|
+
userId: string;
|
|
21
|
+
username: string;
|
|
22
|
+
kind: string;
|
|
23
|
+
activeMs: number;
|
|
24
|
+
idleMs: number;
|
|
25
|
+
breakMs: number;
|
|
26
|
+
avgActivePerDayMs: number;
|
|
27
|
+
avgBreakMs: number;
|
|
28
|
+
breaksCount: number;
|
|
29
|
+
focusBlocksCount: number;
|
|
30
|
+
avgFocusBlockMs: number;
|
|
31
|
+
peakHour: number | null;
|
|
32
|
+
peakWeekday: number | null;
|
|
33
|
+
eventsPerActiveHour: number;
|
|
34
|
+
focusRatio: number;
|
|
35
|
+
productivityScore: number;
|
|
36
|
+
daysActive: number;
|
|
37
|
+
events: number;
|
|
38
|
+
clicks: number;
|
|
39
|
+
pageViews: number;
|
|
40
|
+
} | null;
|
|
41
|
+
heatmap: Array<{ weekday: number; hour: number; events: number; activeMs: number }>;
|
|
42
|
+
daily: Array<{
|
|
43
|
+
date: string;
|
|
44
|
+
activeMs: number;
|
|
45
|
+
idleMs: number;
|
|
46
|
+
breakMs: number;
|
|
47
|
+
breaksCount: number;
|
|
48
|
+
events: number;
|
|
49
|
+
clicks: number;
|
|
50
|
+
pageViews: number;
|
|
51
|
+
productivityScore: number;
|
|
52
|
+
firstSeen: string | null;
|
|
53
|
+
lastSeen: string | null;
|
|
54
|
+
}>;
|
|
55
|
+
breaks: Array<{
|
|
56
|
+
startedAt: string;
|
|
57
|
+
endedAt: string;
|
|
58
|
+
durationMs: number;
|
|
59
|
+
day: string;
|
|
60
|
+
}>;
|
|
61
|
+
focusBlocks: Array<{
|
|
62
|
+
startedAt: string;
|
|
63
|
+
endedAt: string;
|
|
64
|
+
durationMs: number;
|
|
65
|
+
events: number;
|
|
66
|
+
}>;
|
|
67
|
+
leaderboard: Array<{
|
|
68
|
+
userId: string;
|
|
69
|
+
username: string;
|
|
70
|
+
kind: string;
|
|
71
|
+
activeMs: number;
|
|
72
|
+
breakMs: number;
|
|
73
|
+
breaksCount: number;
|
|
74
|
+
productivityScore: number;
|
|
75
|
+
avgActivePerDayMs: number;
|
|
76
|
+
focusRatio: number;
|
|
77
|
+
events: number;
|
|
78
|
+
}>;
|
|
79
|
+
};
|
|
80
|
+
|
|
81
|
+
const WEEKDAYS = ["Dim", "Lun", "Mar", "Mer", "Jeu", "Ven", "Sam"];
|
|
82
|
+
const HOURS = Array.from({ length: 24 }, (_, i) => i);
|
|
83
|
+
|
|
84
|
+
function scoreTone(score: number): string {
|
|
85
|
+
if (score >= 75) return "text-emerald-700 bg-emerald-50 border-emerald-200";
|
|
86
|
+
if (score >= 45) return "text-amber-700 bg-amber-50 border-amber-200";
|
|
87
|
+
return "text-rose-700 bg-rose-50 border-rose-200";
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
function heatColor(intensity: number, max: number): string {
|
|
91
|
+
if (max <= 0 || intensity <= 0) return "bg-slate-50";
|
|
92
|
+
const t = intensity / max;
|
|
93
|
+
if (t < 0.15) return "bg-sky-100";
|
|
94
|
+
if (t < 0.35) return "bg-sky-200";
|
|
95
|
+
if (t < 0.55) return "bg-sky-300";
|
|
96
|
+
if (t < 0.75) return "bg-sky-400";
|
|
97
|
+
if (t < 0.9) return "bg-sky-500";
|
|
98
|
+
return "bg-sky-600";
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
function formatHour(h: number): string {
|
|
102
|
+
return `${String(h).padStart(2, "0")}h`;
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
function formatTs(iso: string): string {
|
|
106
|
+
try {
|
|
107
|
+
return new Date(iso).toLocaleString("fr-FR", {
|
|
108
|
+
day: "2-digit",
|
|
109
|
+
month: "short",
|
|
110
|
+
hour: "2-digit",
|
|
111
|
+
minute: "2-digit",
|
|
112
|
+
});
|
|
113
|
+
} catch {
|
|
114
|
+
return iso;
|
|
115
|
+
}
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
export function AnalyticsProductivityPanel({
|
|
119
|
+
data,
|
|
120
|
+
selectedUserId,
|
|
121
|
+
onSelectUser,
|
|
122
|
+
}: {
|
|
123
|
+
data: ProductivityPayload | null;
|
|
124
|
+
selectedUserId: string;
|
|
125
|
+
onSelectUser: (userId: string) => void;
|
|
126
|
+
}) {
|
|
127
|
+
const summary = data?.summary || null;
|
|
128
|
+
|
|
129
|
+
const heatMax = useMemo(() => {
|
|
130
|
+
let m = 0;
|
|
131
|
+
for (const c of data?.heatmap || []) {
|
|
132
|
+
const v = c.activeMs > 0 ? c.activeMs : c.events * 30_000;
|
|
133
|
+
if (v > m) m = v;
|
|
134
|
+
}
|
|
135
|
+
return m;
|
|
136
|
+
}, [data?.heatmap]);
|
|
137
|
+
|
|
138
|
+
const heatLookup = useMemo(() => {
|
|
139
|
+
const map = new Map<string, { events: number; activeMs: number }>();
|
|
140
|
+
for (const c of data?.heatmap || []) {
|
|
141
|
+
map.set(`${c.weekday}:${c.hour}`, { events: c.events, activeMs: c.activeMs });
|
|
142
|
+
}
|
|
143
|
+
return map;
|
|
144
|
+
}, [data?.heatmap]);
|
|
145
|
+
|
|
146
|
+
const dailyChart = useMemo(
|
|
147
|
+
() =>
|
|
148
|
+
(data?.daily || []).map((d) => ({
|
|
149
|
+
date: d.date.slice(5),
|
|
150
|
+
activeH: Math.round((d.activeMs / 3_600_000) * 100) / 100,
|
|
151
|
+
breakH: Math.round((d.breakMs / 3_600_000) * 100) / 100,
|
|
152
|
+
idleH: Math.round((d.idleMs / 3_600_000) * 100) / 100,
|
|
153
|
+
score: d.productivityScore,
|
|
154
|
+
})),
|
|
155
|
+
[data?.daily],
|
|
156
|
+
);
|
|
157
|
+
|
|
158
|
+
if (!data) {
|
|
159
|
+
return (
|
|
160
|
+
<p className="rounded-2xl border border-dashed border-slate-200 bg-white py-16 text-center text-sm text-slate-400">
|
|
161
|
+
Chargement productivité…
|
|
162
|
+
</p>
|
|
163
|
+
);
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
return (
|
|
167
|
+
<div className="space-y-4">
|
|
168
|
+
{/* Explication méthodo */}
|
|
169
|
+
<div className="rounded-2xl border border-sky-100 bg-gradient-to-r from-sky-50/80 to-white px-4 py-3 text-xs leading-relaxed text-slate-600">
|
|
170
|
+
<strong className="text-slate-800">Comment c’est calculé — </strong>
|
|
171
|
+
présence active via heartbeats (60s) + interactions ; idle après 2 min sans
|
|
172
|
+
activité ; pauses = trous d’activité entre 5 min et 4 h ; score 0–100 =
|
|
173
|
+
couverture horaire × focus (actif vs idle) × densité d’actions. Aucune frappe
|
|
174
|
+
clavier n’est enregistrée.
|
|
175
|
+
</div>
|
|
176
|
+
|
|
177
|
+
{/* Leaderboard */}
|
|
178
|
+
<div className="rounded-2xl border border-slate-200/80 bg-white p-4 shadow-sm">
|
|
179
|
+
<div className="mb-3 flex items-center justify-between gap-2">
|
|
180
|
+
<div>
|
|
181
|
+
<h2 className="text-sm font-semibold text-slate-900">Classement collaborateurs</h2>
|
|
182
|
+
<p className="text-xs text-slate-500">
|
|
183
|
+
Cliquez un employé pour ouvrir sa heatmap et ses pauses
|
|
184
|
+
</p>
|
|
185
|
+
</div>
|
|
186
|
+
<Gauge className="h-4 w-4 text-slate-400" />
|
|
187
|
+
</div>
|
|
188
|
+
<div className="overflow-auto">
|
|
189
|
+
<table className="w-full text-left text-sm">
|
|
190
|
+
<thead className="text-xs uppercase tracking-wide text-slate-500">
|
|
191
|
+
<tr>
|
|
192
|
+
<th className="px-2 py-2 font-medium">#</th>
|
|
193
|
+
<th className="px-2 py-2 font-medium">Collaborateur</th>
|
|
194
|
+
<th className="px-2 py-2 font-medium">Score</th>
|
|
195
|
+
<th className="px-2 py-2 font-medium">Actif / jour</th>
|
|
196
|
+
<th className="px-2 py-2 font-medium">Pauses</th>
|
|
197
|
+
<th className="px-2 py-2 font-medium">Focus</th>
|
|
198
|
+
</tr>
|
|
199
|
+
</thead>
|
|
200
|
+
<tbody>
|
|
201
|
+
{(data.leaderboard || []).length === 0 ? (
|
|
202
|
+
<tr>
|
|
203
|
+
<td colSpan={6} className="px-2 py-8 text-center text-slate-400">
|
|
204
|
+
Pas encore assez de données de présence — naviguez dans l’app quelques
|
|
205
|
+
minutes (heartbeats).
|
|
206
|
+
</td>
|
|
207
|
+
</tr>
|
|
208
|
+
) : (
|
|
209
|
+
data.leaderboard.map((u, i) => (
|
|
210
|
+
<tr
|
|
211
|
+
key={u.userId}
|
|
212
|
+
className={cn(
|
|
213
|
+
"cursor-pointer border-t border-slate-100 transition-colors hover:bg-sky-50/60",
|
|
214
|
+
selectedUserId === u.userId && "bg-sky-50",
|
|
215
|
+
)}
|
|
216
|
+
onClick={() =>
|
|
217
|
+
onSelectUser(selectedUserId === u.userId ? "" : u.userId)
|
|
218
|
+
}
|
|
219
|
+
>
|
|
220
|
+
<td className="px-2 py-2 text-xs text-slate-400">{i + 1}</td>
|
|
221
|
+
<td className="px-2 py-2">
|
|
222
|
+
<div className="flex items-center gap-2">
|
|
223
|
+
<span className="font-medium text-slate-800">{u.username}</span>
|
|
224
|
+
<Badge variant={u.kind === "ai" ? "warning" : "success"}>
|
|
225
|
+
{u.kind === "ai" ? "IA" : "Humain"}
|
|
226
|
+
</Badge>
|
|
227
|
+
</div>
|
|
228
|
+
</td>
|
|
229
|
+
<td className="px-2 py-2">
|
|
230
|
+
<span
|
|
231
|
+
className={cn(
|
|
232
|
+
"inline-flex rounded-md border px-2 py-0.5 text-xs font-semibold tabular-nums",
|
|
233
|
+
scoreTone(u.productivityScore),
|
|
234
|
+
)}
|
|
235
|
+
>
|
|
236
|
+
{u.productivityScore}
|
|
237
|
+
</span>
|
|
238
|
+
</td>
|
|
239
|
+
<td className="px-2 py-2 tabular-nums text-slate-700">
|
|
240
|
+
{formatDuration(u.avgActivePerDayMs)}
|
|
241
|
+
</td>
|
|
242
|
+
<td className="px-2 py-2 text-slate-700">
|
|
243
|
+
{u.breaksCount}{" "}
|
|
244
|
+
<span className="text-xs text-slate-400">
|
|
245
|
+
({formatDuration(u.breakMs)})
|
|
246
|
+
</span>
|
|
247
|
+
</td>
|
|
248
|
+
<td className="px-2 py-2 tabular-nums text-slate-700">
|
|
249
|
+
{Math.round((u.focusRatio || 0) * 100)}%
|
|
250
|
+
</td>
|
|
251
|
+
</tr>
|
|
252
|
+
))
|
|
253
|
+
)}
|
|
254
|
+
</tbody>
|
|
255
|
+
</table>
|
|
256
|
+
</div>
|
|
257
|
+
</div>
|
|
258
|
+
|
|
259
|
+
{summary ? (
|
|
260
|
+
<>
|
|
261
|
+
<div className="grid gap-3 sm:grid-cols-2 xl:grid-cols-4">
|
|
262
|
+
<MiniKpi
|
|
263
|
+
icon={Gauge}
|
|
264
|
+
label={`Score · ${summary.username}`}
|
|
265
|
+
value={String(summary.productivityScore)}
|
|
266
|
+
hint={`${summary.daysActive} jour${summary.daysActive > 1 ? "s" : ""} actif${summary.daysActive > 1 ? "s" : ""}`}
|
|
267
|
+
accent="bg-emerald-500"
|
|
268
|
+
/>
|
|
269
|
+
<MiniKpi
|
|
270
|
+
icon={Timer}
|
|
271
|
+
label="Temps actif / jour"
|
|
272
|
+
value={formatDuration(summary.avgActivePerDayMs)}
|
|
273
|
+
hint={`Total ${formatDuration(summary.activeMs)}`}
|
|
274
|
+
accent="bg-sky-500"
|
|
275
|
+
/>
|
|
276
|
+
<MiniKpi
|
|
277
|
+
icon={Coffee}
|
|
278
|
+
label="Pauses"
|
|
279
|
+
value={String(summary.breaksCount)}
|
|
280
|
+
hint={`Moy. ${formatDuration(summary.avgBreakMs)} · total ${formatDuration(summary.breakMs)}`}
|
|
281
|
+
accent="bg-amber-500"
|
|
282
|
+
/>
|
|
283
|
+
<MiniKpi
|
|
284
|
+
icon={Flame}
|
|
285
|
+
label="Pic d'activité"
|
|
286
|
+
value={
|
|
287
|
+
summary.peakHour != null && summary.peakWeekday != null
|
|
288
|
+
? `${WEEKDAYS[summary.peakWeekday]} ${formatHour(summary.peakHour)}`
|
|
289
|
+
: "—"
|
|
290
|
+
}
|
|
291
|
+
hint={`${summary.eventsPerActiveHour} evt/h actif · focus ${Math.round(summary.focusRatio * 100)}%`}
|
|
292
|
+
accent="bg-violet-500"
|
|
293
|
+
/>
|
|
294
|
+
</div>
|
|
295
|
+
|
|
296
|
+
{/* Heatmap */}
|
|
297
|
+
<div className="rounded-2xl border border-slate-200/80 bg-white p-4 shadow-sm">
|
|
298
|
+
<div className="mb-3">
|
|
299
|
+
<h2 className="text-sm font-semibold text-slate-900">
|
|
300
|
+
Heatmap d’activité — {summary.username}
|
|
301
|
+
</h2>
|
|
302
|
+
<p className="text-xs text-slate-500">
|
|
303
|
+
Intensité par jour de la semaine × heure (UTC). Plus c’est foncé, plus
|
|
304
|
+
l’employé est actif.
|
|
305
|
+
</p>
|
|
306
|
+
</div>
|
|
307
|
+
<div className="overflow-x-auto">
|
|
308
|
+
<div className="inline-block min-w-full">
|
|
309
|
+
<div
|
|
310
|
+
className="grid gap-1"
|
|
311
|
+
style={{ gridTemplateColumns: `48px repeat(24, minmax(18px, 1fr))` }}
|
|
312
|
+
>
|
|
313
|
+
<div />
|
|
314
|
+
{HOURS.map((h) => (
|
|
315
|
+
<div
|
|
316
|
+
key={h}
|
|
317
|
+
className="text-center text-[9px] tabular-nums text-slate-400"
|
|
318
|
+
>
|
|
319
|
+
{h % 3 === 0 ? h : ""}
|
|
320
|
+
</div>
|
|
321
|
+
))}
|
|
322
|
+
{WEEKDAYS.map((label, weekday) => (
|
|
323
|
+
<div key={label} className="contents">
|
|
324
|
+
<div className="flex items-center text-[11px] font-medium text-slate-500">
|
|
325
|
+
{label}
|
|
326
|
+
</div>
|
|
327
|
+
{HOURS.map((hour) => {
|
|
328
|
+
const cell = heatLookup.get(`${weekday}:${hour}`);
|
|
329
|
+
const intensity = cell
|
|
330
|
+
? cell.activeMs > 0
|
|
331
|
+
? cell.activeMs
|
|
332
|
+
: cell.events * 30_000
|
|
333
|
+
: 0;
|
|
334
|
+
const title = cell
|
|
335
|
+
? `${label} ${formatHour(hour)} — ${cell.events} evt · ${formatDuration(cell.activeMs)}`
|
|
336
|
+
: `${label} ${formatHour(hour)} — aucune activité`;
|
|
337
|
+
return (
|
|
338
|
+
<div
|
|
339
|
+
key={`${weekday}-${hour}`}
|
|
340
|
+
title={title}
|
|
341
|
+
className={cn(
|
|
342
|
+
"aspect-square rounded-sm transition-colors",
|
|
343
|
+
heatColor(intensity, heatMax),
|
|
344
|
+
)}
|
|
345
|
+
/>
|
|
346
|
+
);
|
|
347
|
+
})}
|
|
348
|
+
</div>
|
|
349
|
+
))}
|
|
350
|
+
</div>
|
|
351
|
+
<div className="mt-3 flex items-center gap-2 text-[10px] text-slate-400">
|
|
352
|
+
<span>Faible</span>
|
|
353
|
+
<div className="flex gap-0.5">
|
|
354
|
+
{["bg-slate-50", "bg-sky-100", "bg-sky-300", "bg-sky-500", "bg-sky-600"].map(
|
|
355
|
+
(c) => (
|
|
356
|
+
<div key={c} className={cn("h-3 w-4 rounded-sm", c)} />
|
|
357
|
+
),
|
|
358
|
+
)}
|
|
359
|
+
</div>
|
|
360
|
+
<span>Fort</span>
|
|
361
|
+
</div>
|
|
362
|
+
</div>
|
|
363
|
+
</div>
|
|
364
|
+
</div>
|
|
365
|
+
|
|
366
|
+
<div className="grid gap-4 xl:grid-cols-2">
|
|
367
|
+
<div className="rounded-2xl border border-slate-200/80 bg-white p-4 shadow-sm">
|
|
368
|
+
<h2 className="text-sm font-semibold text-slate-900">Heures actives / jour</h2>
|
|
369
|
+
<p className="mb-3 text-xs text-slate-500">
|
|
370
|
+
Temps actif vs pauses détectées (heures)
|
|
371
|
+
</p>
|
|
372
|
+
<div className="h-56">
|
|
373
|
+
{dailyChart.length < 1 ? (
|
|
374
|
+
<p className="flex h-full items-center justify-center text-sm text-slate-400">
|
|
375
|
+
Pas de série quotidienne
|
|
376
|
+
</p>
|
|
377
|
+
) : (
|
|
378
|
+
<ResponsiveContainer width="100%" height="100%">
|
|
379
|
+
<BarChart data={dailyChart}>
|
|
380
|
+
<CartesianGrid strokeDasharray="3 3" stroke="#e2e8f0" />
|
|
381
|
+
<XAxis dataKey="date" tick={{ fontSize: 11 }} stroke="#94a3b8" />
|
|
382
|
+
<YAxis tick={{ fontSize: 11 }} stroke="#94a3b8" width={28} />
|
|
383
|
+
<Tooltip
|
|
384
|
+
contentStyle={{ fontSize: 12, borderRadius: 12 }}
|
|
385
|
+
formatter={(v: number, name: string) => [
|
|
386
|
+
`${v} h`,
|
|
387
|
+
name === "activeH"
|
|
388
|
+
? "Actif"
|
|
389
|
+
: name === "breakH"
|
|
390
|
+
? "Pauses"
|
|
391
|
+
: "Idle",
|
|
392
|
+
]}
|
|
393
|
+
/>
|
|
394
|
+
<Bar dataKey="activeH" name="activeH" fill="#0ea5e9" radius={[4, 4, 0, 0]} />
|
|
395
|
+
<Bar dataKey="breakH" name="breakH" fill="#f59e0b" radius={[4, 4, 0, 0]} />
|
|
396
|
+
</BarChart>
|
|
397
|
+
</ResponsiveContainer>
|
|
398
|
+
)}
|
|
399
|
+
</div>
|
|
400
|
+
</div>
|
|
401
|
+
|
|
402
|
+
<div className="rounded-2xl border border-slate-200/80 bg-white p-4 shadow-sm">
|
|
403
|
+
<h2 className="text-sm font-semibold text-slate-900">Pauses détectées</h2>
|
|
404
|
+
<p className="mb-3 text-xs text-slate-500">
|
|
405
|
+
Gaps d’activité 5 min – 4 h (au-delà = coupure de journée)
|
|
406
|
+
</p>
|
|
407
|
+
<div className="max-h-56 space-y-2 overflow-auto">
|
|
408
|
+
{(data.breaks || []).length === 0 ? (
|
|
409
|
+
<p className="py-8 text-center text-sm text-slate-400">
|
|
410
|
+
Aucune pause détectée sur la période
|
|
411
|
+
</p>
|
|
412
|
+
) : (
|
|
413
|
+
data.breaks.map((b, i) => (
|
|
414
|
+
<div
|
|
415
|
+
key={`${b.startedAt}-${i}`}
|
|
416
|
+
className="flex items-center justify-between gap-3 rounded-xl border border-slate-100 bg-slate-50/70 px-3 py-2"
|
|
417
|
+
>
|
|
418
|
+
<div className="min-w-0">
|
|
419
|
+
<p className="truncate text-sm font-medium text-slate-800">
|
|
420
|
+
{formatTs(b.startedAt)} → {formatTs(b.endedAt)}
|
|
421
|
+
</p>
|
|
422
|
+
<p className="text-[11px] text-slate-400">{b.day}</p>
|
|
423
|
+
</div>
|
|
424
|
+
<span className="shrink-0 rounded-md bg-amber-100 px-2 py-0.5 text-xs font-semibold tabular-nums text-amber-800">
|
|
425
|
+
{formatDuration(b.durationMs)}
|
|
426
|
+
</span>
|
|
427
|
+
</div>
|
|
428
|
+
))
|
|
429
|
+
)}
|
|
430
|
+
</div>
|
|
431
|
+
</div>
|
|
432
|
+
</div>
|
|
433
|
+
|
|
434
|
+
<div className="rounded-2xl border border-slate-200/80 bg-white p-4 shadow-sm">
|
|
435
|
+
<h2 className="text-sm font-semibold text-slate-900">Blocs de focus</h2>
|
|
436
|
+
<p className="mb-3 text-xs text-slate-500">
|
|
437
|
+
Plus longs segments d’activité continue (sans pause ≥ 5 min)
|
|
438
|
+
</p>
|
|
439
|
+
<div className="grid gap-2 sm:grid-cols-2 lg:grid-cols-3">
|
|
440
|
+
{(data.focusBlocks || []).length === 0 ? (
|
|
441
|
+
<p className="col-span-full py-6 text-center text-sm text-slate-400">
|
|
442
|
+
Pas encore de blocs de focus
|
|
443
|
+
</p>
|
|
444
|
+
) : (
|
|
445
|
+
data.focusBlocks.map((f, i) => (
|
|
446
|
+
<div
|
|
447
|
+
key={`${f.startedAt}-${i}`}
|
|
448
|
+
className="rounded-xl border border-slate-100 bg-gradient-to-br from-white to-sky-50/40 px-3 py-2.5"
|
|
449
|
+
>
|
|
450
|
+
<p className="text-sm font-semibold tabular-nums text-slate-900">
|
|
451
|
+
{formatDuration(f.durationMs)}
|
|
452
|
+
</p>
|
|
453
|
+
<p className="mt-0.5 text-[11px] text-slate-500">
|
|
454
|
+
{formatTs(f.startedAt)} · {f.events} événements
|
|
455
|
+
</p>
|
|
456
|
+
</div>
|
|
457
|
+
))
|
|
458
|
+
)}
|
|
459
|
+
</div>
|
|
460
|
+
</div>
|
|
461
|
+
</>
|
|
462
|
+
) : (
|
|
463
|
+
<p className="rounded-2xl border border-dashed border-slate-200 bg-white py-10 text-center text-sm text-slate-400">
|
|
464
|
+
Sélectionnez un collaborateur dans le classement pour voir sa heatmap détaillée.
|
|
465
|
+
</p>
|
|
466
|
+
)}
|
|
467
|
+
</div>
|
|
468
|
+
);
|
|
469
|
+
}
|
|
470
|
+
|
|
471
|
+
function MiniKpi({
|
|
472
|
+
label,
|
|
473
|
+
value,
|
|
474
|
+
hint,
|
|
475
|
+
icon: Icon,
|
|
476
|
+
accent,
|
|
477
|
+
}: {
|
|
478
|
+
label: string;
|
|
479
|
+
value: string;
|
|
480
|
+
hint?: string;
|
|
481
|
+
icon: React.ComponentType<{ className?: string }>;
|
|
482
|
+
accent: string;
|
|
483
|
+
}) {
|
|
484
|
+
return (
|
|
485
|
+
<div className="relative overflow-hidden rounded-2xl border border-slate-200/80 bg-white p-4 shadow-sm">
|
|
486
|
+
<div
|
|
487
|
+
className={cn(
|
|
488
|
+
"pointer-events-none absolute -right-6 -top-6 h-24 w-24 rounded-full opacity-[0.12]",
|
|
489
|
+
accent,
|
|
490
|
+
)}
|
|
491
|
+
/>
|
|
492
|
+
<div className="flex items-start justify-between gap-3">
|
|
493
|
+
<div>
|
|
494
|
+
<p className="text-xs font-medium uppercase tracking-wide text-slate-500">
|
|
495
|
+
{label}
|
|
496
|
+
</p>
|
|
497
|
+
<p className="mt-1 text-2xl font-semibold tabular-nums tracking-tight text-slate-900">
|
|
498
|
+
{value}
|
|
499
|
+
</p>
|
|
500
|
+
{hint ? <p className="mt-1 text-xs text-slate-500">{hint}</p> : null}
|
|
501
|
+
</div>
|
|
502
|
+
<div
|
|
503
|
+
className={cn(
|
|
504
|
+
"flex h-10 w-10 items-center justify-center rounded-xl text-white shadow-sm",
|
|
505
|
+
accent,
|
|
506
|
+
)}
|
|
507
|
+
>
|
|
508
|
+
<Icon className="h-5 w-5" />
|
|
509
|
+
</div>
|
|
510
|
+
</div>
|
|
511
|
+
</div>
|
|
512
|
+
);
|
|
513
|
+
}
|