@agenticmail/enterprise 0.5.88 → 0.5.89
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.
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { h, useState, useEffect, useCallback, Fragment, useApp, apiCall, engineCall, formatUptime, buildAgentDataMap, renderAgentBadge, showConfirm, getOrgId } from '../components/utils.js';
|
|
2
2
|
import { I } from '../components/icons.js';
|
|
3
|
+
import { TimezoneSelect } from '../components/timezones.js';
|
|
3
4
|
import { DetailModal } from '../components/modal.js';
|
|
4
5
|
import { CULTURES, LANGUAGES, DEFAULT_TRAITS, computeAge, PersonaForm } from '../components/persona-fields.js';
|
|
5
6
|
import { TagInput } from '../components/tag-input.js';
|
|
@@ -2239,6 +2240,22 @@ function WorkforceSection(props) {
|
|
|
2239
2240
|
var showAddTask = _showAddTask[0]; var setShowAddTask = _showAddTask[1];
|
|
2240
2241
|
var _taskForm = useState({ title: '', description: '', priority: 'normal', type: 'general' });
|
|
2241
2242
|
var taskForm = _taskForm[0]; var setTaskForm = _taskForm[1];
|
|
2243
|
+
var _editing = useState(false);
|
|
2244
|
+
var editing = _editing[0]; var setEditing = _editing[1];
|
|
2245
|
+
var _saving = useState(false);
|
|
2246
|
+
var saving = _saving[0]; var setSaving = _saving[1];
|
|
2247
|
+
|
|
2248
|
+
var dayNames = ['Sun', 'Mon', 'Tue', 'Wed', 'Thu', 'Fri', 'Sat'];
|
|
2249
|
+
|
|
2250
|
+
var defaultSchedForm = {
|
|
2251
|
+
agentId: agentId, timezone: 'UTC', scheduleType: 'standard',
|
|
2252
|
+
config: { standardHours: { start: '09:00', end: '17:00', daysOfWeek: [1, 2, 3, 4, 5] } },
|
|
2253
|
+
enforceClockIn: true, enforceClockOut: true, autoWakeEnabled: true,
|
|
2254
|
+
offHoursAction: 'pause', gracePeriodMinutes: 5, enabled: true
|
|
2255
|
+
};
|
|
2256
|
+
|
|
2257
|
+
var _schedForm = useState(defaultSchedForm);
|
|
2258
|
+
var schedForm = _schedForm[0]; var setSchedForm = _schedForm[1];
|
|
2242
2259
|
|
|
2243
2260
|
var loadAll = function() {
|
|
2244
2261
|
setLoading(true);
|
|
@@ -2248,7 +2265,8 @@ function WorkforceSection(props) {
|
|
|
2248
2265
|
engineCall('/workforce/tasks/' + agentId).catch(function() { return []; }),
|
|
2249
2266
|
engineCall('/workforce/clock-records/' + agentId).catch(function() { return []; })
|
|
2250
2267
|
]).then(function(results) {
|
|
2251
|
-
|
|
2268
|
+
var sched = results[0]?.schedule || results[0];
|
|
2269
|
+
setSchedule(sched);
|
|
2252
2270
|
setStatus(results[1]);
|
|
2253
2271
|
setTasks(results[2]?.tasks || results[2] || []);
|
|
2254
2272
|
setClockRecords(results[3]?.records || results[3] || []);
|
|
@@ -2258,6 +2276,69 @@ function WorkforceSection(props) {
|
|
|
2258
2276
|
|
|
2259
2277
|
useEffect(function() { loadAll(); }, [agentId]);
|
|
2260
2278
|
|
|
2279
|
+
var startEdit = function() {
|
|
2280
|
+
if (schedule) {
|
|
2281
|
+
setSchedForm({
|
|
2282
|
+
agentId: agentId,
|
|
2283
|
+
timezone: schedule.timezone || 'UTC',
|
|
2284
|
+
scheduleType: schedule.scheduleType || 'standard',
|
|
2285
|
+
config: schedule.config || { standardHours: { start: '09:00', end: '17:00', daysOfWeek: [1, 2, 3, 4, 5] } },
|
|
2286
|
+
enforceClockIn: schedule.enforceClockIn ?? true,
|
|
2287
|
+
enforceClockOut: schedule.enforceClockOut ?? true,
|
|
2288
|
+
autoWakeEnabled: schedule.autoWakeEnabled ?? true,
|
|
2289
|
+
offHoursAction: schedule.offHoursAction || 'pause',
|
|
2290
|
+
gracePeriodMinutes: schedule.gracePeriodMinutes ?? 5,
|
|
2291
|
+
enabled: schedule.enabled ?? true
|
|
2292
|
+
});
|
|
2293
|
+
} else {
|
|
2294
|
+
setSchedForm(Object.assign({}, defaultSchedForm, { agentId: agentId }));
|
|
2295
|
+
}
|
|
2296
|
+
setEditing(true);
|
|
2297
|
+
};
|
|
2298
|
+
|
|
2299
|
+
var saveSchedule = function() {
|
|
2300
|
+
setSaving(true);
|
|
2301
|
+
var isUpdate = schedule && schedule.id;
|
|
2302
|
+
var method = isUpdate ? 'PUT' : 'POST';
|
|
2303
|
+
var url = isUpdate ? '/workforce/schedules/' + schedule.id : '/workforce/schedules';
|
|
2304
|
+
engineCall(url, { method: method, body: JSON.stringify(schedForm) })
|
|
2305
|
+
.then(function() { toast('Schedule saved', 'success'); setEditing(false); loadAll(); })
|
|
2306
|
+
.catch(function(err) { toast(err.message, 'error'); })
|
|
2307
|
+
.finally(function() { setSaving(false); });
|
|
2308
|
+
};
|
|
2309
|
+
|
|
2310
|
+
var deleteSchedule = function() {
|
|
2311
|
+
if (!schedule) return;
|
|
2312
|
+
engineCall('/workforce/schedules/' + agentId, { method: 'DELETE' })
|
|
2313
|
+
.then(function() { toast('Schedule removed', 'success'); setEditing(false); loadAll(); })
|
|
2314
|
+
.catch(function(err) { toast(err.message, 'error'); });
|
|
2315
|
+
};
|
|
2316
|
+
|
|
2317
|
+
var toggleDay = function(d) {
|
|
2318
|
+
var days = (schedForm.config?.standardHours?.daysOfWeek || []).slice();
|
|
2319
|
+
var idx = days.indexOf(d);
|
|
2320
|
+
if (idx >= 0) days.splice(idx, 1); else days.push(d);
|
|
2321
|
+
days.sort();
|
|
2322
|
+
setSchedForm(Object.assign({}, schedForm, { config: Object.assign({}, schedForm.config, { standardHours: Object.assign({}, schedForm.config?.standardHours, { daysOfWeek: days }) }) }));
|
|
2323
|
+
};
|
|
2324
|
+
|
|
2325
|
+
var addShift = function() {
|
|
2326
|
+
var shifts = (schedForm.config?.shifts || []).concat([{ name: '', start: '09:00', end: '17:00' }]);
|
|
2327
|
+
setSchedForm(Object.assign({}, schedForm, { config: Object.assign({}, schedForm.config, { shifts: shifts }) }));
|
|
2328
|
+
};
|
|
2329
|
+
|
|
2330
|
+
var updateShift = function(idx, key, val) {
|
|
2331
|
+
var shifts = (schedForm.config?.shifts || []).slice();
|
|
2332
|
+
shifts[idx] = Object.assign({}, shifts[idx], (function() { var o = {}; o[key] = val; return o; })());
|
|
2333
|
+
setSchedForm(Object.assign({}, schedForm, { config: Object.assign({}, schedForm.config, { shifts: shifts }) }));
|
|
2334
|
+
};
|
|
2335
|
+
|
|
2336
|
+
var removeShift = function(idx) {
|
|
2337
|
+
var shifts = (schedForm.config?.shifts || []).slice();
|
|
2338
|
+
shifts.splice(idx, 1);
|
|
2339
|
+
setSchedForm(Object.assign({}, schedForm, { config: Object.assign({}, schedForm.config, { shifts: shifts }) }));
|
|
2340
|
+
};
|
|
2341
|
+
|
|
2261
2342
|
var clockIn = function() {
|
|
2262
2343
|
engineCall('/workforce/clock-in/' + agentId, { method: 'POST' })
|
|
2263
2344
|
.then(function() { toast('Agent clocked in', 'success'); loadAll(); })
|
|
@@ -2294,6 +2375,20 @@ function WorkforceSection(props) {
|
|
|
2294
2375
|
.catch(function(err) { toast(err.message, 'error'); });
|
|
2295
2376
|
};
|
|
2296
2377
|
|
|
2378
|
+
// Helper: format schedule hours display
|
|
2379
|
+
var formatHours = function(s) {
|
|
2380
|
+
if (!s) return '-';
|
|
2381
|
+
if (s.scheduleType === 'standard' && s.config?.standardHours) {
|
|
2382
|
+
return (s.config.standardHours.start || '09:00') + ' - ' + (s.config.standardHours.end || '17:00');
|
|
2383
|
+
}
|
|
2384
|
+
if (s.scheduleType === 'shift' && s.config?.shifts?.length) {
|
|
2385
|
+
return s.config.shifts.map(function(sh) { return (sh.name ? sh.name + ': ' : '') + sh.start + '-' + sh.end; }).join(', ');
|
|
2386
|
+
}
|
|
2387
|
+
return '-';
|
|
2388
|
+
};
|
|
2389
|
+
|
|
2390
|
+
var formatDays = function(days) { return days?.map(function(d) { return dayNames[d]; }).join(', ') || '-'; };
|
|
2391
|
+
|
|
2297
2392
|
if (loading) {
|
|
2298
2393
|
return h('div', { style: { padding: 40, textAlign: 'center', color: 'var(--text-muted)' } }, 'Loading workforce data...');
|
|
2299
2394
|
}
|
|
@@ -2321,32 +2416,151 @@ function WorkforceSection(props) {
|
|
|
2321
2416
|
|
|
2322
2417
|
// ─── Schedule Card ──────────────────────────────────
|
|
2323
2418
|
h('div', { className: 'card', style: { marginBottom: 20 } },
|
|
2324
|
-
h('div', { className: 'card-header'
|
|
2419
|
+
h('div', { className: 'card-header', style: { display: 'flex', justifyContent: 'space-between', alignItems: 'center' } },
|
|
2420
|
+
h('span', null, 'Schedule'),
|
|
2421
|
+
!editing && h('button', { className: 'btn btn-ghost btn-sm', onClick: startEdit }, I.edit(), schedule ? ' Edit' : ' Configure')
|
|
2422
|
+
),
|
|
2325
2423
|
h('div', { className: 'card-body' },
|
|
2326
|
-
|
|
2327
|
-
? h(
|
|
2328
|
-
|
|
2329
|
-
|
|
2330
|
-
h('
|
|
2424
|
+
editing
|
|
2425
|
+
? h(Fragment, null,
|
|
2426
|
+
// Schedule Type
|
|
2427
|
+
h('div', { className: 'form-group' },
|
|
2428
|
+
h('label', { className: 'form-label' }, 'Schedule Type'),
|
|
2429
|
+
h('div', { style: { display: 'flex', gap: 16 } },
|
|
2430
|
+
['standard', 'shift', 'custom'].map(function(t) {
|
|
2431
|
+
return h('label', { key: t, style: { display: 'flex', alignItems: 'center', gap: 4, cursor: 'pointer' } },
|
|
2432
|
+
h('input', { type: 'radio', name: 'schedTypeDetail', checked: schedForm.scheduleType === t, onChange: function() { setSchedForm(Object.assign({}, schedForm, { scheduleType: t })); } }),
|
|
2433
|
+
t.charAt(0).toUpperCase() + t.slice(1)
|
|
2434
|
+
);
|
|
2435
|
+
})
|
|
2436
|
+
)
|
|
2331
2437
|
),
|
|
2332
|
-
|
|
2333
|
-
|
|
2334
|
-
h('div', { style: {
|
|
2438
|
+
// Standard fields
|
|
2439
|
+
schedForm.scheduleType === 'standard' && h(Fragment, null,
|
|
2440
|
+
h('div', { style: { display: 'flex', gap: 12 } },
|
|
2441
|
+
h('div', { className: 'form-group', style: { flex: 1 } },
|
|
2442
|
+
h('label', { className: 'form-label' }, 'Start Time'),
|
|
2443
|
+
h('input', { className: 'input', type: 'time', value: schedForm.config?.standardHours?.start || '09:00', onChange: function(e) { setSchedForm(Object.assign({}, schedForm, { config: Object.assign({}, schedForm.config, { standardHours: Object.assign({}, schedForm.config?.standardHours, { start: e.target.value }) }) })); } })
|
|
2444
|
+
),
|
|
2445
|
+
h('div', { className: 'form-group', style: { flex: 1 } },
|
|
2446
|
+
h('label', { className: 'form-label' }, 'End Time'),
|
|
2447
|
+
h('input', { className: 'input', type: 'time', value: schedForm.config?.standardHours?.end || '17:00', onChange: function(e) { setSchedForm(Object.assign({}, schedForm, { config: Object.assign({}, schedForm.config, { standardHours: Object.assign({}, schedForm.config?.standardHours, { end: e.target.value }) }) })); } })
|
|
2448
|
+
)
|
|
2449
|
+
),
|
|
2450
|
+
h('div', { className: 'form-group' },
|
|
2451
|
+
h('label', { className: 'form-label' }, 'Days of Week'),
|
|
2452
|
+
h('div', { style: { display: 'flex', gap: 6, flexWrap: 'wrap' } },
|
|
2453
|
+
[0, 1, 2, 3, 4, 5, 6].map(function(d) {
|
|
2454
|
+
return h('button', {
|
|
2455
|
+
key: d, type: 'button',
|
|
2456
|
+
className: 'btn btn-sm ' + ((schedForm.config?.standardHours?.daysOfWeek || []).includes(d) ? 'btn-primary' : 'btn-ghost'),
|
|
2457
|
+
onClick: function() { toggleDay(d); }
|
|
2458
|
+
}, dayNames[d]);
|
|
2459
|
+
})
|
|
2460
|
+
)
|
|
2461
|
+
)
|
|
2335
2462
|
),
|
|
2336
|
-
|
|
2337
|
-
|
|
2338
|
-
h('div', {
|
|
2463
|
+
// Shift fields
|
|
2464
|
+
schedForm.scheduleType === 'shift' && h(Fragment, null,
|
|
2465
|
+
h('div', { className: 'form-group' },
|
|
2466
|
+
h('label', { className: 'form-label' }, 'Shifts'),
|
|
2467
|
+
(schedForm.config?.shifts || []).map(function(sh, idx) {
|
|
2468
|
+
return h('div', { key: idx, style: { display: 'flex', gap: 8, alignItems: 'center', marginBottom: 8, padding: 8, background: 'var(--bg-tertiary)', borderRadius: 6 } },
|
|
2469
|
+
h('input', { className: 'input', style: { flex: 1 }, placeholder: 'Shift name', value: sh.name, onChange: function(e) { updateShift(idx, 'name', e.target.value); } }),
|
|
2470
|
+
h('input', { className: 'input', type: 'time', style: { width: 110 }, value: sh.start, onChange: function(e) { updateShift(idx, 'start', e.target.value); } }),
|
|
2471
|
+
h('input', { className: 'input', type: 'time', style: { width: 110 }, value: sh.end, onChange: function(e) { updateShift(idx, 'end', e.target.value); } }),
|
|
2472
|
+
h('button', { className: 'btn btn-ghost btn-icon btn-sm', onClick: function() { removeShift(idx); } }, I.x())
|
|
2473
|
+
);
|
|
2474
|
+
}),
|
|
2475
|
+
h('button', { className: 'btn btn-ghost btn-sm', onClick: addShift }, I.plus(), ' Add Shift')
|
|
2476
|
+
)
|
|
2339
2477
|
),
|
|
2340
|
-
|
|
2341
|
-
|
|
2342
|
-
h('
|
|
2343
|
-
|
|
2344
|
-
|
|
2345
|
-
|
|
2478
|
+
// Timezone
|
|
2479
|
+
h('div', { className: 'form-group' },
|
|
2480
|
+
h('label', { className: 'form-label' }, 'Timezone'),
|
|
2481
|
+
TimezoneSelect(h, schedForm.timezone, function(e) { setSchedForm(Object.assign({}, schedForm, { timezone: e.target.value })); })
|
|
2482
|
+
),
|
|
2483
|
+
// Toggles
|
|
2484
|
+
h('div', { style: { display: 'grid', gridTemplateColumns: '1fr 1fr', gap: 12, marginTop: 8 } },
|
|
2485
|
+
h('label', { style: { display: 'flex', alignItems: 'center', gap: 6, cursor: 'pointer', fontSize: 13 } },
|
|
2486
|
+
h('input', { type: 'checkbox', checked: schedForm.enforceClockIn, onChange: function(e) { setSchedForm(Object.assign({}, schedForm, { enforceClockIn: e.target.checked })); } }),
|
|
2487
|
+
'Enforce Clock-In'
|
|
2488
|
+
),
|
|
2489
|
+
h('label', { style: { display: 'flex', alignItems: 'center', gap: 6, cursor: 'pointer', fontSize: 13 } },
|
|
2490
|
+
h('input', { type: 'checkbox', checked: schedForm.enforceClockOut, onChange: function(e) { setSchedForm(Object.assign({}, schedForm, { enforceClockOut: e.target.checked })); } }),
|
|
2491
|
+
'Enforce Clock-Out'
|
|
2492
|
+
),
|
|
2493
|
+
h('label', { style: { display: 'flex', alignItems: 'center', gap: 6, cursor: 'pointer', fontSize: 13 } },
|
|
2494
|
+
h('input', { type: 'checkbox', checked: schedForm.autoWakeEnabled, onChange: function(e) { setSchedForm(Object.assign({}, schedForm, { autoWakeEnabled: e.target.checked })); } }),
|
|
2495
|
+
'Auto-Wake'
|
|
2496
|
+
),
|
|
2497
|
+
h('label', { style: { display: 'flex', alignItems: 'center', gap: 6, cursor: 'pointer', fontSize: 13 } },
|
|
2498
|
+
h('input', { type: 'checkbox', checked: schedForm.enabled, onChange: function(e) { setSchedForm(Object.assign({}, schedForm, { enabled: e.target.checked })); } }),
|
|
2499
|
+
'Enabled'
|
|
2500
|
+
)
|
|
2501
|
+
),
|
|
2502
|
+
// Off-hours + grace
|
|
2503
|
+
h('div', { style: { display: 'flex', gap: 12, marginTop: 12 } },
|
|
2504
|
+
h('div', { className: 'form-group', style: { flex: 1 } },
|
|
2505
|
+
h('label', { className: 'form-label' }, 'Off-Hours Action'),
|
|
2506
|
+
h('select', { className: 'input', value: schedForm.offHoursAction, onChange: function(e) { setSchedForm(Object.assign({}, schedForm, { offHoursAction: e.target.value })); } },
|
|
2507
|
+
h('option', { value: 'pause' }, 'Pause'), h('option', { value: 'stop' }, 'Stop'), h('option', { value: 'queue' }, 'Queue'))
|
|
2508
|
+
),
|
|
2509
|
+
h('div', { className: 'form-group', style: { flex: 1 } },
|
|
2510
|
+
h('label', { className: 'form-label' }, 'Grace Period (min)'),
|
|
2511
|
+
h('input', { className: 'input', type: 'number', value: schedForm.gracePeriodMinutes, onChange: function(e) { setSchedForm(Object.assign({}, schedForm, { gracePeriodMinutes: parseInt(e.target.value) || 0 })); } })
|
|
2346
2512
|
)
|
|
2513
|
+
),
|
|
2514
|
+
// Actions
|
|
2515
|
+
h('div', { style: { display: 'flex', gap: 8, marginTop: 16, justifyContent: 'space-between' } },
|
|
2516
|
+
h('div', { style: { display: 'flex', gap: 8 } },
|
|
2517
|
+
h('button', { className: 'btn btn-primary', disabled: saving, onClick: saveSchedule }, saving ? 'Saving...' : 'Save Schedule'),
|
|
2518
|
+
h('button', { className: 'btn btn-ghost', onClick: function() { setEditing(false); } }, 'Cancel')
|
|
2519
|
+
),
|
|
2520
|
+
schedule && h('button', { className: 'btn btn-ghost', style: { color: 'var(--danger)' }, onClick: deleteSchedule }, I.x(), ' Remove Schedule')
|
|
2347
2521
|
)
|
|
2348
2522
|
)
|
|
2349
|
-
:
|
|
2523
|
+
: schedule
|
|
2524
|
+
? h('div', { style: { display: 'grid', gridTemplateColumns: '1fr 1fr', gap: 16 } },
|
|
2525
|
+
h('div', null,
|
|
2526
|
+
h('div', { style: { fontSize: 12, color: 'var(--text-muted)', marginBottom: 4 } }, 'Schedule Type'),
|
|
2527
|
+
h('div', { style: { fontSize: 14, fontWeight: 600 } }, (schedule.scheduleType || 'standard').charAt(0).toUpperCase() + (schedule.scheduleType || 'standard').slice(1))
|
|
2528
|
+
),
|
|
2529
|
+
h('div', null,
|
|
2530
|
+
h('div', { style: { fontSize: 12, color: 'var(--text-muted)', marginBottom: 4 } }, 'Hours'),
|
|
2531
|
+
h('div', { style: { fontSize: 14, fontWeight: 600 } }, formatHours(schedule))
|
|
2532
|
+
),
|
|
2533
|
+
h('div', null,
|
|
2534
|
+
h('div', { style: { fontSize: 12, color: 'var(--text-muted)', marginBottom: 4 } }, 'Timezone'),
|
|
2535
|
+
h('div', { style: { fontSize: 14, fontWeight: 600 } }, schedule.timezone || 'UTC')
|
|
2536
|
+
),
|
|
2537
|
+
h('div', null,
|
|
2538
|
+
h('div', { style: { fontSize: 12, color: 'var(--text-muted)', marginBottom: 4 } }, 'Days'),
|
|
2539
|
+
h('div', { style: { fontSize: 14, fontWeight: 600 } }, formatDays(schedule.config?.standardHours?.daysOfWeek))
|
|
2540
|
+
),
|
|
2541
|
+
h('div', null,
|
|
2542
|
+
h('div', { style: { fontSize: 12, color: 'var(--text-muted)', marginBottom: 4 } }, 'Enforcement'),
|
|
2543
|
+
h('div', { style: { display: 'flex', gap: 6, flexWrap: 'wrap' } },
|
|
2544
|
+
h('span', { className: schedule.enforceClockIn ? 'badge badge-success' : 'badge badge-neutral' }, schedule.enforceClockIn ? 'Clock-In Enforced' : 'Clock-In Flexible'),
|
|
2545
|
+
h('span', { className: schedule.enforceClockOut ? 'badge badge-success' : 'badge badge-neutral' }, schedule.enforceClockOut ? 'Clock-Out Enforced' : 'Clock-Out Flexible')
|
|
2546
|
+
)
|
|
2547
|
+
),
|
|
2548
|
+
h('div', null,
|
|
2549
|
+
h('div', { style: { fontSize: 12, color: 'var(--text-muted)', marginBottom: 4 } }, 'Off-Hours'),
|
|
2550
|
+
h('div', { style: { fontSize: 14, fontWeight: 600 } }, (schedule.offHoursAction || 'pause').charAt(0).toUpperCase() + (schedule.offHoursAction || 'pause').slice(1) + (schedule.gracePeriodMinutes ? ' (' + schedule.gracePeriodMinutes + 'min grace)' : ''))
|
|
2551
|
+
),
|
|
2552
|
+
h('div', null,
|
|
2553
|
+
h('div', { style: { fontSize: 12, color: 'var(--text-muted)', marginBottom: 4 } }, 'Status'),
|
|
2554
|
+
h('div', { style: { display: 'flex', gap: 6 } },
|
|
2555
|
+
h('span', { className: schedule.enabled ? 'badge badge-success' : 'badge badge-neutral' }, schedule.enabled ? 'Enabled' : 'Disabled'),
|
|
2556
|
+
schedule.autoWakeEnabled && h('span', { className: 'badge badge-info' }, 'Auto-Wake')
|
|
2557
|
+
)
|
|
2558
|
+
)
|
|
2559
|
+
)
|
|
2560
|
+
: h('div', { style: { textAlign: 'center', padding: 20, color: 'var(--text-muted)', fontSize: 13 } },
|
|
2561
|
+
'No schedule configured. ',
|
|
2562
|
+
h('button', { className: 'btn btn-ghost btn-sm', onClick: startEdit }, 'Configure now')
|
|
2563
|
+
)
|
|
2350
2564
|
)
|
|
2351
2565
|
),
|
|
2352
2566
|
|
package/package.json
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { h, useState, useEffect, useCallback, Fragment, useApp, apiCall, engineCall, formatUptime, buildAgentDataMap, renderAgentBadge, showConfirm, getOrgId } from '../components/utils.js';
|
|
2
2
|
import { I } from '../components/icons.js';
|
|
3
|
+
import { TimezoneSelect } from '../components/timezones.js';
|
|
3
4
|
import { DetailModal } from '../components/modal.js';
|
|
4
5
|
import { CULTURES, LANGUAGES, DEFAULT_TRAITS, computeAge, PersonaForm } from '../components/persona-fields.js';
|
|
5
6
|
import { TagInput } from '../components/tag-input.js';
|
|
@@ -2239,6 +2240,22 @@ function WorkforceSection(props) {
|
|
|
2239
2240
|
var showAddTask = _showAddTask[0]; var setShowAddTask = _showAddTask[1];
|
|
2240
2241
|
var _taskForm = useState({ title: '', description: '', priority: 'normal', type: 'general' });
|
|
2241
2242
|
var taskForm = _taskForm[0]; var setTaskForm = _taskForm[1];
|
|
2243
|
+
var _editing = useState(false);
|
|
2244
|
+
var editing = _editing[0]; var setEditing = _editing[1];
|
|
2245
|
+
var _saving = useState(false);
|
|
2246
|
+
var saving = _saving[0]; var setSaving = _saving[1];
|
|
2247
|
+
|
|
2248
|
+
var dayNames = ['Sun', 'Mon', 'Tue', 'Wed', 'Thu', 'Fri', 'Sat'];
|
|
2249
|
+
|
|
2250
|
+
var defaultSchedForm = {
|
|
2251
|
+
agentId: agentId, timezone: 'UTC', scheduleType: 'standard',
|
|
2252
|
+
config: { standardHours: { start: '09:00', end: '17:00', daysOfWeek: [1, 2, 3, 4, 5] } },
|
|
2253
|
+
enforceClockIn: true, enforceClockOut: true, autoWakeEnabled: true,
|
|
2254
|
+
offHoursAction: 'pause', gracePeriodMinutes: 5, enabled: true
|
|
2255
|
+
};
|
|
2256
|
+
|
|
2257
|
+
var _schedForm = useState(defaultSchedForm);
|
|
2258
|
+
var schedForm = _schedForm[0]; var setSchedForm = _schedForm[1];
|
|
2242
2259
|
|
|
2243
2260
|
var loadAll = function() {
|
|
2244
2261
|
setLoading(true);
|
|
@@ -2248,7 +2265,8 @@ function WorkforceSection(props) {
|
|
|
2248
2265
|
engineCall('/workforce/tasks/' + agentId).catch(function() { return []; }),
|
|
2249
2266
|
engineCall('/workforce/clock-records/' + agentId).catch(function() { return []; })
|
|
2250
2267
|
]).then(function(results) {
|
|
2251
|
-
|
|
2268
|
+
var sched = results[0]?.schedule || results[0];
|
|
2269
|
+
setSchedule(sched);
|
|
2252
2270
|
setStatus(results[1]);
|
|
2253
2271
|
setTasks(results[2]?.tasks || results[2] || []);
|
|
2254
2272
|
setClockRecords(results[3]?.records || results[3] || []);
|
|
@@ -2258,6 +2276,69 @@ function WorkforceSection(props) {
|
|
|
2258
2276
|
|
|
2259
2277
|
useEffect(function() { loadAll(); }, [agentId]);
|
|
2260
2278
|
|
|
2279
|
+
var startEdit = function() {
|
|
2280
|
+
if (schedule) {
|
|
2281
|
+
setSchedForm({
|
|
2282
|
+
agentId: agentId,
|
|
2283
|
+
timezone: schedule.timezone || 'UTC',
|
|
2284
|
+
scheduleType: schedule.scheduleType || 'standard',
|
|
2285
|
+
config: schedule.config || { standardHours: { start: '09:00', end: '17:00', daysOfWeek: [1, 2, 3, 4, 5] } },
|
|
2286
|
+
enforceClockIn: schedule.enforceClockIn ?? true,
|
|
2287
|
+
enforceClockOut: schedule.enforceClockOut ?? true,
|
|
2288
|
+
autoWakeEnabled: schedule.autoWakeEnabled ?? true,
|
|
2289
|
+
offHoursAction: schedule.offHoursAction || 'pause',
|
|
2290
|
+
gracePeriodMinutes: schedule.gracePeriodMinutes ?? 5,
|
|
2291
|
+
enabled: schedule.enabled ?? true
|
|
2292
|
+
});
|
|
2293
|
+
} else {
|
|
2294
|
+
setSchedForm(Object.assign({}, defaultSchedForm, { agentId: agentId }));
|
|
2295
|
+
}
|
|
2296
|
+
setEditing(true);
|
|
2297
|
+
};
|
|
2298
|
+
|
|
2299
|
+
var saveSchedule = function() {
|
|
2300
|
+
setSaving(true);
|
|
2301
|
+
var isUpdate = schedule && schedule.id;
|
|
2302
|
+
var method = isUpdate ? 'PUT' : 'POST';
|
|
2303
|
+
var url = isUpdate ? '/workforce/schedules/' + schedule.id : '/workforce/schedules';
|
|
2304
|
+
engineCall(url, { method: method, body: JSON.stringify(schedForm) })
|
|
2305
|
+
.then(function() { toast('Schedule saved', 'success'); setEditing(false); loadAll(); })
|
|
2306
|
+
.catch(function(err) { toast(err.message, 'error'); })
|
|
2307
|
+
.finally(function() { setSaving(false); });
|
|
2308
|
+
};
|
|
2309
|
+
|
|
2310
|
+
var deleteSchedule = function() {
|
|
2311
|
+
if (!schedule) return;
|
|
2312
|
+
engineCall('/workforce/schedules/' + agentId, { method: 'DELETE' })
|
|
2313
|
+
.then(function() { toast('Schedule removed', 'success'); setEditing(false); loadAll(); })
|
|
2314
|
+
.catch(function(err) { toast(err.message, 'error'); });
|
|
2315
|
+
};
|
|
2316
|
+
|
|
2317
|
+
var toggleDay = function(d) {
|
|
2318
|
+
var days = (schedForm.config?.standardHours?.daysOfWeek || []).slice();
|
|
2319
|
+
var idx = days.indexOf(d);
|
|
2320
|
+
if (idx >= 0) days.splice(idx, 1); else days.push(d);
|
|
2321
|
+
days.sort();
|
|
2322
|
+
setSchedForm(Object.assign({}, schedForm, { config: Object.assign({}, schedForm.config, { standardHours: Object.assign({}, schedForm.config?.standardHours, { daysOfWeek: days }) }) }));
|
|
2323
|
+
};
|
|
2324
|
+
|
|
2325
|
+
var addShift = function() {
|
|
2326
|
+
var shifts = (schedForm.config?.shifts || []).concat([{ name: '', start: '09:00', end: '17:00' }]);
|
|
2327
|
+
setSchedForm(Object.assign({}, schedForm, { config: Object.assign({}, schedForm.config, { shifts: shifts }) }));
|
|
2328
|
+
};
|
|
2329
|
+
|
|
2330
|
+
var updateShift = function(idx, key, val) {
|
|
2331
|
+
var shifts = (schedForm.config?.shifts || []).slice();
|
|
2332
|
+
shifts[idx] = Object.assign({}, shifts[idx], (function() { var o = {}; o[key] = val; return o; })());
|
|
2333
|
+
setSchedForm(Object.assign({}, schedForm, { config: Object.assign({}, schedForm.config, { shifts: shifts }) }));
|
|
2334
|
+
};
|
|
2335
|
+
|
|
2336
|
+
var removeShift = function(idx) {
|
|
2337
|
+
var shifts = (schedForm.config?.shifts || []).slice();
|
|
2338
|
+
shifts.splice(idx, 1);
|
|
2339
|
+
setSchedForm(Object.assign({}, schedForm, { config: Object.assign({}, schedForm.config, { shifts: shifts }) }));
|
|
2340
|
+
};
|
|
2341
|
+
|
|
2261
2342
|
var clockIn = function() {
|
|
2262
2343
|
engineCall('/workforce/clock-in/' + agentId, { method: 'POST' })
|
|
2263
2344
|
.then(function() { toast('Agent clocked in', 'success'); loadAll(); })
|
|
@@ -2294,6 +2375,20 @@ function WorkforceSection(props) {
|
|
|
2294
2375
|
.catch(function(err) { toast(err.message, 'error'); });
|
|
2295
2376
|
};
|
|
2296
2377
|
|
|
2378
|
+
// Helper: format schedule hours display
|
|
2379
|
+
var formatHours = function(s) {
|
|
2380
|
+
if (!s) return '-';
|
|
2381
|
+
if (s.scheduleType === 'standard' && s.config?.standardHours) {
|
|
2382
|
+
return (s.config.standardHours.start || '09:00') + ' - ' + (s.config.standardHours.end || '17:00');
|
|
2383
|
+
}
|
|
2384
|
+
if (s.scheduleType === 'shift' && s.config?.shifts?.length) {
|
|
2385
|
+
return s.config.shifts.map(function(sh) { return (sh.name ? sh.name + ': ' : '') + sh.start + '-' + sh.end; }).join(', ');
|
|
2386
|
+
}
|
|
2387
|
+
return '-';
|
|
2388
|
+
};
|
|
2389
|
+
|
|
2390
|
+
var formatDays = function(days) { return days?.map(function(d) { return dayNames[d]; }).join(', ') || '-'; };
|
|
2391
|
+
|
|
2297
2392
|
if (loading) {
|
|
2298
2393
|
return h('div', { style: { padding: 40, textAlign: 'center', color: 'var(--text-muted)' } }, 'Loading workforce data...');
|
|
2299
2394
|
}
|
|
@@ -2321,32 +2416,151 @@ function WorkforceSection(props) {
|
|
|
2321
2416
|
|
|
2322
2417
|
// ─── Schedule Card ──────────────────────────────────
|
|
2323
2418
|
h('div', { className: 'card', style: { marginBottom: 20 } },
|
|
2324
|
-
h('div', { className: 'card-header'
|
|
2419
|
+
h('div', { className: 'card-header', style: { display: 'flex', justifyContent: 'space-between', alignItems: 'center' } },
|
|
2420
|
+
h('span', null, 'Schedule'),
|
|
2421
|
+
!editing && h('button', { className: 'btn btn-ghost btn-sm', onClick: startEdit }, I.edit(), schedule ? ' Edit' : ' Configure')
|
|
2422
|
+
),
|
|
2325
2423
|
h('div', { className: 'card-body' },
|
|
2326
|
-
|
|
2327
|
-
? h(
|
|
2328
|
-
|
|
2329
|
-
|
|
2330
|
-
h('
|
|
2424
|
+
editing
|
|
2425
|
+
? h(Fragment, null,
|
|
2426
|
+
// Schedule Type
|
|
2427
|
+
h('div', { className: 'form-group' },
|
|
2428
|
+
h('label', { className: 'form-label' }, 'Schedule Type'),
|
|
2429
|
+
h('div', { style: { display: 'flex', gap: 16 } },
|
|
2430
|
+
['standard', 'shift', 'custom'].map(function(t) {
|
|
2431
|
+
return h('label', { key: t, style: { display: 'flex', alignItems: 'center', gap: 4, cursor: 'pointer' } },
|
|
2432
|
+
h('input', { type: 'radio', name: 'schedTypeDetail', checked: schedForm.scheduleType === t, onChange: function() { setSchedForm(Object.assign({}, schedForm, { scheduleType: t })); } }),
|
|
2433
|
+
t.charAt(0).toUpperCase() + t.slice(1)
|
|
2434
|
+
);
|
|
2435
|
+
})
|
|
2436
|
+
)
|
|
2331
2437
|
),
|
|
2332
|
-
|
|
2333
|
-
|
|
2334
|
-
h('div', { style: {
|
|
2438
|
+
// Standard fields
|
|
2439
|
+
schedForm.scheduleType === 'standard' && h(Fragment, null,
|
|
2440
|
+
h('div', { style: { display: 'flex', gap: 12 } },
|
|
2441
|
+
h('div', { className: 'form-group', style: { flex: 1 } },
|
|
2442
|
+
h('label', { className: 'form-label' }, 'Start Time'),
|
|
2443
|
+
h('input', { className: 'input', type: 'time', value: schedForm.config?.standardHours?.start || '09:00', onChange: function(e) { setSchedForm(Object.assign({}, schedForm, { config: Object.assign({}, schedForm.config, { standardHours: Object.assign({}, schedForm.config?.standardHours, { start: e.target.value }) }) })); } })
|
|
2444
|
+
),
|
|
2445
|
+
h('div', { className: 'form-group', style: { flex: 1 } },
|
|
2446
|
+
h('label', { className: 'form-label' }, 'End Time'),
|
|
2447
|
+
h('input', { className: 'input', type: 'time', value: schedForm.config?.standardHours?.end || '17:00', onChange: function(e) { setSchedForm(Object.assign({}, schedForm, { config: Object.assign({}, schedForm.config, { standardHours: Object.assign({}, schedForm.config?.standardHours, { end: e.target.value }) }) })); } })
|
|
2448
|
+
)
|
|
2449
|
+
),
|
|
2450
|
+
h('div', { className: 'form-group' },
|
|
2451
|
+
h('label', { className: 'form-label' }, 'Days of Week'),
|
|
2452
|
+
h('div', { style: { display: 'flex', gap: 6, flexWrap: 'wrap' } },
|
|
2453
|
+
[0, 1, 2, 3, 4, 5, 6].map(function(d) {
|
|
2454
|
+
return h('button', {
|
|
2455
|
+
key: d, type: 'button',
|
|
2456
|
+
className: 'btn btn-sm ' + ((schedForm.config?.standardHours?.daysOfWeek || []).includes(d) ? 'btn-primary' : 'btn-ghost'),
|
|
2457
|
+
onClick: function() { toggleDay(d); }
|
|
2458
|
+
}, dayNames[d]);
|
|
2459
|
+
})
|
|
2460
|
+
)
|
|
2461
|
+
)
|
|
2335
2462
|
),
|
|
2336
|
-
|
|
2337
|
-
|
|
2338
|
-
h('div', {
|
|
2463
|
+
// Shift fields
|
|
2464
|
+
schedForm.scheduleType === 'shift' && h(Fragment, null,
|
|
2465
|
+
h('div', { className: 'form-group' },
|
|
2466
|
+
h('label', { className: 'form-label' }, 'Shifts'),
|
|
2467
|
+
(schedForm.config?.shifts || []).map(function(sh, idx) {
|
|
2468
|
+
return h('div', { key: idx, style: { display: 'flex', gap: 8, alignItems: 'center', marginBottom: 8, padding: 8, background: 'var(--bg-tertiary)', borderRadius: 6 } },
|
|
2469
|
+
h('input', { className: 'input', style: { flex: 1 }, placeholder: 'Shift name', value: sh.name, onChange: function(e) { updateShift(idx, 'name', e.target.value); } }),
|
|
2470
|
+
h('input', { className: 'input', type: 'time', style: { width: 110 }, value: sh.start, onChange: function(e) { updateShift(idx, 'start', e.target.value); } }),
|
|
2471
|
+
h('input', { className: 'input', type: 'time', style: { width: 110 }, value: sh.end, onChange: function(e) { updateShift(idx, 'end', e.target.value); } }),
|
|
2472
|
+
h('button', { className: 'btn btn-ghost btn-icon btn-sm', onClick: function() { removeShift(idx); } }, I.x())
|
|
2473
|
+
);
|
|
2474
|
+
}),
|
|
2475
|
+
h('button', { className: 'btn btn-ghost btn-sm', onClick: addShift }, I.plus(), ' Add Shift')
|
|
2476
|
+
)
|
|
2339
2477
|
),
|
|
2340
|
-
|
|
2341
|
-
|
|
2342
|
-
h('
|
|
2343
|
-
|
|
2344
|
-
|
|
2345
|
-
|
|
2478
|
+
// Timezone
|
|
2479
|
+
h('div', { className: 'form-group' },
|
|
2480
|
+
h('label', { className: 'form-label' }, 'Timezone'),
|
|
2481
|
+
TimezoneSelect(h, schedForm.timezone, function(e) { setSchedForm(Object.assign({}, schedForm, { timezone: e.target.value })); })
|
|
2482
|
+
),
|
|
2483
|
+
// Toggles
|
|
2484
|
+
h('div', { style: { display: 'grid', gridTemplateColumns: '1fr 1fr', gap: 12, marginTop: 8 } },
|
|
2485
|
+
h('label', { style: { display: 'flex', alignItems: 'center', gap: 6, cursor: 'pointer', fontSize: 13 } },
|
|
2486
|
+
h('input', { type: 'checkbox', checked: schedForm.enforceClockIn, onChange: function(e) { setSchedForm(Object.assign({}, schedForm, { enforceClockIn: e.target.checked })); } }),
|
|
2487
|
+
'Enforce Clock-In'
|
|
2488
|
+
),
|
|
2489
|
+
h('label', { style: { display: 'flex', alignItems: 'center', gap: 6, cursor: 'pointer', fontSize: 13 } },
|
|
2490
|
+
h('input', { type: 'checkbox', checked: schedForm.enforceClockOut, onChange: function(e) { setSchedForm(Object.assign({}, schedForm, { enforceClockOut: e.target.checked })); } }),
|
|
2491
|
+
'Enforce Clock-Out'
|
|
2492
|
+
),
|
|
2493
|
+
h('label', { style: { display: 'flex', alignItems: 'center', gap: 6, cursor: 'pointer', fontSize: 13 } },
|
|
2494
|
+
h('input', { type: 'checkbox', checked: schedForm.autoWakeEnabled, onChange: function(e) { setSchedForm(Object.assign({}, schedForm, { autoWakeEnabled: e.target.checked })); } }),
|
|
2495
|
+
'Auto-Wake'
|
|
2496
|
+
),
|
|
2497
|
+
h('label', { style: { display: 'flex', alignItems: 'center', gap: 6, cursor: 'pointer', fontSize: 13 } },
|
|
2498
|
+
h('input', { type: 'checkbox', checked: schedForm.enabled, onChange: function(e) { setSchedForm(Object.assign({}, schedForm, { enabled: e.target.checked })); } }),
|
|
2499
|
+
'Enabled'
|
|
2500
|
+
)
|
|
2501
|
+
),
|
|
2502
|
+
// Off-hours + grace
|
|
2503
|
+
h('div', { style: { display: 'flex', gap: 12, marginTop: 12 } },
|
|
2504
|
+
h('div', { className: 'form-group', style: { flex: 1 } },
|
|
2505
|
+
h('label', { className: 'form-label' }, 'Off-Hours Action'),
|
|
2506
|
+
h('select', { className: 'input', value: schedForm.offHoursAction, onChange: function(e) { setSchedForm(Object.assign({}, schedForm, { offHoursAction: e.target.value })); } },
|
|
2507
|
+
h('option', { value: 'pause' }, 'Pause'), h('option', { value: 'stop' }, 'Stop'), h('option', { value: 'queue' }, 'Queue'))
|
|
2508
|
+
),
|
|
2509
|
+
h('div', { className: 'form-group', style: { flex: 1 } },
|
|
2510
|
+
h('label', { className: 'form-label' }, 'Grace Period (min)'),
|
|
2511
|
+
h('input', { className: 'input', type: 'number', value: schedForm.gracePeriodMinutes, onChange: function(e) { setSchedForm(Object.assign({}, schedForm, { gracePeriodMinutes: parseInt(e.target.value) || 0 })); } })
|
|
2346
2512
|
)
|
|
2513
|
+
),
|
|
2514
|
+
// Actions
|
|
2515
|
+
h('div', { style: { display: 'flex', gap: 8, marginTop: 16, justifyContent: 'space-between' } },
|
|
2516
|
+
h('div', { style: { display: 'flex', gap: 8 } },
|
|
2517
|
+
h('button', { className: 'btn btn-primary', disabled: saving, onClick: saveSchedule }, saving ? 'Saving...' : 'Save Schedule'),
|
|
2518
|
+
h('button', { className: 'btn btn-ghost', onClick: function() { setEditing(false); } }, 'Cancel')
|
|
2519
|
+
),
|
|
2520
|
+
schedule && h('button', { className: 'btn btn-ghost', style: { color: 'var(--danger)' }, onClick: deleteSchedule }, I.x(), ' Remove Schedule')
|
|
2347
2521
|
)
|
|
2348
2522
|
)
|
|
2349
|
-
:
|
|
2523
|
+
: schedule
|
|
2524
|
+
? h('div', { style: { display: 'grid', gridTemplateColumns: '1fr 1fr', gap: 16 } },
|
|
2525
|
+
h('div', null,
|
|
2526
|
+
h('div', { style: { fontSize: 12, color: 'var(--text-muted)', marginBottom: 4 } }, 'Schedule Type'),
|
|
2527
|
+
h('div', { style: { fontSize: 14, fontWeight: 600 } }, (schedule.scheduleType || 'standard').charAt(0).toUpperCase() + (schedule.scheduleType || 'standard').slice(1))
|
|
2528
|
+
),
|
|
2529
|
+
h('div', null,
|
|
2530
|
+
h('div', { style: { fontSize: 12, color: 'var(--text-muted)', marginBottom: 4 } }, 'Hours'),
|
|
2531
|
+
h('div', { style: { fontSize: 14, fontWeight: 600 } }, formatHours(schedule))
|
|
2532
|
+
),
|
|
2533
|
+
h('div', null,
|
|
2534
|
+
h('div', { style: { fontSize: 12, color: 'var(--text-muted)', marginBottom: 4 } }, 'Timezone'),
|
|
2535
|
+
h('div', { style: { fontSize: 14, fontWeight: 600 } }, schedule.timezone || 'UTC')
|
|
2536
|
+
),
|
|
2537
|
+
h('div', null,
|
|
2538
|
+
h('div', { style: { fontSize: 12, color: 'var(--text-muted)', marginBottom: 4 } }, 'Days'),
|
|
2539
|
+
h('div', { style: { fontSize: 14, fontWeight: 600 } }, formatDays(schedule.config?.standardHours?.daysOfWeek))
|
|
2540
|
+
),
|
|
2541
|
+
h('div', null,
|
|
2542
|
+
h('div', { style: { fontSize: 12, color: 'var(--text-muted)', marginBottom: 4 } }, 'Enforcement'),
|
|
2543
|
+
h('div', { style: { display: 'flex', gap: 6, flexWrap: 'wrap' } },
|
|
2544
|
+
h('span', { className: schedule.enforceClockIn ? 'badge badge-success' : 'badge badge-neutral' }, schedule.enforceClockIn ? 'Clock-In Enforced' : 'Clock-In Flexible'),
|
|
2545
|
+
h('span', { className: schedule.enforceClockOut ? 'badge badge-success' : 'badge badge-neutral' }, schedule.enforceClockOut ? 'Clock-Out Enforced' : 'Clock-Out Flexible')
|
|
2546
|
+
)
|
|
2547
|
+
),
|
|
2548
|
+
h('div', null,
|
|
2549
|
+
h('div', { style: { fontSize: 12, color: 'var(--text-muted)', marginBottom: 4 } }, 'Off-Hours'),
|
|
2550
|
+
h('div', { style: { fontSize: 14, fontWeight: 600 } }, (schedule.offHoursAction || 'pause').charAt(0).toUpperCase() + (schedule.offHoursAction || 'pause').slice(1) + (schedule.gracePeriodMinutes ? ' (' + schedule.gracePeriodMinutes + 'min grace)' : ''))
|
|
2551
|
+
),
|
|
2552
|
+
h('div', null,
|
|
2553
|
+
h('div', { style: { fontSize: 12, color: 'var(--text-muted)', marginBottom: 4 } }, 'Status'),
|
|
2554
|
+
h('div', { style: { display: 'flex', gap: 6 } },
|
|
2555
|
+
h('span', { className: schedule.enabled ? 'badge badge-success' : 'badge badge-neutral' }, schedule.enabled ? 'Enabled' : 'Disabled'),
|
|
2556
|
+
schedule.autoWakeEnabled && h('span', { className: 'badge badge-info' }, 'Auto-Wake')
|
|
2557
|
+
)
|
|
2558
|
+
)
|
|
2559
|
+
)
|
|
2560
|
+
: h('div', { style: { textAlign: 'center', padding: 20, color: 'var(--text-muted)', fontSize: 13 } },
|
|
2561
|
+
'No schedule configured. ',
|
|
2562
|
+
h('button', { className: 'btn btn-ghost btn-sm', onClick: startEdit }, 'Configure now')
|
|
2563
|
+
)
|
|
2350
2564
|
)
|
|
2351
2565
|
),
|
|
2352
2566
|
|