@burdenoff/microfe-timecampus 2026.626.2 → 2026.703.2
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/generated/wspace-operations.js +996 -816
- package/dist/generated/wspace-operations.js.map +1 -1
- package/dist/generated/wspace-types.js +28 -0
- package/dist/generated/wspace-types.js.map +1 -0
- package/dist/index.js +4 -4
- package/dist/timecampus/TimeCampusRoutes.js +16 -12
- package/dist/timecampus/TimeCampusRoutes.js.map +1 -1
- package/dist/timecampus/components/Modal.js +65 -0
- package/dist/timecampus/components/Modal.js.map +1 -0
- package/dist/timecampus/components/StatusBadge.js +28 -0
- package/dist/timecampus/components/StatusBadge.js.map +1 -0
- package/dist/timecampus/components/Toast.js +87 -0
- package/dist/timecampus/components/Toast.js.map +1 -0
- package/dist/timecampus/index.js +1 -1
- package/dist/timecampus/pages/HomePage.js +143 -135
- package/dist/timecampus/pages/HomePage.js.map +1 -1
- package/dist/timecampus/pages/applets/AppletStorePage.js +14 -19
- package/dist/timecampus/pages/applets/AppletStorePage.js.map +1 -1
- package/dist/timecampus/pages/applets/InstalledAppletsPage.js +49 -37
- package/dist/timecampus/pages/applets/InstalledAppletsPage.js.map +1 -1
- package/dist/timecampus/pages/application-usage/ApplicationUsagePage.js +315 -87
- package/dist/timecampus/pages/application-usage/ApplicationUsagePage.js.map +1 -1
- package/dist/timecampus/pages/channels/ChannelsPage.js +340 -46
- package/dist/timecampus/pages/channels/ChannelsPage.js.map +1 -1
- package/dist/timecampus/pages/client-portal/ClientPortalPage.js +374 -92
- package/dist/timecampus/pages/client-portal/ClientPortalPage.js.map +1 -1
- package/dist/timecampus/pages/dashboard/ReportsPage.js +70 -37
- package/dist/timecampus/pages/dashboard/ReportsPage.js.map +1 -1
- package/dist/timecampus/pages/email-tracking/EmailTrackingPage.js +366 -60
- package/dist/timecampus/pages/email-tracking/EmailTrackingPage.js.map +1 -1
- package/dist/timecampus/pages/expense-tracking/ExpenseTrackingPage.js +461 -87
- package/dist/timecampus/pages/expense-tracking/ExpenseTrackingPage.js.map +1 -1
- package/dist/timecampus/pages/geofencing/GeofencingPage.js +314 -55
- package/dist/timecampus/pages/geofencing/GeofencingPage.js.map +1 -1
- package/dist/timecampus/pages/industry-solutions/IndustrySolutionsPage.js +30 -27
- package/dist/timecampus/pages/industry-solutions/IndustrySolutionsPage.js.map +1 -1
- package/dist/timecampus/pages/location-tracking/LocationTrackingPage.js +269 -74
- package/dist/timecampus/pages/location-tracking/LocationTrackingPage.js.map +1 -1
- package/dist/timecampus/pages/meetings/ActionItemFormModal.js +127 -0
- package/dist/timecampus/pages/meetings/ActionItemFormModal.js.map +1 -0
- package/dist/timecampus/pages/meetings/MeetingCreatePage.js +62 -52
- package/dist/timecampus/pages/meetings/MeetingCreatePage.js.map +1 -1
- package/dist/timecampus/pages/meetings/MeetingDetailPage.js +361 -74
- package/dist/timecampus/pages/meetings/MeetingDetailPage.js.map +1 -1
- package/dist/timecampus/pages/meetings/MeetingFormModal.js +156 -0
- package/dist/timecampus/pages/meetings/MeetingFormModal.js.map +1 -0
- package/dist/timecampus/pages/meetings/MeetingsPage.js +175 -74
- package/dist/timecampus/pages/meetings/MeetingsPage.js.map +1 -1
- package/dist/timecampus/pages/meetings/ParticipantFormModal.js +105 -0
- package/dist/timecampus/pages/meetings/ParticipantFormModal.js.map +1 -0
- package/dist/timecampus/pages/pomodoro/PomodoroPage.js +307 -67
- package/dist/timecampus/pages/pomodoro/PomodoroPage.js.map +1 -1
- package/dist/timecampus/pages/projects/ProjectCreatePage.js +51 -48
- package/dist/timecampus/pages/projects/ProjectCreatePage.js.map +1 -1
- package/dist/timecampus/pages/projects/ProjectDetailPage.js +248 -51
- package/dist/timecampus/pages/projects/ProjectDetailPage.js.map +1 -1
- package/dist/timecampus/pages/projects/ProjectsPage.js +354 -87
- package/dist/timecampus/pages/projects/ProjectsPage.js.map +1 -1
- package/dist/timecampus/pages/projects/TasksPage.js +367 -65
- package/dist/timecampus/pages/projects/TasksPage.js.map +1 -1
- package/dist/timecampus/pages/resource-planning/ResourcePlanningPage.js +674 -104
- package/dist/timecampus/pages/resource-planning/ResourcePlanningPage.js.map +1 -1
- package/dist/timecampus/pages/team/TeamAnalyticsPage.js +46 -34
- package/dist/timecampus/pages/team/TeamAnalyticsPage.js.map +1 -1
- package/dist/timecampus/pages/team/TeamMembersPage.js +93 -69
- package/dist/timecampus/pages/team/TeamMembersPage.js.map +1 -1
- package/dist/timecampus/pages/team/TeamPage.js +4 -3
- package/dist/timecampus/pages/team/TeamPage.js.map +1 -1
- package/dist/timecampus/pages/templates/TemplateDetailPage.js +72 -53
- package/dist/timecampus/pages/templates/TemplateDetailPage.js.map +1 -1
- package/dist/timecampus/pages/templates/TemplatesPage.js +40 -35
- package/dist/timecampus/pages/templates/TemplatesPage.js.map +1 -1
- package/dist/timecampus/pages/time/TimeEntriesPage.js +484 -97
- package/dist/timecampus/pages/time/TimeEntriesPage.js.map +1 -1
- package/dist/timecampus/pages/time/TimeTrackingPage.js +210 -99
- package/dist/timecampus/pages/time/TimeTrackingPage.js.map +1 -1
- package/dist/timecampus/pages/time/TimerPage.js +229 -97
- package/dist/timecampus/pages/time/TimerPage.js.map +1 -1
- package/dist/timecampus/pages/time-approval/TimeApprovalPage.js +315 -110
- package/dist/timecampus/pages/time-approval/TimeApprovalPage.js.map +1 -1
- package/dist/timecampus/pages/timeline/TimelinePage.js +250 -0
- package/dist/timecampus/pages/timeline/TimelinePage.js.map +1 -0
- package/dist/timecampus/pages/widgets/WidgetDetailPage.js +158 -76
- package/dist/timecampus/pages/widgets/WidgetDetailPage.js.map +1 -1
- package/dist/timecampus/pages/widgets/WidgetsPage.js +181 -52
- package/dist/timecampus/pages/widgets/WidgetsPage.js.map +1 -1
- package/dist/timecampus/pages/workflows/WorkflowsPage.js +212 -61
- package/dist/timecampus/pages/workflows/WorkflowsPage.js.map +1 -1
- package/dist/timecampus/store/timecampusStore.js +13 -1
- package/dist/timecampus/store/timecampusStore.js.map +1 -1
- package/dist/timecampus/utils/helpers.js +20 -10
- package/dist/timecampus/utils/helpers.js.map +1 -1
- package/package.json +1 -1
|
@@ -1,67 +1,361 @@
|
|
|
1
|
-
import {
|
|
2
|
-
import {
|
|
3
|
-
import {
|
|
4
|
-
import {
|
|
5
|
-
import {
|
|
6
|
-
import {
|
|
7
|
-
import {
|
|
8
|
-
import {
|
|
1
|
+
import { LoadingSkeleton as e } from "../../components/LoadingSkeleton.js";
|
|
2
|
+
import { useCreateChannelConfigMutation as t, useDeleteChannelConfigMutation as n, useListChannelConfigsQuery as r, useListChannelStatusesQuery as i, useSyncChannelMutation as ee, useToggleChannelMutation as a, useUpdateChannelConfigMutation as te } from "../../../generated/wspace-operations.js";
|
|
3
|
+
import { PageLayout as o } from "../../components/PageLayout.js";
|
|
4
|
+
import { EmptyState as ne } from "../../components/EmptyState.js";
|
|
5
|
+
import { ChannelStatus as s, TimeCampusChannelType as c } from "../../../generated/wspace-types.js";
|
|
6
|
+
import { Modal as l } from "../../components/Modal.js";
|
|
7
|
+
import { StatusBadge as re } from "../../components/StatusBadge.js";
|
|
8
|
+
import { ToastContainer as ie, useToast as ae } from "../../components/Toast.js";
|
|
9
|
+
import { useState as u } from "react";
|
|
10
|
+
import { Pencil as oe, Plus as d, Power as f, RefreshCw as p, Trash2 as m, Wifi as h, WifiOff as g } from "lucide-react";
|
|
11
|
+
import { Fragment as _, jsx as v, jsxs as y } from "react/jsx-runtime";
|
|
9
12
|
//#region src/timecampus/pages/channels/ChannelsPage.tsx
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
+
var b = 25, se = [
|
|
14
|
+
{
|
|
15
|
+
value: c.Calendar,
|
|
16
|
+
label: "Calendar"
|
|
17
|
+
},
|
|
18
|
+
{
|
|
19
|
+
value: c.Desktop,
|
|
20
|
+
label: "Desktop"
|
|
21
|
+
},
|
|
22
|
+
{
|
|
23
|
+
value: c.Browser,
|
|
24
|
+
label: "Browser"
|
|
25
|
+
},
|
|
26
|
+
{
|
|
27
|
+
value: c.Mobile,
|
|
28
|
+
label: "Mobile"
|
|
29
|
+
},
|
|
30
|
+
{
|
|
31
|
+
value: c.Ide,
|
|
32
|
+
label: "IDE"
|
|
33
|
+
},
|
|
34
|
+
{
|
|
35
|
+
value: c.Cli,
|
|
36
|
+
label: "CLI"
|
|
37
|
+
},
|
|
38
|
+
{
|
|
39
|
+
value: c.Api,
|
|
40
|
+
label: "API"
|
|
41
|
+
},
|
|
42
|
+
{
|
|
43
|
+
value: c.Custom,
|
|
44
|
+
label: "Custom"
|
|
45
|
+
}
|
|
46
|
+
], x = "mt-1 w-full rounded-md border border-border-subtle bg-bg-surface px-3 py-2 text-sm text-text-primary placeholder:text-text-secondary", S = {
|
|
47
|
+
name: "",
|
|
48
|
+
type: c.Calendar,
|
|
49
|
+
syncInterval: ""
|
|
50
|
+
};
|
|
51
|
+
function C() {
|
|
52
|
+
let { toasts: C, addToast: w, dismissToast: ce } = ae(), { data: le, loading: ue, error: de, refetch: T, fetchMore: E } = r({
|
|
53
|
+
variables: {
|
|
54
|
+
limit: b,
|
|
55
|
+
offset: 0
|
|
56
|
+
},
|
|
57
|
+
notifyOnNetworkStatusChange: !0
|
|
58
|
+
}), { data: D } = i(), [O, { loading: k }] = t(), [A, { loading: j }] = te(), [M, { loading: N }] = n(), [P, { loading: F }] = a(), [I, { loading: L }] = ee(), R = le?.channelConfigs ?? [], z = D?.channelStatuses ?? [], [B, V] = u(!1), [H, fe] = u(!0), [pe, U] = u(!1), [W, G] = u(null), [K, q] = u(S), [J, Y] = u(null), [me, X] = u(null), he = new Map(z.map((e) => [e.channelId, e])), ge = () => {
|
|
59
|
+
G(null), q(S), U(!0);
|
|
60
|
+
}, _e = (e) => {
|
|
61
|
+
G(e), q({
|
|
62
|
+
name: e.name,
|
|
63
|
+
type: Object.values(c).find((t) => t === e.type) ?? c.Custom,
|
|
64
|
+
syncInterval: e.syncInterval == null ? "" : String(e.syncInterval)
|
|
65
|
+
}), U(!0);
|
|
66
|
+
}, Z = () => {
|
|
67
|
+
U(!1), G(null);
|
|
68
|
+
}, Q = async () => {
|
|
69
|
+
if (!K.name.trim()) return;
|
|
70
|
+
let e = K.syncInterval ? Number(K.syncInterval) : void 0;
|
|
71
|
+
try {
|
|
72
|
+
W ? (await A({ variables: {
|
|
73
|
+
id: W.id,
|
|
74
|
+
input: {
|
|
75
|
+
name: K.name.trim(),
|
|
76
|
+
type: K.type,
|
|
77
|
+
syncInterval: e
|
|
78
|
+
}
|
|
79
|
+
} }), w({
|
|
80
|
+
title: "Channel updated",
|
|
81
|
+
variant: "success"
|
|
82
|
+
})) : (await O({ variables: { input: {
|
|
83
|
+
name: K.name.trim(),
|
|
84
|
+
type: K.type,
|
|
85
|
+
syncInterval: e,
|
|
86
|
+
status: s.Active
|
|
87
|
+
} } }), w({
|
|
88
|
+
title: "Channel added",
|
|
89
|
+
variant: "success"
|
|
90
|
+
})), Z(), await T();
|
|
91
|
+
} catch {
|
|
92
|
+
w({
|
|
93
|
+
title: W ? "Failed to update channel" : "Failed to add channel",
|
|
94
|
+
variant: "error"
|
|
95
|
+
});
|
|
96
|
+
}
|
|
97
|
+
}, ve = async () => {
|
|
98
|
+
if (J) try {
|
|
99
|
+
await M({ variables: { id: J.id } }), w({
|
|
100
|
+
title: "Channel deleted",
|
|
101
|
+
variant: "success"
|
|
102
|
+
}), Y(null), await T();
|
|
103
|
+
} catch {
|
|
104
|
+
w({
|
|
105
|
+
title: "Failed to delete channel",
|
|
106
|
+
variant: "error"
|
|
107
|
+
});
|
|
108
|
+
}
|
|
109
|
+
}, ye = async (e) => {
|
|
110
|
+
let t = e.status.toLowerCase() === "active";
|
|
111
|
+
X(e.id);
|
|
112
|
+
try {
|
|
113
|
+
await P({ variables: {
|
|
114
|
+
id: e.id,
|
|
115
|
+
status: t ? s.Inactive : s.Active
|
|
116
|
+
} }), w({
|
|
117
|
+
title: t ? "Channel disabled" : "Channel enabled",
|
|
118
|
+
variant: "success"
|
|
119
|
+
}), await T();
|
|
120
|
+
} catch {
|
|
121
|
+
w({
|
|
122
|
+
title: "Failed to toggle channel",
|
|
123
|
+
variant: "error"
|
|
124
|
+
});
|
|
125
|
+
} finally {
|
|
126
|
+
X(null);
|
|
127
|
+
}
|
|
128
|
+
}, be = async (e) => {
|
|
129
|
+
X(e.id);
|
|
130
|
+
try {
|
|
131
|
+
await I({ variables: { id: e.id } }), w({
|
|
132
|
+
title: "Sync started",
|
|
133
|
+
variant: "success"
|
|
134
|
+
}), await T();
|
|
135
|
+
} catch {
|
|
136
|
+
w({
|
|
137
|
+
title: "Failed to sync channel",
|
|
138
|
+
variant: "error"
|
|
139
|
+
});
|
|
140
|
+
} finally {
|
|
141
|
+
X(null);
|
|
142
|
+
}
|
|
143
|
+
}, xe = async () => {
|
|
144
|
+
V(!0);
|
|
145
|
+
try {
|
|
146
|
+
((await E({
|
|
147
|
+
variables: {
|
|
148
|
+
limit: b,
|
|
149
|
+
offset: R.length
|
|
150
|
+
},
|
|
151
|
+
updateQuery: (e, { fetchMoreResult: t }) => t ? {
|
|
152
|
+
...e,
|
|
153
|
+
channelConfigs: [...e.channelConfigs, ...t.channelConfigs]
|
|
154
|
+
} : e
|
|
155
|
+
})).data?.channelConfigs?.length ?? 0) < b && fe(!1);
|
|
156
|
+
} finally {
|
|
157
|
+
V(!1);
|
|
158
|
+
}
|
|
159
|
+
};
|
|
160
|
+
if (ue && R.length === 0) return /* @__PURE__ */ v(o, {
|
|
13
161
|
title: "Channels",
|
|
14
162
|
description: "Manage time tracking channels and integrations",
|
|
15
|
-
children: /* @__PURE__ */
|
|
163
|
+
children: /* @__PURE__ */ v(e, {
|
|
16
164
|
variant: "card",
|
|
17
165
|
count: 6
|
|
18
166
|
})
|
|
19
|
-
})
|
|
167
|
+
});
|
|
168
|
+
if (de) return /* @__PURE__ */ v(o, {
|
|
20
169
|
title: "Channels",
|
|
21
170
|
description: "Manage time tracking channels and integrations",
|
|
22
|
-
children: /* @__PURE__ */
|
|
171
|
+
children: /* @__PURE__ */ v("div", {
|
|
23
172
|
className: "rounded-lg border border-status-error-bg/40 bg-status-error-bg p-4 text-sm text-status-error-text",
|
|
24
173
|
children: "Failed to load channels data. Please try again later."
|
|
25
174
|
})
|
|
26
|
-
})
|
|
175
|
+
});
|
|
176
|
+
let $ = /* @__PURE__ */ y("button", {
|
|
177
|
+
onClick: ge,
|
|
178
|
+
className: "inline-flex items-center gap-1.5 rounded-md bg-action-primary-bg px-3 py-2 text-sm font-medium text-action-primary-text hover:bg-action-primary-bg/90",
|
|
179
|
+
children: [/* @__PURE__ */ v(d, { className: "h-4 w-4" }), "Add Channel"]
|
|
180
|
+
});
|
|
181
|
+
return /* @__PURE__ */ y(o, {
|
|
27
182
|
title: "Channels",
|
|
28
183
|
description: "Manage time tracking channels and integrations",
|
|
29
184
|
purpose: "Connect the tools and devices that feed time into TimeCampus — calendars, desktop trackers, integrations — and keep an eye on each connection's health so no work hours slip through the cracks.",
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
className: "
|
|
46
|
-
children:
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
185
|
+
actions: $,
|
|
186
|
+
children: [
|
|
187
|
+
R.length === 0 ? /* @__PURE__ */ v(ne, {
|
|
188
|
+
illustration: "empty-generic",
|
|
189
|
+
title: "No channels configured yet",
|
|
190
|
+
description: "Set up your first tracking channel to start pulling time and activity from your tools and devices.",
|
|
191
|
+
action: $
|
|
192
|
+
}) : /* @__PURE__ */ y(_, { children: [/* @__PURE__ */ v("div", {
|
|
193
|
+
className: "grid grid-cols-1 gap-4 sm:grid-cols-2 lg:grid-cols-3",
|
|
194
|
+
children: R.map((e) => {
|
|
195
|
+
let t = he.get(e.id), n = e.status.toLowerCase() === "active", r = e.lastSync ? new Date(e.lastSync).toLocaleString() : "Never", i = me === e.id;
|
|
196
|
+
return /* @__PURE__ */ y("div", {
|
|
197
|
+
className: "flex flex-col rounded-xl border border-border-seam bg-gloss-card shadow-pop p-5 transition-colors",
|
|
198
|
+
children: [
|
|
199
|
+
/* @__PURE__ */ y("div", {
|
|
200
|
+
className: "flex items-start justify-between",
|
|
201
|
+
children: [/* @__PURE__ */ y("div", {
|
|
202
|
+
className: "flex items-center gap-3",
|
|
203
|
+
children: [n ? /* @__PURE__ */ v(h, { className: "h-5 w-5 text-status-success-text" }) : /* @__PURE__ */ v(g, { className: "h-5 w-5 text-text-secondary" }), /* @__PURE__ */ y("div", { children: [/* @__PURE__ */ v("p", {
|
|
204
|
+
className: "text-sm font-semibold text-text-primary",
|
|
205
|
+
children: e.name
|
|
206
|
+
}), /* @__PURE__ */ v("p", {
|
|
207
|
+
className: "text-xs text-text-secondary",
|
|
208
|
+
children: e.type
|
|
209
|
+
})] })]
|
|
210
|
+
}), /* @__PURE__ */ v(re, {
|
|
211
|
+
label: e.status,
|
|
212
|
+
status: e.status.toLowerCase()
|
|
213
|
+
})]
|
|
214
|
+
}),
|
|
215
|
+
/* @__PURE__ */ y("div", {
|
|
216
|
+
className: "mt-3 flex items-center justify-between text-xs text-text-secondary",
|
|
217
|
+
children: [/* @__PURE__ */ v("span", { children: t?.isHealthy ? "Healthy" : "Unhealthy" }), /* @__PURE__ */ y("span", { children: ["Sync: ", r] })]
|
|
218
|
+
}),
|
|
219
|
+
/* @__PURE__ */ y("div", {
|
|
220
|
+
className: "mt-4 flex items-center gap-1.5 border-t border-border-seam pt-3",
|
|
221
|
+
children: [
|
|
222
|
+
/* @__PURE__ */ y("button", {
|
|
223
|
+
type: "button",
|
|
224
|
+
disabled: i && L,
|
|
225
|
+
onClick: () => be(e),
|
|
226
|
+
title: "Sync now",
|
|
227
|
+
className: "inline-flex items-center gap-1 rounded-md border border-border-seam bg-bg-surface px-2 py-1 text-xs text-text-secondary hover:bg-bg-sunken/50 disabled:opacity-50",
|
|
228
|
+
children: [/* @__PURE__ */ v(p, { className: `h-3.5 w-3.5 ${i && L ? "animate-spin" : ""}` }), "Sync"]
|
|
229
|
+
}),
|
|
230
|
+
/* @__PURE__ */ y("button", {
|
|
231
|
+
type: "button",
|
|
232
|
+
disabled: i && F,
|
|
233
|
+
onClick: () => ye(e),
|
|
234
|
+
title: n ? "Disable channel" : "Enable channel",
|
|
235
|
+
className: "inline-flex items-center gap-1 rounded-md border border-border-seam bg-bg-surface px-2 py-1 text-xs text-text-secondary hover:bg-bg-sunken/50 disabled:opacity-50",
|
|
236
|
+
children: [/* @__PURE__ */ v(f, { className: "h-3.5 w-3.5" }), n ? "Disable" : "Enable"]
|
|
237
|
+
}),
|
|
238
|
+
/* @__PURE__ */ y("div", {
|
|
239
|
+
className: "ml-auto flex items-center gap-1",
|
|
240
|
+
children: [/* @__PURE__ */ v("button", {
|
|
241
|
+
type: "button",
|
|
242
|
+
onClick: () => _e(e),
|
|
243
|
+
title: "Edit channel",
|
|
244
|
+
className: "inline-flex items-center rounded-md border border-border-seam bg-bg-surface p-1.5 text-text-secondary hover:bg-bg-sunken/50",
|
|
245
|
+
children: /* @__PURE__ */ v(oe, { className: "h-3.5 w-3.5" })
|
|
246
|
+
}), /* @__PURE__ */ v("button", {
|
|
247
|
+
type: "button",
|
|
248
|
+
onClick: () => Y(e),
|
|
249
|
+
title: "Delete channel",
|
|
250
|
+
className: "inline-flex items-center rounded-md border border-border-seam bg-bg-surface p-1.5 text-status-error-text hover:bg-status-error-bg",
|
|
251
|
+
children: /* @__PURE__ */ v(m, { className: "h-3.5 w-3.5" })
|
|
252
|
+
})]
|
|
253
|
+
})
|
|
254
|
+
]
|
|
255
|
+
})
|
|
256
|
+
]
|
|
257
|
+
}, e.id);
|
|
258
|
+
})
|
|
259
|
+
}), H && R.length >= b && /* @__PURE__ */ v("div", {
|
|
260
|
+
className: "mt-4 flex justify-center",
|
|
261
|
+
children: /* @__PURE__ */ v("button", {
|
|
262
|
+
onClick: xe,
|
|
263
|
+
disabled: B,
|
|
264
|
+
className: "rounded-md border border-border-subtle px-4 py-2 text-sm font-medium text-text-primary hover:bg-bg-sunken/50 disabled:opacity-50",
|
|
265
|
+
children: B ? "Loading..." : "Load more"
|
|
266
|
+
})
|
|
267
|
+
})] }),
|
|
268
|
+
/* @__PURE__ */ v(l, {
|
|
269
|
+
open: pe,
|
|
270
|
+
onClose: Z,
|
|
271
|
+
title: W ? "Edit Channel" : "Add Channel",
|
|
272
|
+
description: W ? "Update this channel's configuration." : "Connect a new tracking channel to feed time into TimeCampus.",
|
|
273
|
+
footer: /* @__PURE__ */ y(_, { children: [/* @__PURE__ */ v("button", {
|
|
274
|
+
onClick: Z,
|
|
275
|
+
className: "rounded-md border border-border-subtle px-4 py-2 text-sm font-medium text-text-primary hover:bg-bg-sunken/50",
|
|
276
|
+
children: "Cancel"
|
|
277
|
+
}), /* @__PURE__ */ v("button", {
|
|
278
|
+
onClick: Q,
|
|
279
|
+
disabled: k || j || !K.name.trim(),
|
|
280
|
+
className: "rounded-md bg-action-primary-bg px-4 py-2 text-sm font-medium text-action-primary-text hover:bg-action-primary-bg/90 disabled:opacity-50",
|
|
281
|
+
children: k || j ? "Saving..." : W ? "Save Changes" : "Add Channel"
|
|
282
|
+
})] }),
|
|
283
|
+
children: /* @__PURE__ */ y("div", {
|
|
284
|
+
className: "space-y-4",
|
|
285
|
+
children: [/* @__PURE__ */ y("div", { children: [/* @__PURE__ */ v("label", {
|
|
286
|
+
className: "block text-sm font-medium text-text-primary",
|
|
287
|
+
children: "Name"
|
|
288
|
+
}), /* @__PURE__ */ v("input", {
|
|
289
|
+
type: "text",
|
|
290
|
+
placeholder: "e.g. Google Calendar",
|
|
291
|
+
value: K.name,
|
|
292
|
+
onChange: (e) => q({
|
|
293
|
+
...K,
|
|
294
|
+
name: e.target.value
|
|
295
|
+
}),
|
|
296
|
+
className: x
|
|
297
|
+
})] }), /* @__PURE__ */ y("div", {
|
|
298
|
+
className: "grid grid-cols-1 gap-4 sm:grid-cols-2",
|
|
299
|
+
children: [/* @__PURE__ */ y("div", { children: [/* @__PURE__ */ v("label", {
|
|
300
|
+
className: "block text-sm font-medium text-text-primary",
|
|
301
|
+
children: "Type"
|
|
302
|
+
}), /* @__PURE__ */ v("select", {
|
|
303
|
+
value: K.type,
|
|
304
|
+
onChange: (e) => q({
|
|
305
|
+
...K,
|
|
306
|
+
type: e.target.value
|
|
307
|
+
}),
|
|
308
|
+
className: x,
|
|
309
|
+
children: se.map((e) => /* @__PURE__ */ v("option", {
|
|
310
|
+
value: e.value,
|
|
311
|
+
children: e.label
|
|
312
|
+
}, e.value))
|
|
313
|
+
})] }), /* @__PURE__ */ y("div", { children: [/* @__PURE__ */ v("label", {
|
|
314
|
+
className: "block text-sm font-medium text-text-primary",
|
|
315
|
+
children: "Sync Interval (seconds)"
|
|
316
|
+
}), /* @__PURE__ */ v("input", {
|
|
317
|
+
type: "number",
|
|
318
|
+
min: 0,
|
|
319
|
+
placeholder: "e.g. 300",
|
|
320
|
+
value: K.syncInterval,
|
|
321
|
+
onChange: (e) => q({
|
|
322
|
+
...K,
|
|
323
|
+
syncInterval: e.target.value
|
|
324
|
+
}),
|
|
325
|
+
className: x
|
|
326
|
+
})] })]
|
|
58
327
|
})]
|
|
59
|
-
}
|
|
328
|
+
})
|
|
329
|
+
}),
|
|
330
|
+
/* @__PURE__ */ v(l, {
|
|
331
|
+
open: J !== null,
|
|
332
|
+
onClose: () => Y(null),
|
|
333
|
+
title: "Delete channel?",
|
|
334
|
+
description: J ? `"${J.name}" will be removed. This cannot be undone.` : void 0,
|
|
335
|
+
size: "sm",
|
|
336
|
+
footer: /* @__PURE__ */ y(_, { children: [/* @__PURE__ */ v("button", {
|
|
337
|
+
onClick: () => Y(null),
|
|
338
|
+
className: "rounded-md border border-border-subtle px-4 py-2 text-sm font-medium text-text-primary hover:bg-bg-sunken/50",
|
|
339
|
+
children: "Cancel"
|
|
340
|
+
}), /* @__PURE__ */ v("button", {
|
|
341
|
+
onClick: ve,
|
|
342
|
+
disabled: N,
|
|
343
|
+
className: "rounded-md bg-status-error-bg px-4 py-2 text-sm font-medium text-status-error-text hover:bg-status-error-bg/80 disabled:opacity-50",
|
|
344
|
+
children: N ? "Deleting..." : "Delete"
|
|
345
|
+
})] }),
|
|
346
|
+
children: /* @__PURE__ */ v("p", {
|
|
347
|
+
className: "text-sm text-text-secondary",
|
|
348
|
+
children: "Deleting a channel stops it from feeding new time into TimeCampus. Previously captured entries are kept."
|
|
349
|
+
})
|
|
350
|
+
}),
|
|
351
|
+
/* @__PURE__ */ v(ie, {
|
|
352
|
+
toasts: C,
|
|
353
|
+
onDismiss: ce
|
|
60
354
|
})
|
|
61
|
-
|
|
355
|
+
]
|
|
62
356
|
});
|
|
63
357
|
}
|
|
64
358
|
//#endregion
|
|
65
|
-
export {
|
|
359
|
+
export { C as ChannelsPage };
|
|
66
360
|
|
|
67
361
|
//# sourceMappingURL=ChannelsPage.js.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"ChannelsPage.js","names":[],"sources":["../../../../src/timecampus/pages/channels/ChannelsPage.tsx"],"sourcesContent":["import { Wifi, WifiOff } from \"lucide-react\";\nimport { PageLayout } from \"../../components/PageLayout\";\nimport { EmptyState } from \"../../components/EmptyState\";\nimport { LoadingSkeleton } from \"../../components/LoadingSkeleton\";\nimport { getStatusColor } from \"../../utils/helpers\";\nimport { useTimeCampus } from \"../../providers/TimeCampusProvider\";\nimport {\n useListChannelConfigsQuery,\n useListChannelStatusesQuery,\n type ChannelConfigFieldsFragment,\n} from \"@/generated/wspace-operations\";\n\nexport function ChannelsPage() {\n const { workspaceId = \"\" } = useTimeCampus();\n\n const {\n data: configsData,\n loading: configsLoading,\n error: configsError,\n } = useListChannelConfigsQuery();\n const { data: statusesData } = useListChannelStatusesQuery();\n\n const channels = configsData?.channelConfigs ?? [];\n const statuses = statusesData?.channelStatuses ?? [];\n\n type ChannelStatusItem = {\n channelId: string;\n name: string;\n type: string;\n status: string;\n isHealthy: boolean;\n lastSync?: string | null;\n };\n\n // Build a lookup map for status info\n const statusMap = new Map<string, ChannelStatusItem>(\n statuses.map((s: ChannelStatusItem) => [s.channelId, s]),\n );\n\n if (configsLoading) {\n return (\n <PageLayout\n title=\"Channels\"\n description=\"Manage time tracking channels and integrations\"\n >\n <LoadingSkeleton variant=\"card\" count={6} />\n </PageLayout>\n );\n }\n\n if (configsError) {\n return (\n <PageLayout\n title=\"Channels\"\n description=\"Manage time tracking channels and integrations\"\n >\n <div className=\"rounded-lg border border-status-error-bg/40 bg-status-error-bg p-4 text-sm text-status-error-text\">\n Failed to load channels data. Please try again later.\n </div>\n </PageLayout>\n );\n }\n\n return (\n <PageLayout\n title=\"Channels\"\n description=\"Manage time tracking channels and integrations\"\n purpose=\"Connect the tools and devices that feed time into TimeCampus — calendars, desktop trackers, integrations — and keep an eye on each connection's health so no work hours slip through the cracks.\"\n >\n {channels.length === 0 ? (\n <EmptyState\n illustration=\"empty-generic\"\n title=\"No channels configured yet\"\n description=\"Set up your first tracking channel to start pulling time and activity from your tools and devices.\"\n />\n ) : (\n <div className=\"grid grid-cols-1 gap-4 sm:grid-cols-2 lg:grid-cols-3\">\n {channels.map((channel: ChannelConfigFieldsFragment) => {\n const statusInfo = statusMap.get(channel.id);\n const isActive = channel.status === \"active\";\n const lastSync = channel.lastSync\n ? new Date(channel.lastSync).toLocaleString()\n : \"Never\";\n\n return (\n <div\n key={channel.id}\n className=\"rounded-xl border border-border-seam bg-gloss-card shadow-pop p-5 transition-colors hover:bg-bg-sunken/50 cursor-pointer\"\n >\n <div className=\"flex items-start justify-between\">\n <div className=\"flex items-center gap-3\">\n {isActive ? (\n <Wifi className=\"h-5 w-5 text-status-success-text\" />\n ) : (\n <WifiOff className=\"h-5 w-5 text-text-secondary\" />\n )}\n <div>\n <p className=\"text-sm font-semibold text-text-primary\">\n {channel.name}\n </p>\n <p className=\"text-xs text-text-secondary\">\n {channel.type}\n </p>\n </div>\n </div>\n <span\n className={`inline-flex rounded-full px-2 py-0.5 text-xs font-medium ${getStatusColor(channel.status)}`}\n >\n {channel.status}\n </span>\n </div>\n <div className=\"mt-3 flex items-center justify-between text-xs text-text-secondary\">\n <span>{statusInfo?.isHealthy ? \"Healthy\" : \"Unhealthy\"}</span>\n <span>Sync: {lastSync}</span>\n </div>\n </div>\n );\n })}\n </div>\n )}\n </PageLayout>\n );\n}\n"],"mappings":";;;;;;;;;AAYA,SAAgB,IAAe;CAC7B,IAAM,EAAE,iBAAc,OAAO,GAAe,EAEtC,EACJ,MAAM,GACN,SAAS,GACT,OAAO,MACL,GAA4B,EAC1B,EAAE,MAAM,MAAiB,GAA6B,EAEtD,IAAW,GAAa,kBAAkB,EAAE,EAC5C,IAAW,GAAc,mBAAmB,EAAE,EAY9C,IAAY,IAAI,IACpB,EAAS,KAAK,MAAyB,CAAC,EAAE,WAAW,EAAE,CAAC,CACzD;AA0BD,QAxBI,IAEA,kBAAC,GAAD;EACE,OAAM;EACN,aAAY;YAEZ,kBAAC,GAAD;GAAiB,SAAQ;GAAO,OAAO;GAAK,CAAA;EACjC,CAAA,GAIb,IAEA,kBAAC,GAAD;EACE,OAAM;EACN,aAAY;YAEZ,kBAAC,OAAD;GAAK,WAAU;aAAoG;GAE7G,CAAA;EACK,CAAA,GAKf,kBAAC,GAAD;EACE,OAAM;EACN,aAAY;EACZ,SAAQ;YAEP,EAAS,WAAW,IACnB,kBAAC,GAAD;GACE,cAAa;GACb,OAAM;GACN,aAAY;GACZ,CAAA,GAEF,kBAAC,OAAD;GAAK,WAAU;aACZ,EAAS,KAAK,MAAyC;IACtD,IAAM,IAAa,EAAU,IAAI,EAAQ,GAAG,EACtC,IAAW,EAAQ,WAAW,UAC9B,IAAW,EAAQ,WACrB,IAAI,KAAK,EAAQ,SAAS,CAAC,gBAAgB,GAC3C;AAEJ,WACE,kBAAC,OAAD;KAEE,WAAU;eAFZ,CAIE,kBAAC,OAAD;MAAK,WAAU;gBAAf,CACE,kBAAC,OAAD;OAAK,WAAU;iBAAf,CACG,IACC,kBAAC,GAAD,EAAM,WAAU,oCAAqC,CAAA,GAErD,kBAAC,GAAD,EAAS,WAAU,+BAAgC,CAAA,EAErD,kBAAC,OAAD,EAAA,UAAA,CACE,kBAAC,KAAD;QAAG,WAAU;kBACV,EAAQ;QACP,CAAA,EACJ,kBAAC,KAAD;QAAG,WAAU;kBACV,EAAQ;QACP,CAAA,CACA,EAAA,CAAA,CACF;UACN,kBAAC,QAAD;OACE,WAAW,4DAA4D,EAAe,EAAQ,OAAO;iBAEpG,EAAQ;OACJ,CAAA,CACH;SACN,kBAAC,OAAD;MAAK,WAAU;gBAAf,CACE,kBAAC,QAAD,EAAA,UAAO,GAAY,YAAY,YAAY,aAAmB,CAAA,EAC9D,kBAAC,QAAD,EAAA,UAAA,CAAM,UAAO,EAAgB,EAAA,CAAA,CACzB;QACF;OA7BC,EAAQ,GA6BT;KAER;GACE,CAAA;EAEG,CAAA"}
|
|
1
|
+
{"version":3,"file":"ChannelsPage.js","names":[],"sources":["../../../../src/timecampus/pages/channels/ChannelsPage.tsx"],"sourcesContent":["import { useState } from \"react\";\nimport {\n Wifi,\n WifiOff,\n Plus,\n RefreshCw,\n Pencil,\n Trash2,\n Power,\n} from \"lucide-react\";\nimport { PageLayout } from \"../../components/PageLayout\";\nimport { EmptyState } from \"../../components/EmptyState\";\nimport { LoadingSkeleton } from \"../../components/LoadingSkeleton\";\nimport { StatusBadge } from \"../../components/StatusBadge\";\nimport { Modal } from \"../../components/Modal\";\nimport { ToastContainer, useToast } from \"../../components/Toast\";\nimport {\n useListChannelConfigsQuery,\n useListChannelStatusesQuery,\n useCreateChannelConfigMutation,\n useUpdateChannelConfigMutation,\n useDeleteChannelConfigMutation,\n useToggleChannelMutation,\n useSyncChannelMutation,\n type ChannelConfigFieldsFragment,\n} from \"@/generated/wspace-operations\";\nimport { ChannelStatus, TimeCampusChannelType } from \"@/generated/wspace-types\";\n\nconst PAGE_SIZE = 25;\n\nconst CHANNEL_TYPE_OPTIONS: { value: TimeCampusChannelType; label: string }[] =\n [\n { value: TimeCampusChannelType.Calendar, label: \"Calendar\" },\n { value: TimeCampusChannelType.Desktop, label: \"Desktop\" },\n { value: TimeCampusChannelType.Browser, label: \"Browser\" },\n { value: TimeCampusChannelType.Mobile, label: \"Mobile\" },\n { value: TimeCampusChannelType.Ide, label: \"IDE\" },\n { value: TimeCampusChannelType.Cli, label: \"CLI\" },\n { value: TimeCampusChannelType.Api, label: \"API\" },\n { value: TimeCampusChannelType.Custom, label: \"Custom\" },\n ];\n\nconst inputClass =\n \"mt-1 w-full rounded-md border border-border-subtle bg-bg-surface px-3 py-2 text-sm text-text-primary placeholder:text-text-secondary\";\n\ninterface ChannelFormState {\n name: string;\n type: TimeCampusChannelType;\n syncInterval: string;\n}\n\nconst emptyForm: ChannelFormState = {\n name: \"\",\n type: TimeCampusChannelType.Calendar,\n syncInterval: \"\",\n};\n\nexport function ChannelsPage() {\n const { toasts, addToast, dismissToast } = useToast();\n\n const {\n data: configsData,\n loading: configsLoading,\n error: configsError,\n refetch,\n fetchMore,\n } = useListChannelConfigsQuery({\n variables: { limit: PAGE_SIZE, offset: 0 },\n notifyOnNetworkStatusChange: true,\n });\n const { data: statusesData } = useListChannelStatusesQuery();\n\n const [createChannel, { loading: creating }] =\n useCreateChannelConfigMutation();\n const [updateChannel, { loading: updating }] =\n useUpdateChannelConfigMutation();\n const [deleteChannel, { loading: deleting }] =\n useDeleteChannelConfigMutation();\n const [toggleChannel, { loading: toggling }] = useToggleChannelMutation();\n const [syncChannel, { loading: syncing }] = useSyncChannelMutation();\n\n const channels = configsData?.channelConfigs ?? [];\n const statuses = statusesData?.channelStatuses ?? [];\n\n const [loadingMore, setLoadingMore] = useState(false);\n const [canLoadMore, setCanLoadMore] = useState(true);\n\n // Create / edit modal state\n const [formOpen, setFormOpen] = useState(false);\n const [editing, setEditing] = useState<ChannelConfigFieldsFragment | null>(\n null,\n );\n const [form, setForm] = useState<ChannelFormState>(emptyForm);\n\n // Delete confirm state\n const [deleteTarget, setDeleteTarget] =\n useState<ChannelConfigFieldsFragment | null>(null);\n\n // Per-row action tracking (which channel id is mid-mutation)\n const [busyId, setBusyId] = useState<string | null>(null);\n\n type ChannelStatusItem = {\n channelId: string;\n name: string;\n type: string;\n status: string;\n isHealthy: boolean;\n lastSync?: string | null;\n };\n\n const statusMap = new Map<string, ChannelStatusItem>(\n statuses.map((s: ChannelStatusItem) => [s.channelId, s]),\n );\n\n const openCreate = () => {\n setEditing(null);\n setForm(emptyForm);\n setFormOpen(true);\n };\n\n const openEdit = (channel: ChannelConfigFieldsFragment) => {\n setEditing(channel);\n setForm({\n name: channel.name,\n type:\n (Object.values(TimeCampusChannelType).find(\n (t) => t === channel.type,\n ) as TimeCampusChannelType | undefined) ?? TimeCampusChannelType.Custom,\n syncInterval:\n channel.syncInterval != null ? String(channel.syncInterval) : \"\",\n });\n setFormOpen(true);\n };\n\n const closeForm = () => {\n setFormOpen(false);\n setEditing(null);\n };\n\n const handleSubmit = async () => {\n if (!form.name.trim()) return;\n const syncInterval = form.syncInterval\n ? Number(form.syncInterval)\n : undefined;\n try {\n if (editing) {\n await updateChannel({\n variables: {\n id: editing.id,\n input: {\n name: form.name.trim(),\n type: form.type,\n syncInterval,\n },\n },\n });\n addToast({ title: \"Channel updated\", variant: \"success\" });\n } else {\n await createChannel({\n variables: {\n input: {\n name: form.name.trim(),\n type: form.type,\n syncInterval,\n status: ChannelStatus.Active,\n },\n },\n });\n addToast({ title: \"Channel added\", variant: \"success\" });\n }\n closeForm();\n await refetch();\n } catch {\n addToast({\n title: editing ? \"Failed to update channel\" : \"Failed to add channel\",\n variant: \"error\",\n });\n }\n };\n\n const handleDelete = async () => {\n if (!deleteTarget) return;\n try {\n await deleteChannel({ variables: { id: deleteTarget.id } });\n addToast({ title: \"Channel deleted\", variant: \"success\" });\n setDeleteTarget(null);\n await refetch();\n } catch {\n addToast({ title: \"Failed to delete channel\", variant: \"error\" });\n }\n };\n\n const handleToggle = async (channel: ChannelConfigFieldsFragment) => {\n const isActive = channel.status.toLowerCase() === \"active\";\n setBusyId(channel.id);\n try {\n await toggleChannel({\n variables: {\n id: channel.id,\n status: isActive ? ChannelStatus.Inactive : ChannelStatus.Active,\n },\n });\n addToast({\n title: isActive ? \"Channel disabled\" : \"Channel enabled\",\n variant: \"success\",\n });\n await refetch();\n } catch {\n addToast({ title: \"Failed to toggle channel\", variant: \"error\" });\n } finally {\n setBusyId(null);\n }\n };\n\n const handleSync = async (channel: ChannelConfigFieldsFragment) => {\n setBusyId(channel.id);\n try {\n await syncChannel({ variables: { id: channel.id } });\n addToast({ title: \"Sync started\", variant: \"success\" });\n await refetch();\n } catch {\n addToast({ title: \"Failed to sync channel\", variant: \"error\" });\n } finally {\n setBusyId(null);\n }\n };\n\n const handleLoadMore = async () => {\n setLoadingMore(true);\n try {\n const res = await fetchMore({\n variables: { limit: PAGE_SIZE, offset: channels.length },\n updateQuery: (prev, { fetchMoreResult }) => {\n if (!fetchMoreResult) return prev;\n return {\n ...prev,\n channelConfigs: [\n ...prev.channelConfigs,\n ...fetchMoreResult.channelConfigs,\n ],\n };\n },\n });\n if ((res.data?.channelConfigs?.length ?? 0) < PAGE_SIZE) {\n setCanLoadMore(false);\n }\n } finally {\n setLoadingMore(false);\n }\n };\n\n if (configsLoading && channels.length === 0) {\n return (\n <PageLayout\n title=\"Channels\"\n description=\"Manage time tracking channels and integrations\"\n >\n <LoadingSkeleton variant=\"card\" count={6} />\n </PageLayout>\n );\n }\n\n if (configsError) {\n return (\n <PageLayout\n title=\"Channels\"\n description=\"Manage time tracking channels and integrations\"\n >\n <div className=\"rounded-lg border border-status-error-bg/40 bg-status-error-bg p-4 text-sm text-status-error-text\">\n Failed to load channels data. Please try again later.\n </div>\n </PageLayout>\n );\n }\n\n const addButton = (\n <button\n onClick={openCreate}\n className=\"inline-flex items-center gap-1.5 rounded-md bg-action-primary-bg px-3 py-2 text-sm font-medium text-action-primary-text hover:bg-action-primary-bg/90\"\n >\n <Plus className=\"h-4 w-4\" />\n Add Channel\n </button>\n );\n\n return (\n <PageLayout\n title=\"Channels\"\n description=\"Manage time tracking channels and integrations\"\n purpose=\"Connect the tools and devices that feed time into TimeCampus — calendars, desktop trackers, integrations — and keep an eye on each connection's health so no work hours slip through the cracks.\"\n actions={addButton}\n >\n {channels.length === 0 ? (\n <EmptyState\n illustration=\"empty-generic\"\n title=\"No channels configured yet\"\n description=\"Set up your first tracking channel to start pulling time and activity from your tools and devices.\"\n action={addButton}\n />\n ) : (\n <>\n <div className=\"grid grid-cols-1 gap-4 sm:grid-cols-2 lg:grid-cols-3\">\n {channels.map((channel: ChannelConfigFieldsFragment) => {\n const statusInfo = statusMap.get(channel.id);\n const isActive = channel.status.toLowerCase() === \"active\";\n const lastSync = channel.lastSync\n ? new Date(channel.lastSync).toLocaleString()\n : \"Never\";\n const rowBusy = busyId === channel.id;\n\n return (\n <div\n key={channel.id}\n className=\"flex flex-col rounded-xl border border-border-seam bg-gloss-card shadow-pop p-5 transition-colors\"\n >\n <div className=\"flex items-start justify-between\">\n <div className=\"flex items-center gap-3\">\n {isActive ? (\n <Wifi className=\"h-5 w-5 text-status-success-text\" />\n ) : (\n <WifiOff className=\"h-5 w-5 text-text-secondary\" />\n )}\n <div>\n <p className=\"text-sm font-semibold text-text-primary\">\n {channel.name}\n </p>\n <p className=\"text-xs text-text-secondary\">\n {channel.type}\n </p>\n </div>\n </div>\n <StatusBadge\n label={channel.status}\n status={channel.status.toLowerCase()}\n />\n </div>\n <div className=\"mt-3 flex items-center justify-between text-xs text-text-secondary\">\n <span>\n {statusInfo?.isHealthy ? \"Healthy\" : \"Unhealthy\"}\n </span>\n <span>Sync: {lastSync}</span>\n </div>\n <div className=\"mt-4 flex items-center gap-1.5 border-t border-border-seam pt-3\">\n <button\n type=\"button\"\n disabled={rowBusy && syncing}\n onClick={() => handleSync(channel)}\n title=\"Sync now\"\n className=\"inline-flex items-center gap-1 rounded-md border border-border-seam bg-bg-surface px-2 py-1 text-xs text-text-secondary hover:bg-bg-sunken/50 disabled:opacity-50\"\n >\n <RefreshCw\n className={`h-3.5 w-3.5 ${rowBusy && syncing ? \"animate-spin\" : \"\"}`}\n />\n Sync\n </button>\n <button\n type=\"button\"\n disabled={rowBusy && toggling}\n onClick={() => handleToggle(channel)}\n title={isActive ? \"Disable channel\" : \"Enable channel\"}\n className=\"inline-flex items-center gap-1 rounded-md border border-border-seam bg-bg-surface px-2 py-1 text-xs text-text-secondary hover:bg-bg-sunken/50 disabled:opacity-50\"\n >\n <Power className=\"h-3.5 w-3.5\" />\n {isActive ? \"Disable\" : \"Enable\"}\n </button>\n <div className=\"ml-auto flex items-center gap-1\">\n <button\n type=\"button\"\n onClick={() => openEdit(channel)}\n title=\"Edit channel\"\n className=\"inline-flex items-center rounded-md border border-border-seam bg-bg-surface p-1.5 text-text-secondary hover:bg-bg-sunken/50\"\n >\n <Pencil className=\"h-3.5 w-3.5\" />\n </button>\n <button\n type=\"button\"\n onClick={() => setDeleteTarget(channel)}\n title=\"Delete channel\"\n className=\"inline-flex items-center rounded-md border border-border-seam bg-bg-surface p-1.5 text-status-error-text hover:bg-status-error-bg\"\n >\n <Trash2 className=\"h-3.5 w-3.5\" />\n </button>\n </div>\n </div>\n </div>\n );\n })}\n </div>\n\n {canLoadMore && channels.length >= PAGE_SIZE && (\n <div className=\"mt-4 flex justify-center\">\n <button\n onClick={handleLoadMore}\n disabled={loadingMore}\n className=\"rounded-md border border-border-subtle px-4 py-2 text-sm font-medium text-text-primary hover:bg-bg-sunken/50 disabled:opacity-50\"\n >\n {loadingMore ? \"Loading...\" : \"Load more\"}\n </button>\n </div>\n )}\n </>\n )}\n\n <Modal\n open={formOpen}\n onClose={closeForm}\n title={editing ? \"Edit Channel\" : \"Add Channel\"}\n description={\n editing\n ? \"Update this channel's configuration.\"\n : \"Connect a new tracking channel to feed time into TimeCampus.\"\n }\n footer={\n <>\n <button\n onClick={closeForm}\n className=\"rounded-md border border-border-subtle px-4 py-2 text-sm font-medium text-text-primary hover:bg-bg-sunken/50\"\n >\n Cancel\n </button>\n <button\n onClick={handleSubmit}\n disabled={creating || updating || !form.name.trim()}\n className=\"rounded-md bg-action-primary-bg px-4 py-2 text-sm font-medium text-action-primary-text hover:bg-action-primary-bg/90 disabled:opacity-50\"\n >\n {creating || updating\n ? \"Saving...\"\n : editing\n ? \"Save Changes\"\n : \"Add Channel\"}\n </button>\n </>\n }\n >\n <div className=\"space-y-4\">\n <div>\n <label className=\"block text-sm font-medium text-text-primary\">\n Name\n </label>\n <input\n type=\"text\"\n placeholder=\"e.g. Google Calendar\"\n value={form.name}\n onChange={(e) => setForm({ ...form, name: e.target.value })}\n className={inputClass}\n />\n </div>\n <div className=\"grid grid-cols-1 gap-4 sm:grid-cols-2\">\n <div>\n <label className=\"block text-sm font-medium text-text-primary\">\n Type\n </label>\n <select\n value={form.type}\n onChange={(e) =>\n setForm({\n ...form,\n type: e.target.value as TimeCampusChannelType,\n })\n }\n className={inputClass}\n >\n {CHANNEL_TYPE_OPTIONS.map((opt) => (\n <option key={opt.value} value={opt.value}>\n {opt.label}\n </option>\n ))}\n </select>\n </div>\n <div>\n <label className=\"block text-sm font-medium text-text-primary\">\n Sync Interval (seconds)\n </label>\n <input\n type=\"number\"\n min={0}\n placeholder=\"e.g. 300\"\n value={form.syncInterval}\n onChange={(e) =>\n setForm({ ...form, syncInterval: e.target.value })\n }\n className={inputClass}\n />\n </div>\n </div>\n </div>\n </Modal>\n\n <Modal\n open={deleteTarget !== null}\n onClose={() => setDeleteTarget(null)}\n title=\"Delete channel?\"\n description={\n deleteTarget\n ? `\"${deleteTarget.name}\" will be removed. This cannot be undone.`\n : undefined\n }\n size=\"sm\"\n footer={\n <>\n <button\n onClick={() => setDeleteTarget(null)}\n className=\"rounded-md border border-border-subtle px-4 py-2 text-sm font-medium text-text-primary hover:bg-bg-sunken/50\"\n >\n Cancel\n </button>\n <button\n onClick={handleDelete}\n disabled={deleting}\n className=\"rounded-md bg-status-error-bg px-4 py-2 text-sm font-medium text-status-error-text hover:bg-status-error-bg/80 disabled:opacity-50\"\n >\n {deleting ? \"Deleting...\" : \"Delete\"}\n </button>\n </>\n }\n >\n <p className=\"text-sm text-text-secondary\">\n Deleting a channel stops it from feeding new time into TimeCampus.\n Previously captured entries are kept.\n </p>\n </Modal>\n\n <ToastContainer toasts={toasts} onDismiss={dismissToast} />\n </PageLayout>\n );\n}\n"],"mappings":"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"}
|