dsh-offwork 1.0.0 → 1.0.1
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/CHANGELOG.md +17 -0
- package/README.md +58 -39
- package/client.js +1001 -299
- package/client.source.js +896 -252
- package/clock.js +81 -34
- package/index.js +142 -66
- package/locale/en.json +6 -1
- package/locale/zh.json +6 -1
- package/package.json +18 -5
- package/tests/clock.test.mjs +76 -33
- package/tests/host.test.mjs +76 -27
- package/tests/ui.test.mjs +77 -20
- package/tools/build.mjs +22 -7
- package/tools/check-package.mjs +96 -0
- package/ui-utils.js +21 -10
package/client.source.js
CHANGED
|
@@ -7,34 +7,125 @@ window.__ModuleLoader__.load({
|
|
|
7
7
|
/* PURE_MODULES */
|
|
8
8
|
const messages = {
|
|
9
9
|
zh: {
|
|
10
|
-
title:'下班计时器',
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
10
|
+
title: '下班计时器',
|
|
11
|
+
working: '距离下班',
|
|
12
|
+
before: '距离开工',
|
|
13
|
+
lunch: '午休中',
|
|
14
|
+
done: '本班已完成',
|
|
15
|
+
rest: '今天休息',
|
|
16
|
+
earned: '本班已赚 · 税前',
|
|
17
|
+
daily: '日薪',
|
|
18
|
+
hourly: '有效时薪',
|
|
19
|
+
progress: '计薪进度',
|
|
20
|
+
settings: '设置',
|
|
21
|
+
collapse: '收起为小条',
|
|
22
|
+
expand: '展开',
|
|
23
|
+
hide: '隐藏工资',
|
|
24
|
+
show: '显示工资',
|
|
25
|
+
save: '保存',
|
|
26
|
+
cancel: '取消',
|
|
27
|
+
monthlySalary: '税前月薪(元)',
|
|
28
|
+
dailySalary: '税前日薪(元)',
|
|
29
|
+
workdaysPerMonth: '月计薪天数',
|
|
30
|
+
start: '上班时间',
|
|
31
|
+
end: '下班时间',
|
|
32
|
+
breakStart: '休息开始',
|
|
33
|
+
breakEnd: '休息结束',
|
|
34
|
+
timezone: '时区',
|
|
35
|
+
weekdays: '班次开始日',
|
|
36
|
+
payMode: '计薪方式',
|
|
37
|
+
monthly: '月薪',
|
|
38
|
+
dailyMode: '日薪',
|
|
39
|
+
loading: '正在读取设置…',
|
|
40
|
+
retry: '重新读取',
|
|
41
|
+
noSession: '请先打开或创建一个会话。',
|
|
42
|
+
error: '操作失败,请重试。',
|
|
43
|
+
saved: '设置已保存',
|
|
44
|
+
days: '日一二三四五六',
|
|
45
|
+
privacy: '配置保存在 Harness 所在机器;读写结果进入所选会话记录。',
|
|
46
|
+
drag: '按住可拖动;方向键微调,Home 复位',
|
|
47
|
+
setup: '先设好你的作息',
|
|
48
|
+
setupHint: '示例数值不是你的收入,保存后才开始计时。',
|
|
49
|
+
breakEnabled: '扣除无薪休息',
|
|
50
|
+
noBreak: '不扣休息',
|
|
51
|
+
nextDay: '次日',
|
|
52
|
+
preview: '折算',
|
|
53
|
+
salaryGroup: '薪资',
|
|
54
|
+
scheduleGroup: '作息',
|
|
55
|
+
appearanceGroup: '外观',
|
|
56
|
+
advanced: '高级设置',
|
|
57
|
+
sessionHint: '仅用于记录配置读写操作,不是账本,不影响工资计算。',
|
|
58
|
+
session: '操作记录会话',
|
|
59
|
+
masked: '工资已隐藏',
|
|
60
|
+
reset: '复位位置',
|
|
61
|
+
synced: '已读取最新配置',
|
|
62
|
+
refresh: '读取最新配置',
|
|
63
|
+
busy: '保存中…',
|
|
64
|
+
storage: '浏览器禁止本地存储;位置与透明度本次有效。',
|
|
65
|
+
shiftDate: '班次',
|
|
66
|
+
opacity: '背景透明度',
|
|
67
|
+
opacityHint: '只影响背景,不影响文字。',
|
|
23
68
|
},
|
|
24
69
|
en: {
|
|
25
|
-
title:'Offwork Clock',
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
70
|
+
title: 'Offwork Clock',
|
|
71
|
+
working: 'Until clock-out',
|
|
72
|
+
before: 'Until work starts',
|
|
73
|
+
lunch: 'On break',
|
|
74
|
+
done: 'Shift complete',
|
|
75
|
+
rest: 'Day off',
|
|
76
|
+
earned: 'Earned this shift · gross',
|
|
77
|
+
daily: 'Shift pay',
|
|
78
|
+
hourly: 'Paid-hour rate',
|
|
79
|
+
progress: 'Paid progress',
|
|
80
|
+
settings: 'Settings',
|
|
81
|
+
collapse: 'Minimize to strip',
|
|
82
|
+
expand: 'Expand',
|
|
83
|
+
hide: 'Hide salary',
|
|
84
|
+
show: 'Show salary',
|
|
85
|
+
save: 'Save',
|
|
86
|
+
cancel: 'Cancel',
|
|
87
|
+
monthlySalary: 'Monthly gross (CNY)',
|
|
88
|
+
dailySalary: 'Daily gross (CNY)',
|
|
89
|
+
workdaysPerMonth: 'Paid days / month',
|
|
90
|
+
start: 'Shift starts',
|
|
91
|
+
end: 'Shift ends',
|
|
92
|
+
breakStart: 'Break starts',
|
|
93
|
+
breakEnd: 'Break ends',
|
|
94
|
+
timezone: 'Time zone',
|
|
95
|
+
weekdays: 'Shift starting days',
|
|
96
|
+
payMode: 'Pay basis',
|
|
97
|
+
monthly: 'Monthly',
|
|
98
|
+
dailyMode: 'Daily',
|
|
99
|
+
loading: 'Loading settings…',
|
|
100
|
+
retry: 'Reload',
|
|
101
|
+
noSession: 'Open or create a session first.',
|
|
102
|
+
error: 'Operation failed. Please retry.',
|
|
103
|
+
saved: 'Settings saved',
|
|
104
|
+
days: 'SMTWTFS',
|
|
105
|
+
privacy: 'Settings are saved on the Harness host; results enter the selected session log.',
|
|
106
|
+
drag: 'Drag to move; arrow keys to adjust; Home resets',
|
|
107
|
+
setup: 'Set up your schedule',
|
|
108
|
+
setupHint: 'These are example values, not your income. Save to start.',
|
|
109
|
+
breakEnabled: 'Deduct unpaid break',
|
|
110
|
+
noBreak: 'no break',
|
|
111
|
+
nextDay: 'next day',
|
|
112
|
+
preview: 'Preview',
|
|
113
|
+
salaryGroup: 'Salary',
|
|
114
|
+
scheduleGroup: 'Schedule',
|
|
115
|
+
appearanceGroup: 'Appearance',
|
|
116
|
+
advanced: 'Advanced',
|
|
117
|
+
sessionHint:
|
|
118
|
+
'Logs configuration operations only; not a ledger and does not affect pay calculations.',
|
|
119
|
+
session: 'Action log session',
|
|
120
|
+
masked: 'Salary hidden',
|
|
121
|
+
reset: 'Reset position',
|
|
122
|
+
synced: 'Latest settings loaded',
|
|
123
|
+
refresh: 'Reload latest settings',
|
|
124
|
+
busy: 'Saving…',
|
|
125
|
+
storage: 'Local storage is blocked; position and opacity last only for this visit.',
|
|
126
|
+
shiftDate: 'Shift',
|
|
127
|
+
opacity: 'Surface opacity',
|
|
128
|
+
opacityHint: 'Affects the background only.',
|
|
38
129
|
},
|
|
39
130
|
};
|
|
40
131
|
const STYLE = `
|
|
@@ -124,31 +215,65 @@ window.__ModuleLoader__.load({
|
|
|
124
215
|
@media(max-width:420px){.owc-pill{min-width:136px;padding:8px 8px 7px 12px}.owc-pill-status{display:none}.owc-pill-time{font-size:22px}.owc-main,.owc-form{padding-left:14px;padding-right:14px}.owc-time{font-size:31px}}
|
|
125
216
|
`;
|
|
126
217
|
const ICONS = {
|
|
127
|
-
eye: [
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
218
|
+
eye: [
|
|
219
|
+
['path', { d: 'M1.7 8S4.3 3.9 8 3.9 14.3 8 14.3 8 11.7 12.1 8 12.1 1.7 8 1.7 8Z' }],
|
|
220
|
+
['circle', { cx: 8, cy: 8, r: 2.05 }],
|
|
221
|
+
],
|
|
222
|
+
eyeOff: [
|
|
223
|
+
['path', { d: 'M1.7 8S4.3 3.9 8 3.9 14.3 8 14.3 8 11.7 12.1 8 12.1 1.7 8 1.7 8Z' }],
|
|
224
|
+
['circle', { cx: 8, cy: 8, r: 2.05 }],
|
|
225
|
+
['path', { d: 'M3 3l10 10' }],
|
|
226
|
+
],
|
|
227
|
+
sliders: [
|
|
228
|
+
['path', { d: 'M2.2 4.6h3.4M9.2 4.6h4.6M2.2 11.4h5.4M11.4 11.4h2.4' }],
|
|
229
|
+
['circle', { cx: 7.3, cy: 4.6, r: 1.55 }],
|
|
230
|
+
['circle', { cx: 9.4, cy: 11.4, r: 1.55 }],
|
|
231
|
+
],
|
|
232
|
+
alarm: [
|
|
233
|
+
['circle', { cx: 8, cy: 9, r: 4.5 }],
|
|
234
|
+
['path', { d: 'M8 6.5V9l1.7 1M2 4l2-2M12 2l2 2M5 13l-1 2M11 13l1 2' }],
|
|
235
|
+
],
|
|
236
|
+
chevronUp: [['path', { d: 'M3.7 9.8L8 5.5l4.3 4.3' }]],
|
|
237
|
+
chevronDown: [['path', { d: 'M3.7 6.2L8 10.5l4.3-4.3' }]],
|
|
238
|
+
close: [['path', { d: 'M4.3 4.3l7.4 7.4M11.7 4.3l-7.4 7.4' }]],
|
|
239
|
+
refresh: [
|
|
240
|
+
['path', { d: 'M13.1 8a5.1 5.1 0 1 1-1.5-3.6' }],
|
|
241
|
+
['path', { d: 'M13.1 3v3.2H9.9' }],
|
|
242
|
+
],
|
|
135
243
|
};
|
|
136
|
-
const iconSvg = (name, size = 15) =>
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
244
|
+
const iconSvg = (name, size = 15) =>
|
|
245
|
+
h(
|
|
246
|
+
'svg',
|
|
247
|
+
{
|
|
248
|
+
width: size,
|
|
249
|
+
height: size,
|
|
250
|
+
viewBox: '0 0 16 16',
|
|
251
|
+
fill: 'none',
|
|
252
|
+
stroke: 'currentColor',
|
|
253
|
+
strokeWidth: 1.55,
|
|
254
|
+
strokeLinecap: 'round',
|
|
255
|
+
strokeLinejoin: 'round',
|
|
256
|
+
'aria-hidden': 'true',
|
|
257
|
+
focusable: 'false',
|
|
258
|
+
},
|
|
259
|
+
ICONS[name].map(([tag, attrs], index) => h(tag, { key: index, ...attrs })),
|
|
260
|
+
);
|
|
140
261
|
|
|
141
262
|
return {
|
|
142
263
|
inject: ['slots', 'locale', 'remote', 'remote.commands'],
|
|
143
264
|
apply(ctx) {
|
|
144
|
-
for (const [lang, dict] of Object.entries(messages))
|
|
265
|
+
for (const [lang, dict] of Object.entries(messages))
|
|
266
|
+
ctx.effect(() => ctx.locale.register('offwork-clock', lang, dict));
|
|
145
267
|
const viewKey = 'dsh.offwork-clock.view.v4';
|
|
146
268
|
const legacyKey = 'dsh.offwork-clock.view.v3';
|
|
147
269
|
|
|
148
270
|
function Clock(props) {
|
|
149
|
-
const locale = React.useSyncExternalStore(
|
|
271
|
+
const locale = React.useSyncExternalStore(
|
|
272
|
+
(fn) => ctx.locale.subscribe(fn),
|
|
273
|
+
() => ctx.locale.getSnapshot(),
|
|
274
|
+
);
|
|
150
275
|
const t = ctx.locale.bind('offwork-clock');
|
|
151
|
-
const sessions = props.useSessions(s => s);
|
|
276
|
+
const sessions = props.useSessions((s) => s);
|
|
152
277
|
const sessionIds = Array.isArray(sessions?.ids) ? sessions.ids : [];
|
|
153
278
|
const [sessionChoice, setSessionChoice] = React.useState('');
|
|
154
279
|
const sessionId = sessionIds.includes(sessionChoice) ? sessionChoice : sessionIds[0];
|
|
@@ -175,37 +300,76 @@ window.__ModuleLoader__.load({
|
|
|
175
300
|
const preview = workingDraft ? safeCalculate(workingDraft) : null;
|
|
176
301
|
const safeState = state && !state.error ? state : null;
|
|
177
302
|
const collapsed = !!safeState && !editing && view.mini && !error && !state?.error;
|
|
178
|
-
const seconds = safeState
|
|
303
|
+
const seconds = safeState
|
|
304
|
+
? safeState.status === 'before'
|
|
305
|
+
? safeState.startRemaining
|
|
306
|
+
: safeState.remaining
|
|
307
|
+
: NaN;
|
|
179
308
|
const duration = formatDuration(seconds);
|
|
180
|
-
const formatMoney = React.useMemo(
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
309
|
+
const formatMoney = React.useMemo(
|
|
310
|
+
() =>
|
|
311
|
+
new Intl.NumberFormat(locale.active, {
|
|
312
|
+
style: 'currency',
|
|
313
|
+
currency: 'CNY',
|
|
314
|
+
minimumFractionDigits: 2,
|
|
315
|
+
maximumFractionDigits: 2,
|
|
316
|
+
}),
|
|
317
|
+
[locale.active],
|
|
318
|
+
);
|
|
319
|
+
const money = (value) => (view.masked ? '••••' : formatMoney.format(value));
|
|
184
320
|
const moneyText = safeState ? money(safeState.earned) : '—';
|
|
185
|
-
const cardStyle = Object.assign(
|
|
186
|
-
|
|
321
|
+
const cardStyle = Object.assign(
|
|
322
|
+
{ '--owc-alpha': String(view.opacity) },
|
|
323
|
+
view.position ? { left: view.position.x, top: view.position.y, right: 'auto' } : {},
|
|
324
|
+
);
|
|
187
325
|
|
|
188
326
|
function safeCalculate(value) {
|
|
189
|
-
try {
|
|
190
|
-
|
|
327
|
+
try {
|
|
328
|
+
return calculate(value, now);
|
|
329
|
+
} catch (e) {
|
|
330
|
+
return { error: e instanceof Error ? e.message : t('error') };
|
|
331
|
+
}
|
|
332
|
+
}
|
|
333
|
+
function errorText(e) {
|
|
334
|
+
return e instanceof Error ? e.message : t('error');
|
|
335
|
+
}
|
|
336
|
+
function setPreference(patch) {
|
|
337
|
+
setView((current) => ({ ...current, ...patch }));
|
|
191
338
|
}
|
|
192
|
-
function errorText(e) { return e instanceof Error ? e.message : t('error'); }
|
|
193
|
-
function setPreference(patch) { setView(current => ({ ...current, ...patch })); }
|
|
194
339
|
|
|
195
|
-
const clamp = React.useCallback(
|
|
196
|
-
|
|
197
|
-
|
|
340
|
+
const clamp = React.useCallback(
|
|
341
|
+
(position) =>
|
|
342
|
+
clampPosition(
|
|
343
|
+
position,
|
|
344
|
+
{ width: window.innerWidth, height: window.innerHeight },
|
|
345
|
+
{
|
|
346
|
+
width: box.current?.offsetWidth || 300,
|
|
347
|
+
height: box.current?.offsetHeight || 200,
|
|
348
|
+
},
|
|
349
|
+
),
|
|
350
|
+
[],
|
|
351
|
+
);
|
|
198
352
|
|
|
199
353
|
React.useEffect(() => {
|
|
200
354
|
try {
|
|
201
355
|
const saved = localStorage.getItem(viewKey);
|
|
202
356
|
const legacy = !saved ? JSON.parse(localStorage.getItem(legacyKey) || 'null') : null;
|
|
203
|
-
setView(
|
|
204
|
-
|
|
357
|
+
setView(
|
|
358
|
+
normalizeView(
|
|
359
|
+
saved
|
|
360
|
+
? JSON.parse(saved)
|
|
361
|
+
: { position: legacy?.position ?? null, masked: legacy?.masked ?? true },
|
|
362
|
+
),
|
|
363
|
+
);
|
|
364
|
+
} catch {
|
|
365
|
+
setStorageError(true);
|
|
366
|
+
}
|
|
205
367
|
setReady(true);
|
|
206
|
-
const sync = e => {
|
|
368
|
+
const sync = (e) => {
|
|
207
369
|
if (e.key !== viewKey) return;
|
|
208
|
-
try {
|
|
370
|
+
try {
|
|
371
|
+
setView(normalizeView(JSON.parse(e.newValue || 'null')));
|
|
372
|
+
} catch {}
|
|
209
373
|
};
|
|
210
374
|
window.addEventListener('storage', sync);
|
|
211
375
|
return () => window.removeEventListener('storage', sync);
|
|
@@ -213,72 +377,122 @@ window.__ModuleLoader__.load({
|
|
|
213
377
|
|
|
214
378
|
React.useEffect(() => {
|
|
215
379
|
if (!ready) return;
|
|
216
|
-
try {
|
|
217
|
-
|
|
380
|
+
try {
|
|
381
|
+
localStorage.setItem(viewKey, JSON.stringify(normalizeView(view)));
|
|
382
|
+
} catch {
|
|
383
|
+
setStorageError(true);
|
|
384
|
+
}
|
|
218
385
|
}, [ready, view]);
|
|
219
386
|
|
|
220
387
|
React.useEffect(() => {
|
|
221
388
|
if (sessionId) reload();
|
|
222
389
|
else setError(t('noSession'));
|
|
223
|
-
return () => {
|
|
390
|
+
return () => {
|
|
391
|
+
generation.current++;
|
|
392
|
+
};
|
|
224
393
|
}, [sessionId]);
|
|
225
394
|
|
|
226
395
|
React.useEffect(() => {
|
|
227
396
|
let timer;
|
|
228
|
-
const tick = () => {
|
|
229
|
-
|
|
397
|
+
const tick = () => {
|
|
398
|
+
setNow(Date.now());
|
|
399
|
+
timer = setTimeout(tick, 1000 - (Date.now() % 1000));
|
|
400
|
+
};
|
|
401
|
+
const visibility = () => {
|
|
402
|
+
clearTimeout(timer);
|
|
403
|
+
setNow(Date.now());
|
|
404
|
+
if (!document.hidden) tick();
|
|
405
|
+
};
|
|
230
406
|
if (!document.hidden) tick();
|
|
231
407
|
document.addEventListener('visibilitychange', visibility);
|
|
232
|
-
return () => {
|
|
408
|
+
return () => {
|
|
409
|
+
clearTimeout(timer);
|
|
410
|
+
document.removeEventListener('visibilitychange', visibility);
|
|
411
|
+
};
|
|
233
412
|
}, []);
|
|
234
413
|
|
|
235
414
|
React.useEffect(() => {
|
|
236
|
-
const resize = () =>
|
|
415
|
+
const resize = () =>
|
|
416
|
+
setView((current) =>
|
|
417
|
+
current.position ? { ...current, position: clamp(current.position) } : current,
|
|
418
|
+
);
|
|
237
419
|
const ro = typeof ResizeObserver === 'function' ? new ResizeObserver(resize) : null;
|
|
238
420
|
if (box.current) ro?.observe(box.current);
|
|
239
421
|
window.addEventListener('resize', resize);
|
|
240
|
-
return () => {
|
|
422
|
+
return () => {
|
|
423
|
+
ro?.disconnect();
|
|
424
|
+
window.removeEventListener('resize', resize);
|
|
425
|
+
};
|
|
241
426
|
}, [clamp]);
|
|
242
427
|
|
|
243
428
|
React.useEffect(() => {
|
|
244
|
-
if (!editing) {
|
|
429
|
+
if (!editing) {
|
|
430
|
+
focusReturn.current?.focus?.();
|
|
431
|
+
return;
|
|
432
|
+
}
|
|
245
433
|
box.current?.querySelector('form input,form select')?.focus?.();
|
|
246
434
|
}, [editing]);
|
|
247
435
|
|
|
248
|
-
React.useEffect(
|
|
436
|
+
React.useEffect(
|
|
437
|
+
() => () => {
|
|
438
|
+
generation.current++;
|
|
439
|
+
dragState.current = null;
|
|
440
|
+
},
|
|
441
|
+
[],
|
|
442
|
+
);
|
|
249
443
|
|
|
250
444
|
async function request(action, patch) {
|
|
251
445
|
if (!sessionId) throw new Error(t('noSession'));
|
|
252
|
-
const rpc = await ctx.remote.commands.execute(
|
|
446
|
+
const rpc = await ctx.remote.commands.execute(
|
|
447
|
+
sessionId,
|
|
448
|
+
'/offwork-clock ' + action + (patch ? ' ' + JSON.stringify(patch) : ''),
|
|
449
|
+
[],
|
|
450
|
+
);
|
|
253
451
|
return decodeCommand(rpc);
|
|
254
452
|
}
|
|
255
453
|
|
|
256
454
|
async function reload(explicit = false) {
|
|
257
455
|
if (requestBusy.current) return;
|
|
258
456
|
const token = ++generation.current;
|
|
259
|
-
setBusy(true);
|
|
457
|
+
setBusy(true);
|
|
458
|
+
setError('');
|
|
260
459
|
try {
|
|
261
460
|
const next = await request('get');
|
|
262
461
|
if (token !== generation.current) return;
|
|
263
462
|
setData(next);
|
|
264
|
-
if (!next.configured) {
|
|
463
|
+
if (!next.configured) {
|
|
464
|
+
setDraft({ ...next.settings });
|
|
465
|
+
setEditing(true);
|
|
466
|
+
}
|
|
265
467
|
if (explicit) setNotice(t('synced'));
|
|
266
|
-
} catch (e) {
|
|
267
|
-
|
|
468
|
+
} catch (e) {
|
|
469
|
+
if (token === generation.current) setError(errorText(e));
|
|
470
|
+
} finally {
|
|
471
|
+
if (token === generation.current) setBusy(false);
|
|
472
|
+
}
|
|
268
473
|
}
|
|
269
474
|
|
|
270
475
|
async function save(event) {
|
|
271
476
|
event.preventDefault();
|
|
272
477
|
if (requestBusy.current) return;
|
|
273
|
-
requestBusy.current = true;
|
|
478
|
+
requestBusy.current = true;
|
|
479
|
+
setBusy(true);
|
|
480
|
+
setError('');
|
|
274
481
|
const token = ++generation.current;
|
|
275
482
|
try {
|
|
276
483
|
const next = await request('set', changedSettings(settings, draft));
|
|
277
484
|
if (token === generation.current) {
|
|
278
|
-
setData(next);
|
|
485
|
+
setData(next);
|
|
486
|
+
setEditing(false);
|
|
487
|
+
setDraft(null);
|
|
488
|
+
setNotice(t('saved'));
|
|
279
489
|
}
|
|
280
|
-
} catch (e) {
|
|
281
|
-
|
|
490
|
+
} catch (e) {
|
|
491
|
+
if (token === generation.current) setError(errorText(e));
|
|
492
|
+
} finally {
|
|
493
|
+
requestBusy.current = false;
|
|
494
|
+
if (token === generation.current) setBusy(false);
|
|
495
|
+
}
|
|
282
496
|
}
|
|
283
497
|
|
|
284
498
|
function commitPosition(position) {
|
|
@@ -288,10 +502,21 @@ window.__ModuleLoader__.load({
|
|
|
288
502
|
}
|
|
289
503
|
function dragDown(e) {
|
|
290
504
|
if (e.button !== 0) return;
|
|
291
|
-
if (
|
|
505
|
+
if (
|
|
506
|
+
e.target.closest?.('button,input,select,label,a,textarea,summary,[contenteditable]')
|
|
507
|
+
)
|
|
508
|
+
return;
|
|
292
509
|
const rect = box.current.getBoundingClientRect();
|
|
293
|
-
dragState.current = {
|
|
294
|
-
|
|
510
|
+
dragState.current = {
|
|
511
|
+
dx: e.clientX - rect.left,
|
|
512
|
+
dy: e.clientY - rect.top,
|
|
513
|
+
x0: e.clientX,
|
|
514
|
+
y0: e.clientY,
|
|
515
|
+
moved: false,
|
|
516
|
+
};
|
|
517
|
+
try {
|
|
518
|
+
e.currentTarget.setPointerCapture?.(e.pointerId);
|
|
519
|
+
} catch {}
|
|
295
520
|
}
|
|
296
521
|
function dragMove(e) {
|
|
297
522
|
const d = dragState.current;
|
|
@@ -311,10 +536,17 @@ window.__ModuleLoader__.load({
|
|
|
311
536
|
if (!d) return;
|
|
312
537
|
dragState.current = null;
|
|
313
538
|
if (d.moved && lastPosition.current) commitPosition(lastPosition.current);
|
|
314
|
-
try {
|
|
539
|
+
try {
|
|
540
|
+
if (e.currentTarget.hasPointerCapture?.(e.pointerId))
|
|
541
|
+
e.currentTarget.releasePointerCapture(e.pointerId);
|
|
542
|
+
} catch {}
|
|
315
543
|
}
|
|
316
544
|
function gripKeys(e) {
|
|
317
|
-
if (e.key === 'Home') {
|
|
545
|
+
if (e.key === 'Home') {
|
|
546
|
+
e.preventDefault();
|
|
547
|
+
setPreference({ position: null });
|
|
548
|
+
return;
|
|
549
|
+
}
|
|
318
550
|
if (!['ArrowLeft', 'ArrowRight', 'ArrowUp', 'ArrowDown'].includes(e.key)) return;
|
|
319
551
|
e.preventDefault();
|
|
320
552
|
const rect = box.current.getBoundingClientRect();
|
|
@@ -328,114 +560,324 @@ window.__ModuleLoader__.load({
|
|
|
328
560
|
focusReturn.current = e.currentTarget;
|
|
329
561
|
setDraft({ ...settings });
|
|
330
562
|
setEditing(true);
|
|
331
|
-
setError('');
|
|
563
|
+
setError('');
|
|
564
|
+
setNotice('');
|
|
332
565
|
}
|
|
333
566
|
function closeEditor() {
|
|
334
567
|
if (!data?.configured) return;
|
|
335
|
-
setEditing(false);
|
|
568
|
+
setEditing(false);
|
|
569
|
+
setDraft(null);
|
|
570
|
+
setError('');
|
|
571
|
+
}
|
|
572
|
+
function update(key, value) {
|
|
573
|
+
setDraft((current) => ({ ...current, [key]: value }));
|
|
336
574
|
}
|
|
337
|
-
function update(key, value) { setDraft(current => ({ ...current, [key]: value })); }
|
|
338
575
|
function toggleBreak(e) {
|
|
339
|
-
const point = text => +text.slice(0, 2) * 60 + +text.slice(3);
|
|
340
|
-
const label =
|
|
341
|
-
|
|
342
|
-
|
|
576
|
+
const point = (text) => +text.slice(0, 2) * 60 + +text.slice(3);
|
|
577
|
+
const label = (minutes) =>
|
|
578
|
+
String(Math.floor(minutes / 60)).padStart(2, '0') +
|
|
579
|
+
':' +
|
|
580
|
+
String(minutes % 60).padStart(2, '0');
|
|
581
|
+
setDraft((current) => {
|
|
582
|
+
if (!e.target.checked)
|
|
583
|
+
return { ...current, breakStart: current.start, breakEnd: current.start };
|
|
343
584
|
const start = point(current.start);
|
|
344
585
|
const span = (point(current.end) - start + 1440) % 1440;
|
|
345
|
-
return {
|
|
586
|
+
return {
|
|
587
|
+
...current,
|
|
588
|
+
breakStart: current.start,
|
|
589
|
+
breakEnd: label((start + Math.max(1, Math.min(60, Math.floor(span / 2)))) % 1440),
|
|
590
|
+
};
|
|
346
591
|
});
|
|
347
592
|
}
|
|
348
593
|
|
|
349
|
-
const label = (text, control) =>
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
|
|
594
|
+
const label = (text, control) =>
|
|
595
|
+
h('label', { className: 'owc-field' }, h('span', null, text), control);
|
|
596
|
+
const numberInput = (key) =>
|
|
597
|
+
h('input', {
|
|
598
|
+
name: key,
|
|
599
|
+
type: 'number',
|
|
600
|
+
step: 'any',
|
|
601
|
+
min: key === 'workdaysPerMonth' ? '0.01' : '0',
|
|
602
|
+
value: workingDraft?.[key] ?? '',
|
|
603
|
+
disabled: busy,
|
|
604
|
+
onChange: (e) => update(key, e.target.value === '' ? '' : e.target.valueAsNumber),
|
|
605
|
+
});
|
|
606
|
+
const timeInput = (key) =>
|
|
607
|
+
h('input', {
|
|
608
|
+
name: key,
|
|
609
|
+
type: 'time',
|
|
610
|
+
required: true,
|
|
611
|
+
value: workingDraft?.[key] ?? '',
|
|
612
|
+
disabled: busy,
|
|
613
|
+
onChange: (e) => update(key, e.target.value),
|
|
614
|
+
});
|
|
615
|
+
const sessionSelect = () =>
|
|
616
|
+
h(
|
|
617
|
+
'select',
|
|
618
|
+
{
|
|
619
|
+
value: sessionId || '',
|
|
620
|
+
disabled: busy,
|
|
621
|
+
onChange: (e) => setSessionChoice(e.target.value),
|
|
622
|
+
},
|
|
623
|
+
sessionIds.length
|
|
624
|
+
? sessionIds.map((id) =>
|
|
625
|
+
h(
|
|
626
|
+
'option',
|
|
627
|
+
{ key: id, value: id },
|
|
628
|
+
sessions?.byId?.[id]?.title || String(id).slice(0, 12),
|
|
629
|
+
),
|
|
630
|
+
)
|
|
631
|
+
: h('option', { value: '' }, t('noSession')),
|
|
632
|
+
);
|
|
633
|
+
const salaryToggle = () =>
|
|
634
|
+
h(
|
|
635
|
+
'button',
|
|
636
|
+
{
|
|
637
|
+
type: 'button',
|
|
638
|
+
className: 'owc-text',
|
|
639
|
+
'aria-pressed': view.masked,
|
|
640
|
+
title: t(view.masked ? 'show' : 'hide'),
|
|
641
|
+
onClick: () => setPreference({ masked: !view.masked }),
|
|
642
|
+
},
|
|
643
|
+
iconSvg(view.masked ? 'eyeOff' : 'eye', 13),
|
|
644
|
+
t(view.masked ? 'show' : 'hide'),
|
|
645
|
+
);
|
|
370
646
|
|
|
371
|
-
const strip = () =>
|
|
372
|
-
h(
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
|
|
387
|
-
|
|
388
|
-
|
|
389
|
-
|
|
647
|
+
const strip = () =>
|
|
648
|
+
h(
|
|
649
|
+
'div',
|
|
650
|
+
{ className: 'owc-pill' },
|
|
651
|
+
h(
|
|
652
|
+
'div',
|
|
653
|
+
{ className: 'owc-pill-top' },
|
|
654
|
+
iconSvg('alarm', 16),
|
|
655
|
+
h(
|
|
656
|
+
'span',
|
|
657
|
+
{ className: 'owc-grip', tabIndex: 0, title: t('drag'), onKeyDown: gripKeys },
|
|
658
|
+
safeState.status !== 'done' &&
|
|
659
|
+
h('span', { className: 'owc-pill-status' }, t(safeState.status)),
|
|
660
|
+
h('span', { className: 'owc-pill-time' }, duration),
|
|
661
|
+
),
|
|
662
|
+
h(
|
|
663
|
+
'button',
|
|
664
|
+
{
|
|
665
|
+
type: 'button',
|
|
666
|
+
className: 'owc-icon',
|
|
667
|
+
style: { marginLeft: 'auto' },
|
|
668
|
+
title: t('expand'),
|
|
669
|
+
'aria-label': t('expand'),
|
|
670
|
+
onClick: () => setPreference({ mini: false }),
|
|
671
|
+
},
|
|
672
|
+
iconSvg('chevronUp', 14),
|
|
673
|
+
),
|
|
674
|
+
),
|
|
675
|
+
h(
|
|
676
|
+
'div',
|
|
677
|
+
{ className: 'owc-pill-bottom' },
|
|
678
|
+
h(
|
|
679
|
+
'span',
|
|
680
|
+
{
|
|
681
|
+
className: 'owc-pill-money',
|
|
682
|
+
'aria-label': view.masked ? t('masked') : moneyText,
|
|
683
|
+
},
|
|
684
|
+
moneyText,
|
|
685
|
+
),
|
|
686
|
+
h(
|
|
687
|
+
'div',
|
|
688
|
+
{ className: 'owc-pill-actions' },
|
|
689
|
+
h(
|
|
690
|
+
'button',
|
|
691
|
+
{
|
|
692
|
+
type: 'button',
|
|
693
|
+
className: 'owc-icon',
|
|
694
|
+
title: t(view.masked ? 'show' : 'hide'),
|
|
695
|
+
'aria-label': t(view.masked ? 'show' : 'hide'),
|
|
696
|
+
onClick: () => setPreference({ masked: !view.masked }),
|
|
697
|
+
},
|
|
698
|
+
iconSvg(view.masked ? 'eyeOff' : 'eye', 14),
|
|
699
|
+
),
|
|
700
|
+
),
|
|
701
|
+
),
|
|
702
|
+
);
|
|
390
703
|
|
|
391
|
-
const panelHead = () =>
|
|
392
|
-
h(
|
|
393
|
-
|
|
394
|
-
|
|
395
|
-
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
|
|
399
|
-
|
|
400
|
-
|
|
401
|
-
|
|
402
|
-
|
|
403
|
-
|
|
404
|
-
|
|
405
|
-
|
|
406
|
-
|
|
407
|
-
|
|
408
|
-
|
|
704
|
+
const panelHead = () =>
|
|
705
|
+
h(
|
|
706
|
+
'div',
|
|
707
|
+
{ className: 'owc-head' },
|
|
708
|
+
h(
|
|
709
|
+
'span',
|
|
710
|
+
{
|
|
711
|
+
className: 'owc-title',
|
|
712
|
+
tabIndex: 0,
|
|
713
|
+
title: t('drag'),
|
|
714
|
+
onKeyDown: gripKeys,
|
|
715
|
+
},
|
|
716
|
+
editing ? t(data?.configured ? 'settings' : 'setup') : t('title'),
|
|
717
|
+
),
|
|
718
|
+
editing
|
|
719
|
+
? data?.configured &&
|
|
720
|
+
h(
|
|
721
|
+
'button',
|
|
722
|
+
{
|
|
723
|
+
type: 'button',
|
|
724
|
+
className: 'owc-icon',
|
|
725
|
+
title: t('cancel'),
|
|
726
|
+
'aria-label': t('cancel'),
|
|
727
|
+
onClick: closeEditor,
|
|
728
|
+
disabled: busy,
|
|
729
|
+
},
|
|
730
|
+
iconSvg('close'),
|
|
731
|
+
)
|
|
732
|
+
: h(
|
|
733
|
+
React.Fragment,
|
|
734
|
+
null,
|
|
735
|
+
h(
|
|
736
|
+
'button',
|
|
737
|
+
{
|
|
738
|
+
type: 'button',
|
|
739
|
+
className: 'owc-icon',
|
|
740
|
+
title: t('settings'),
|
|
741
|
+
'aria-label': t('settings'),
|
|
742
|
+
onClick: openEditor,
|
|
743
|
+
disabled: busy,
|
|
744
|
+
},
|
|
745
|
+
iconSvg('sliders'),
|
|
746
|
+
),
|
|
747
|
+
h(
|
|
748
|
+
'button',
|
|
749
|
+
{
|
|
750
|
+
type: 'button',
|
|
751
|
+
className: 'owc-icon',
|
|
752
|
+
title: t('collapse'),
|
|
753
|
+
'aria-label': t('collapse'),
|
|
754
|
+
onClick: () => setPreference({ mini: true }),
|
|
755
|
+
},
|
|
756
|
+
iconSvg('chevronDown'),
|
|
757
|
+
),
|
|
758
|
+
),
|
|
759
|
+
);
|
|
409
760
|
|
|
410
761
|
const panelBody = () => {
|
|
411
|
-
if (!safeState)
|
|
412
|
-
|
|
413
|
-
|
|
414
|
-
|
|
415
|
-
h(
|
|
416
|
-
|
|
762
|
+
if (!safeState)
|
|
763
|
+
return h(
|
|
764
|
+
'div',
|
|
765
|
+
{ className: 'owc-main' },
|
|
766
|
+
h(
|
|
767
|
+
'div',
|
|
768
|
+
{ className: 'owc-status' },
|
|
769
|
+
h('span', { className: 'owc-dot' }),
|
|
770
|
+
t('loading'),
|
|
771
|
+
),
|
|
772
|
+
!sessionId && h('p', { className: 'owc-hint' }, t('noSession')),
|
|
773
|
+
h(
|
|
774
|
+
'div',
|
|
775
|
+
{ className: 'owc-row-actions' },
|
|
776
|
+
h(
|
|
777
|
+
'button',
|
|
778
|
+
{
|
|
779
|
+
type: 'button',
|
|
780
|
+
className: 'owc-text',
|
|
781
|
+
onClick: () => reload(true),
|
|
782
|
+
disabled: busy || !sessionId,
|
|
783
|
+
},
|
|
784
|
+
iconSvg('refresh', 12),
|
|
785
|
+
t('retry'),
|
|
786
|
+
),
|
|
787
|
+
),
|
|
788
|
+
);
|
|
417
789
|
const status = safeState.status === 'done' ? '' : t(safeState.status);
|
|
418
|
-
return h(
|
|
419
|
-
|
|
790
|
+
return h(
|
|
791
|
+
'div',
|
|
792
|
+
{ className: 'owc-main' },
|
|
793
|
+
status &&
|
|
794
|
+
h('div', { className: 'owc-status' }, h('span', { className: 'owc-dot' }), status),
|
|
420
795
|
h('div', { className: 'owc-time', 'aria-label': status + ' ' + duration }, duration),
|
|
421
|
-
h(
|
|
422
|
-
|
|
423
|
-
|
|
424
|
-
|
|
425
|
-
|
|
426
|
-
|
|
427
|
-
|
|
428
|
-
|
|
429
|
-
|
|
430
|
-
|
|
431
|
-
|
|
432
|
-
|
|
433
|
-
|
|
434
|
-
|
|
435
|
-
|
|
436
|
-
h('
|
|
437
|
-
|
|
438
|
-
|
|
796
|
+
h(
|
|
797
|
+
'div',
|
|
798
|
+
{
|
|
799
|
+
className: 'owc-progress',
|
|
800
|
+
role: 'progressbar',
|
|
801
|
+
'aria-label': t('progress'),
|
|
802
|
+
'aria-valuenow': Math.floor(safeState.progress * 100),
|
|
803
|
+
'aria-valuemin': 0,
|
|
804
|
+
'aria-valuemax': 100,
|
|
805
|
+
},
|
|
806
|
+
h('div', { style: { width: safeState.progress * 100 + '%' } }),
|
|
807
|
+
),
|
|
808
|
+
h(
|
|
809
|
+
'div',
|
|
810
|
+
{ className: 'owc-earn-row' },
|
|
811
|
+
h('span', { className: 'owc-muted' }, t('earned')),
|
|
812
|
+
salaryToggle(),
|
|
813
|
+
),
|
|
814
|
+
h(
|
|
815
|
+
'div',
|
|
816
|
+
{ className: 'owc-money', 'aria-label': view.masked ? t('masked') : moneyText },
|
|
817
|
+
moneyText,
|
|
818
|
+
),
|
|
819
|
+
h(
|
|
820
|
+
'div',
|
|
821
|
+
{ className: 'owc-stats' },
|
|
822
|
+
h(
|
|
823
|
+
'div',
|
|
824
|
+
null,
|
|
825
|
+
h('span', null, t('daily')),
|
|
826
|
+
h('strong', null, money(safeState.daily)),
|
|
827
|
+
),
|
|
828
|
+
h(
|
|
829
|
+
'div',
|
|
830
|
+
null,
|
|
831
|
+
h('span', null, t('hourly')),
|
|
832
|
+
h('strong', null, money(safeState.hourly)),
|
|
833
|
+
),
|
|
834
|
+
),
|
|
835
|
+
h(
|
|
836
|
+
'div',
|
|
837
|
+
{ className: 'owc-meta' },
|
|
838
|
+
h(
|
|
839
|
+
'span',
|
|
840
|
+
null,
|
|
841
|
+
settings.start +
|
|
842
|
+
' — ' +
|
|
843
|
+
settings.end +
|
|
844
|
+
(settings.end < settings.start ? ' +1' : '') +
|
|
845
|
+
' · ' +
|
|
846
|
+
(settings.breakStart === settings.breakEnd
|
|
847
|
+
? t('noBreak')
|
|
848
|
+
: settings.breakStart + '–' + settings.breakEnd),
|
|
849
|
+
),
|
|
850
|
+
h(
|
|
851
|
+
'span',
|
|
852
|
+
null,
|
|
853
|
+
t('shiftDate') + ' ' + safeState.shiftDate + ' · ' + settings.timezone,
|
|
854
|
+
),
|
|
855
|
+
),
|
|
856
|
+
h(
|
|
857
|
+
'div',
|
|
858
|
+
{ className: 'owc-row-actions' },
|
|
859
|
+
h(
|
|
860
|
+
'button',
|
|
861
|
+
{
|
|
862
|
+
type: 'button',
|
|
863
|
+
className: 'owc-text',
|
|
864
|
+
onClick: () => reload(true),
|
|
865
|
+
disabled: busy,
|
|
866
|
+
},
|
|
867
|
+
iconSvg('refresh', 12),
|
|
868
|
+
t('refresh'),
|
|
869
|
+
),
|
|
870
|
+
h(
|
|
871
|
+
'button',
|
|
872
|
+
{
|
|
873
|
+
type: 'button',
|
|
874
|
+
className: 'owc-text',
|
|
875
|
+
onClick: () => setPreference({ position: null }),
|
|
876
|
+
},
|
|
877
|
+
t('reset'),
|
|
878
|
+
),
|
|
879
|
+
),
|
|
880
|
+
);
|
|
439
881
|
};
|
|
440
882
|
|
|
441
883
|
const editor = () => {
|
|
@@ -443,91 +885,293 @@ window.__ModuleLoader__.load({
|
|
|
443
885
|
const payDaily = workingDraft.payMode === 'daily';
|
|
444
886
|
const salaryKey = payDaily ? 'dailySalary' : 'monthlySalary';
|
|
445
887
|
const salaryControl = view.masked
|
|
446
|
-
? h(
|
|
447
|
-
|
|
888
|
+
? h(
|
|
889
|
+
'div',
|
|
890
|
+
{ className: 'owc-private' },
|
|
891
|
+
h('span', null, '••••'),
|
|
892
|
+
h(
|
|
893
|
+
'button',
|
|
894
|
+
{
|
|
895
|
+
type: 'button',
|
|
896
|
+
className: 'owc-text',
|
|
897
|
+
'aria-label': t('show'),
|
|
898
|
+
onClick: () => setPreference({ masked: false }),
|
|
899
|
+
},
|
|
900
|
+
t('show'),
|
|
901
|
+
),
|
|
902
|
+
)
|
|
448
903
|
: numberInput(salaryKey);
|
|
449
904
|
const breakOn = workingDraft.breakStart !== workingDraft.breakEnd;
|
|
450
|
-
const commonZones = [
|
|
905
|
+
const commonZones = [
|
|
906
|
+
'Asia/Shanghai',
|
|
907
|
+
'Asia/Hong_Kong',
|
|
908
|
+
'Asia/Taipei',
|
|
909
|
+
'Asia/Singapore',
|
|
910
|
+
'Asia/Tokyo',
|
|
911
|
+
'Asia/Seoul',
|
|
912
|
+
'Asia/Kolkata',
|
|
913
|
+
'Europe/London',
|
|
914
|
+
'Europe/Paris',
|
|
915
|
+
'America/New_York',
|
|
916
|
+
'America/Los_Angeles',
|
|
917
|
+
'Australia/Sydney',
|
|
918
|
+
'UTC',
|
|
919
|
+
];
|
|
451
920
|
let allZones = [];
|
|
452
|
-
try {
|
|
453
|
-
|
|
454
|
-
|
|
455
|
-
|
|
456
|
-
|
|
457
|
-
|
|
458
|
-
|
|
459
|
-
|
|
460
|
-
|
|
461
|
-
|
|
462
|
-
|
|
463
|
-
|
|
464
|
-
|
|
465
|
-
|
|
466
|
-
|
|
467
|
-
|
|
468
|
-
|
|
469
|
-
|
|
470
|
-
h('
|
|
471
|
-
|
|
472
|
-
|
|
473
|
-
|
|
474
|
-
|
|
475
|
-
|
|
476
|
-
|
|
477
|
-
|
|
478
|
-
|
|
479
|
-
|
|
480
|
-
|
|
481
|
-
|
|
482
|
-
|
|
483
|
-
|
|
484
|
-
|
|
485
|
-
|
|
486
|
-
|
|
487
|
-
|
|
488
|
-
|
|
489
|
-
|
|
490
|
-
|
|
491
|
-
|
|
492
|
-
|
|
493
|
-
|
|
494
|
-
|
|
495
|
-
|
|
496
|
-
|
|
497
|
-
|
|
498
|
-
|
|
499
|
-
|
|
500
|
-
|
|
501
|
-
|
|
921
|
+
try {
|
|
922
|
+
allZones = Intl.supportedValuesOf('timeZone');
|
|
923
|
+
} catch {}
|
|
924
|
+
const zones = [
|
|
925
|
+
...new Set([workingDraft.timezone, ...commonZones, ...allZones].filter(Boolean)),
|
|
926
|
+
];
|
|
927
|
+
return h(
|
|
928
|
+
'form',
|
|
929
|
+
{
|
|
930
|
+
className: 'owc-form',
|
|
931
|
+
onSubmit: save,
|
|
932
|
+
onKeyDown: (e) => {
|
|
933
|
+
if (e.key === 'Escape') {
|
|
934
|
+
e.preventDefault();
|
|
935
|
+
closeEditor();
|
|
936
|
+
}
|
|
937
|
+
},
|
|
938
|
+
},
|
|
939
|
+
!data?.configured && h('p', { className: 'owc-hint' }, t('setupHint')),
|
|
940
|
+
h(
|
|
941
|
+
'fieldset',
|
|
942
|
+
{ className: 'owc-setting-group' },
|
|
943
|
+
h('legend', null, t('salaryGroup')),
|
|
944
|
+
label(
|
|
945
|
+
t('payMode'),
|
|
946
|
+
h(
|
|
947
|
+
'div',
|
|
948
|
+
{ className: 'owc-seg' },
|
|
949
|
+
h(
|
|
950
|
+
'button',
|
|
951
|
+
{
|
|
952
|
+
type: 'button',
|
|
953
|
+
disabled: busy,
|
|
954
|
+
'aria-pressed': !payDaily,
|
|
955
|
+
'aria-label': t('monthly'),
|
|
956
|
+
onClick: () => update('payMode', 'monthly'),
|
|
957
|
+
},
|
|
958
|
+
t('monthly'),
|
|
959
|
+
),
|
|
960
|
+
h(
|
|
961
|
+
'button',
|
|
962
|
+
{
|
|
963
|
+
type: 'button',
|
|
964
|
+
disabled: busy,
|
|
965
|
+
'aria-pressed': payDaily,
|
|
966
|
+
'aria-label': t('dailyMode'),
|
|
967
|
+
onClick: () => update('payMode', 'daily'),
|
|
968
|
+
},
|
|
969
|
+
t('dailyMode'),
|
|
970
|
+
),
|
|
971
|
+
),
|
|
972
|
+
),
|
|
973
|
+
h(
|
|
974
|
+
'div',
|
|
975
|
+
{ className: 'owc-row2' },
|
|
976
|
+
label(t(salaryKey), salaryControl),
|
|
977
|
+
!payDaily && label(t('workdaysPerMonth'), numberInput('workdaysPerMonth')),
|
|
978
|
+
),
|
|
979
|
+
),
|
|
980
|
+
h(
|
|
981
|
+
'fieldset',
|
|
982
|
+
{ className: 'owc-setting-group' },
|
|
983
|
+
h('legend', null, t('scheduleGroup')),
|
|
984
|
+
h(
|
|
985
|
+
'div',
|
|
986
|
+
{ className: 'owc-row2' },
|
|
987
|
+
label(t('start'), timeInput('start')),
|
|
988
|
+
label(t('end'), timeInput('end')),
|
|
989
|
+
),
|
|
990
|
+
workingDraft.end < workingDraft.start &&
|
|
991
|
+
h('p', { className: 'owc-hint' }, t('end') + ' · ' + t('nextDay')),
|
|
992
|
+
h(
|
|
993
|
+
'label',
|
|
994
|
+
{ className: 'owc-check' },
|
|
995
|
+
h('input', {
|
|
996
|
+
type: 'checkbox',
|
|
997
|
+
checked: breakOn,
|
|
998
|
+
disabled: busy,
|
|
999
|
+
onChange: toggleBreak,
|
|
1000
|
+
}),
|
|
1001
|
+
t('breakEnabled'),
|
|
1002
|
+
),
|
|
1003
|
+
breakOn &&
|
|
1004
|
+
h(
|
|
1005
|
+
'div',
|
|
1006
|
+
{ className: 'owc-row2' },
|
|
1007
|
+
label(t('breakStart'), timeInput('breakStart')),
|
|
1008
|
+
label(t('breakEnd'), timeInput('breakEnd')),
|
|
1009
|
+
),
|
|
1010
|
+
label(
|
|
1011
|
+
t('weekdays'),
|
|
1012
|
+
h(
|
|
1013
|
+
'div',
|
|
1014
|
+
{ className: 'owc-days' },
|
|
1015
|
+
[0, 1, 2, 3, 4, 5, 6].map((day) =>
|
|
1016
|
+
h(
|
|
1017
|
+
'button',
|
|
1018
|
+
{
|
|
1019
|
+
key: day,
|
|
1020
|
+
type: 'button',
|
|
1021
|
+
disabled: busy,
|
|
1022
|
+
'aria-pressed': workingDraft.weekdays.includes(day),
|
|
1023
|
+
'aria-label': t('weekdays') + ' ' + t('days')[day],
|
|
1024
|
+
onClick: () =>
|
|
1025
|
+
update(
|
|
1026
|
+
'weekdays',
|
|
1027
|
+
workingDraft.weekdays.includes(day)
|
|
1028
|
+
? workingDraft.weekdays.filter((d) => d !== day)
|
|
1029
|
+
: [...workingDraft.weekdays, day].sort((a, b) => a - b),
|
|
1030
|
+
),
|
|
1031
|
+
},
|
|
1032
|
+
t('days')[day],
|
|
1033
|
+
),
|
|
1034
|
+
),
|
|
1035
|
+
),
|
|
1036
|
+
),
|
|
1037
|
+
label(
|
|
1038
|
+
t('timezone'),
|
|
1039
|
+
h(
|
|
1040
|
+
'select',
|
|
1041
|
+
{
|
|
1042
|
+
name: 'timezone',
|
|
1043
|
+
value: workingDraft.timezone ?? '',
|
|
1044
|
+
disabled: busy,
|
|
1045
|
+
onChange: (e) => update('timezone', e.target.value),
|
|
1046
|
+
},
|
|
1047
|
+
zones.map((zone) => h('option', { key: zone, value: zone }, zone)),
|
|
1048
|
+
),
|
|
1049
|
+
),
|
|
1050
|
+
),
|
|
1051
|
+
h(
|
|
1052
|
+
'fieldset',
|
|
1053
|
+
{ className: 'owc-setting-group' },
|
|
1054
|
+
h('legend', null, t('appearanceGroup')),
|
|
1055
|
+
label(
|
|
1056
|
+
t('opacity'),
|
|
1057
|
+
h(
|
|
1058
|
+
React.Fragment,
|
|
1059
|
+
null,
|
|
1060
|
+
h('input', {
|
|
1061
|
+
className: 'owc-range',
|
|
1062
|
+
type: 'range',
|
|
1063
|
+
min: '30',
|
|
1064
|
+
max: '100',
|
|
1065
|
+
step: '1',
|
|
1066
|
+
value: String(Math.round(view.opacity * 100)),
|
|
1067
|
+
disabled: busy,
|
|
1068
|
+
'aria-label': t('opacity'),
|
|
1069
|
+
onChange: (e) => setPreference({ opacity: e.target.valueAsNumber / 100 }),
|
|
1070
|
+
}),
|
|
1071
|
+
h(
|
|
1072
|
+
'span',
|
|
1073
|
+
{ className: 'owc-hint', style: { margin: 0 } },
|
|
1074
|
+
Math.round(view.opacity * 100) + '% · ' + t('opacityHint'),
|
|
1075
|
+
),
|
|
1076
|
+
),
|
|
1077
|
+
),
|
|
1078
|
+
),
|
|
1079
|
+
preview &&
|
|
1080
|
+
!preview.error &&
|
|
1081
|
+
h(
|
|
1082
|
+
'div',
|
|
1083
|
+
{ className: 'owc-preview' },
|
|
1084
|
+
t('preview') +
|
|
1085
|
+
' · ' +
|
|
1086
|
+
t('daily') +
|
|
1087
|
+
' ' +
|
|
1088
|
+
money(preview.daily) +
|
|
1089
|
+
' · ' +
|
|
1090
|
+
t('hourly') +
|
|
1091
|
+
' ' +
|
|
1092
|
+
money(preview.hourly),
|
|
1093
|
+
),
|
|
1094
|
+
preview?.error &&
|
|
1095
|
+
h(
|
|
1096
|
+
'p',
|
|
1097
|
+
{ className: 'owc-error', role: 'alert', style: { padding: 0 } },
|
|
1098
|
+
preview.error,
|
|
1099
|
+
),
|
|
1100
|
+
h(
|
|
1101
|
+
'details',
|
|
1102
|
+
{ className: 'owc-advanced' },
|
|
1103
|
+
h('summary', null, t('advanced')),
|
|
1104
|
+
h('p', { className: 'owc-hint' }, t('sessionHint')),
|
|
1105
|
+
label(t('session'), sessionSelect()),
|
|
1106
|
+
),
|
|
1107
|
+
h('p', { className: 'owc-hint' }, t('privacy')),
|
|
1108
|
+
h(
|
|
1109
|
+
'div',
|
|
1110
|
+
{ className: 'owc-save' },
|
|
1111
|
+
data?.configured &&
|
|
1112
|
+
h(
|
|
1113
|
+
'button',
|
|
1114
|
+
{ type: 'button', className: 'owc-text', onClick: closeEditor, disabled: busy },
|
|
1115
|
+
t('cancel'),
|
|
1116
|
+
),
|
|
1117
|
+
h(
|
|
1118
|
+
'button',
|
|
1119
|
+
{ type: 'submit', className: 'owc-primary', disabled: busy || !!preview?.error },
|
|
1120
|
+
t(busy ? 'busy' : 'save'),
|
|
1121
|
+
),
|
|
1122
|
+
),
|
|
1123
|
+
);
|
|
502
1124
|
};
|
|
503
1125
|
|
|
504
|
-
const notices = () =>
|
|
505
|
-
|
|
506
|
-
|
|
507
|
-
|
|
508
|
-
|
|
1126
|
+
const notices = () =>
|
|
1127
|
+
h(
|
|
1128
|
+
React.Fragment,
|
|
1129
|
+
null,
|
|
1130
|
+
error && h('p', { className: 'owc-error', role: 'alert' }, error),
|
|
1131
|
+
state?.error && h('p', { className: 'owc-error', role: 'alert' }, state.error),
|
|
1132
|
+
notice && h('p', { className: 'owc-note', role: 'status' }, notice),
|
|
1133
|
+
storageError && h('p', { className: 'owc-note' }, t('storage')),
|
|
1134
|
+
);
|
|
509
1135
|
|
|
510
|
-
return h(
|
|
511
|
-
|
|
512
|
-
|
|
513
|
-
|
|
514
|
-
|
|
515
|
-
|
|
516
|
-
|
|
517
|
-
|
|
518
|
-
|
|
519
|
-
|
|
520
|
-
|
|
521
|
-
|
|
522
|
-
|
|
523
|
-
|
|
524
|
-
|
|
1136
|
+
return h(
|
|
1137
|
+
'section',
|
|
1138
|
+
{
|
|
1139
|
+
ref: box,
|
|
1140
|
+
className:
|
|
1141
|
+
'owc-v2' + (collapsed ? ' owc-compact' : '') + (editing ? ' owc-editing' : ''),
|
|
1142
|
+
'aria-label': t('title'),
|
|
1143
|
+
'data-version': '1.0.1',
|
|
1144
|
+
'data-status': safeState?.status || 'setup',
|
|
1145
|
+
style: cardStyle,
|
|
1146
|
+
onPointerDown: dragDown,
|
|
1147
|
+
onPointerMove: dragMove,
|
|
1148
|
+
onPointerUp: dragEnd,
|
|
1149
|
+
onPointerCancel: dragEnd,
|
|
1150
|
+
onLostPointerCapture: dragEnd,
|
|
1151
|
+
},
|
|
1152
|
+
h('style', null, STYLE),
|
|
1153
|
+
h('div', { className: 'owc-glass', 'aria-hidden': 'true' }),
|
|
1154
|
+
h(
|
|
1155
|
+
'div',
|
|
1156
|
+
{ className: 'owc-content' },
|
|
1157
|
+
collapsed
|
|
1158
|
+
? strip()
|
|
1159
|
+
: h(React.Fragment, null, panelHead(), editing ? editor() : panelBody(), notices()),
|
|
1160
|
+
),
|
|
1161
|
+
);
|
|
525
1162
|
}
|
|
526
1163
|
|
|
527
|
-
ctx.slots.inject('shell.overlay', () =>
|
|
528
|
-
|
|
529
|
-
|
|
530
|
-
|
|
1164
|
+
ctx.slots.inject('shell.overlay', () =>
|
|
1165
|
+
ctx.slots.register(
|
|
1166
|
+
{
|
|
1167
|
+
name: 'shell.overlay',
|
|
1168
|
+
id: 'offwork-clock',
|
|
1169
|
+
order: 900,
|
|
1170
|
+
label: () => ctx.locale.bind('offwork-clock')('title'),
|
|
1171
|
+
},
|
|
1172
|
+
Clock,
|
|
1173
|
+
),
|
|
1174
|
+
);
|
|
531
1175
|
},
|
|
532
1176
|
};
|
|
533
1177
|
},
|