@kengcom202/kcore-ui 0.2.0 → 0.4.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -1,6 +1,10 @@
1
1
  import * as i0 from "@angular/core";
2
- import { ApplicationRef, CSP_NONCE, Component, DOCUMENT, DestroyRef, Directive, ElementRef, EnvironmentInjector, NgZone, ViewChild, ViewEncapsulation, computed, createComponent, forwardRef, inject, input, signal } from "@angular/core";
3
- import { NG_VALUE_ACCESSOR } from "@angular/forms";
2
+ import { ApplicationRef, CSP_NONCE, Component, DOCUMENT, DestroyRef, Directive, ElementRef, EnvironmentInjector, Injectable, InjectionToken, NgZone, ViewChild, ViewEncapsulation, computed, createComponent, forwardRef, inject, input, makeEnvironmentProviders, provideEnvironmentInitializer, signal } from "@angular/core";
3
+ import * as i1 from "@angular/forms";
4
+ import { FormBuilder, FormsModule, NG_VALUE_ACCESSOR, ReactiveFormsModule, Validators } from "@angular/forms";
5
+ import { HttpClient, HttpErrorResponse } from "@angular/common/http";
6
+ import { ActivatedRoute, Router, RouterLink } from "@angular/router";
7
+ import { catchError, throwError } from "rxjs";
4
8
  var AnchoredPopup = class {
5
9
  anchor;
6
10
  component;
@@ -355,7 +359,7 @@ i0.ɵɵngDeclareClassMetadata({
355
359
  }]
356
360
  }]
357
361
  });
358
- const STYLE_ID = "kcore-picker-styles";
362
+ const STYLE_ID$1 = "kcore-picker-styles";
359
363
  const PICKER_CSS = `
360
364
  .app-datepicker {
361
365
  cursor: pointer;
@@ -371,9 +375,9 @@ const PICKER_CSS = `
371
375
  `;
372
376
  function ensurePickerStyles() {
373
377
  const doc = inject(DOCUMENT);
374
- if (!doc.head || doc.getElementById(STYLE_ID)) return;
378
+ if (!doc.head || doc.getElementById(STYLE_ID$1)) return;
375
379
  const style = doc.createElement("style");
376
- style.id = STYLE_ID;
380
+ style.id = STYLE_ID$1;
377
381
  const nonce = inject(CSP_NONCE, { optional: true });
378
382
  if (nonce) style.setAttribute("nonce", nonce);
379
383
  style.textContent = PICKER_CSS;
@@ -1331,6 +1335,2786 @@ i0.ɵɵngDeclareClassMetadata({
1331
1335
  }]
1332
1336
  }
1333
1337
  });
1334
- export { AnchoredPopup, ClockPickerDirective, ClockPickerPopupComponent, DatePickerDirective, DatePickerPopupComponent, PICKER_CSS, SearchSelectComponent, ThaiDatePickerComponent, ensurePickerStyles, formatDate, parseDate, toBuddhistText, toChristianText };
1338
+ const PUA_MAP = {
1339
+ "": "ฐ",
1340
+ "": "ิ",
1341
+ "": "ี",
1342
+ "": "ึ",
1343
+ "": "ื",
1344
+ "": "่",
1345
+ "": "้",
1346
+ "": "๊",
1347
+ "": "๋",
1348
+ "": "์",
1349
+ "": "่",
1350
+ "": "้",
1351
+ "": "๊",
1352
+ "": "๋",
1353
+ "": "์",
1354
+ "": "ญ",
1355
+ "": "ั",
1356
+ "": "ํ",
1357
+ "": "็",
1358
+ "": "่",
1359
+ "": "้",
1360
+ "": "๊",
1361
+ "": "๋",
1362
+ "": "์",
1363
+ "": "ุ",
1364
+ "": "ู",
1365
+ "": "ฺ"
1366
+ };
1367
+ function fixThaiText(text) {
1368
+ if (!text) return text;
1369
+ return text.replace(/[-]/g, (c) => PUA_MAP[c] ?? c).replace(/[  ]+(?=[ัิ-ฺ็-๎])/g, "").replace(/ํา/g, "ำ");
1370
+ }
1371
+ function provideThaiPasteFix() {
1372
+ return provideEnvironmentInitializer(() => {
1373
+ const doc = inject(DOCUMENT);
1374
+ const listener = (e) => onPaste(doc, e);
1375
+ doc.addEventListener("paste", listener, true);
1376
+ inject(DestroyRef).onDestroy(() => doc.removeEventListener("paste", listener, true));
1377
+ });
1378
+ }
1379
+ function onPaste(doc, e) {
1380
+ const el = e.target;
1381
+ const view = doc.defaultView;
1382
+ if (!view || !(el instanceof view.HTMLInputElement || el instanceof view.HTMLTextAreaElement)) return;
1383
+ if (el.readOnly || el.disabled) return;
1384
+ const text = e.clipboardData?.getData("text/plain");
1385
+ if (!text) return;
1386
+ const fixed = fixThaiText(text);
1387
+ if (fixed === text) return;
1388
+ e.preventDefault();
1389
+ if (doc.execCommand?.("insertText", false, fixed)) return;
1390
+ if (el.selectionStart === null || el.selectionEnd === null) el.value += fixed;
1391
+ else el.setRangeText(fixed, el.selectionStart, el.selectionEnd, "end");
1392
+ el.dispatchEvent(new Event("input", { bubbles: true }));
1393
+ }
1394
+ const KC_AUTH_CONFIG = new InjectionToken("KC_AUTH_CONFIG");
1395
+ const KC_AUTH_PATHS = {
1396
+ login: "/login",
1397
+ register: "/register",
1398
+ ssoCallback: "/sso-callback"
1399
+ };
1400
+ function resolveKcAuthConfig(o) {
1401
+ return {
1402
+ appName: o.appName,
1403
+ logoUrl: o.logoUrl ?? "",
1404
+ apiBase: (o.apiBase ?? "/api").replace(/\/+$/, ""),
1405
+ homeUrl: o.homeUrl || "/",
1406
+ sso: o.sso ? { label: o.sso.label || "SSO" } : null,
1407
+ passwordLogin: o.passwordLogin !== false || !o.sso,
1408
+ register: o.register ?? "none",
1409
+ orgLabel: o.orgLabel || "หน่วยงาน",
1410
+ adminRoles: o.adminRoles ?? ["admin"],
1411
+ passwordMinLength: o.passwordMinLength ?? 4,
1412
+ kclluHints: o.kclluHints ?? {}
1413
+ };
1414
+ }
1415
+ function provideKcAuth(options) {
1416
+ return makeEnvironmentProviders([{
1417
+ provide: KC_AUTH_CONFIG,
1418
+ useValue: resolveKcAuthConfig(options)
1419
+ }]);
1420
+ }
1421
+ function kcErrorMessage(err, fallback) {
1422
+ if (err instanceof HttpErrorResponse) {
1423
+ const m = err.error?.message;
1424
+ if (typeof m === "string" && m.trim()) return m;
1425
+ if (err.status === 0) return "ไม่สามารถเชื่อมต่อเซิร์ฟเวอร์ได้ กรุณาลองใหม่อีกครั้ง";
1426
+ }
1427
+ return fallback;
1428
+ }
1429
+ const SESSION_KEY = "kc_session";
1430
+ const KC_SSO_STATE_KEY = "kc_sso_state";
1431
+ const KC_SSO_TICKET_KEY = "kc_sso_ticket";
1432
+ const KC_RETURN_URL_KEY = "kc_return_url";
1433
+ function kcTokenExpired(token, now = Date.now()) {
1434
+ const part = token.split(".")[1];
1435
+ if (!part) return false;
1436
+ try {
1437
+ const b64 = part.replace(/-/g, "+").replace(/_/g, "/");
1438
+ const payload = JSON.parse(atob(b64.padEnd(Math.ceil(b64.length / 4) * 4, "=")));
1439
+ return typeof payload?.exp === "number" && payload.exp * 1e3 <= now;
1440
+ } catch {
1441
+ return false;
1442
+ }
1443
+ }
1444
+ var KcAuthService = class KcAuthService {
1445
+ config = inject(KC_AUTH_CONFIG);
1446
+ router = inject(Router);
1447
+ win = inject(DOCUMENT).defaultView;
1448
+ state = signal(this.read(), ...ngDevMode ? [{ debugName: "state" }] : /* istanbul ignore next */ []);
1449
+ session = this.state.asReadonly();
1450
+ user = computed(() => this.state()?.user ?? null, ...ngDevMode ? [{ debugName: "user" }] : /* istanbul ignore next */ []);
1451
+ isAdmin = computed(() => {
1452
+ const u = this.user();
1453
+ return !!u && this.config.adminRoles.includes(u.role);
1454
+ }, ...ngDevMode ? [{ debugName: "isAdmin" }] : /* istanbul ignore next */ []);
1455
+ constructor() {
1456
+ const onStorage = (e) => {
1457
+ if (e.key === SESSION_KEY || e.key === null) this.state.set(this.read());
1458
+ };
1459
+ this.win?.addEventListener("storage", onStorage);
1460
+ inject(DestroyRef).onDestroy(() => this.win?.removeEventListener("storage", onStorage));
1461
+ }
1462
+ isLoggedIn() {
1463
+ const s = this.state();
1464
+ return !!s && !kcTokenExpired(s.token);
1465
+ }
1466
+ token() {
1467
+ return this.state()?.token ?? "";
1468
+ }
1469
+ start(session) {
1470
+ this.clear();
1471
+ this.state.set({
1472
+ token: session.token,
1473
+ user: { ...session.user }
1474
+ });
1475
+ this.storage("localStorage")?.setItem(SESSION_KEY, JSON.stringify(this.state()));
1476
+ }
1477
+ updateUser(patch) {
1478
+ const s = this.state();
1479
+ if (!s) return;
1480
+ this.state.set({
1481
+ token: s.token,
1482
+ user: {
1483
+ ...s.user,
1484
+ ...patch
1485
+ }
1486
+ });
1487
+ this.storage("localStorage")?.setItem(SESSION_KEY, JSON.stringify(this.state()));
1488
+ }
1489
+ clear() {
1490
+ this.state.set(null);
1491
+ this.storage("localStorage")?.removeItem(SESSION_KEY);
1492
+ for (const k of [
1493
+ KC_SSO_STATE_KEY,
1494
+ KC_SSO_TICKET_KEY,
1495
+ KC_RETURN_URL_KEY
1496
+ ]) this.storage("sessionStorage")?.removeItem(k);
1497
+ }
1498
+ logout() {
1499
+ this.clear();
1500
+ return this.router.navigateByUrl(KC_AUTH_PATHS.login);
1501
+ }
1502
+ safeReturnUrl(url) {
1503
+ if (!url || !url.startsWith("/") || url.startsWith("//") || url.includes("\\")) return this.config.homeUrl;
1504
+ const path = url.split(/[?#]/)[0];
1505
+ return Object.values(KC_AUTH_PATHS).includes(path) ? this.config.homeUrl : url;
1506
+ }
1507
+ setTemp(key, value) {
1508
+ this.storage("sessionStorage")?.setItem(key, value);
1509
+ }
1510
+ getTemp(key) {
1511
+ return this.storage("sessionStorage")?.getItem(key) ?? "";
1512
+ }
1513
+ takeTemp(key) {
1514
+ const v = this.getTemp(key);
1515
+ this.storage("sessionStorage")?.removeItem(key);
1516
+ return v;
1517
+ }
1518
+ redirect(url) {
1519
+ this.win?.location.assign(url);
1520
+ }
1521
+ read() {
1522
+ try {
1523
+ const s = JSON.parse(this.storage("localStorage")?.getItem(SESSION_KEY) ?? "null");
1524
+ return s && typeof s.token === "string" && s.user && typeof s.user === "object" ? s : null;
1525
+ } catch {
1526
+ return null;
1527
+ }
1528
+ }
1529
+ storage(kind) {
1530
+ try {
1531
+ return this.win?.[kind] ?? null;
1532
+ } catch {
1533
+ return null;
1534
+ }
1535
+ }
1536
+ static ɵfac = i0.ɵɵngDeclareFactory({
1537
+ minVersion: "12.0.0",
1538
+ version: "22.2.1",
1539
+ ngImport: i0,
1540
+ type: KcAuthService,
1541
+ deps: [],
1542
+ target: i0.ɵɵFactoryTarget.Injectable
1543
+ });
1544
+ static ɵprov = i0.ɵɵngDeclareInjectable({
1545
+ minVersion: "12.0.0",
1546
+ version: "22.2.1",
1547
+ ngImport: i0,
1548
+ type: KcAuthService,
1549
+ providedIn: "root"
1550
+ });
1551
+ };
1552
+ i0.ɵɵngDeclareClassMetadata({
1553
+ minVersion: "12.0.0",
1554
+ version: "22.2.1",
1555
+ ngImport: i0,
1556
+ type: KcAuthService,
1557
+ decorators: [{
1558
+ type: Injectable,
1559
+ args: [{ providedIn: "root" }]
1560
+ }],
1561
+ ctorParameters: () => []
1562
+ });
1563
+ function loginTree(state) {
1564
+ const expired = !!inject(KcAuthService).session();
1565
+ return inject(Router).createUrlTree([KC_AUTH_PATHS.login], { queryParams: {
1566
+ returnUrl: state.url,
1567
+ ...expired ? { expired: 1 } : {}
1568
+ } });
1569
+ }
1570
+ const kcAuthGuard = (_route, state) => inject(KcAuthService).isLoggedIn() || loginTree(state);
1571
+ const kcAdminGuard = (_route, state) => {
1572
+ const auth = inject(KcAuthService);
1573
+ if (!auth.isLoggedIn()) return loginTree(state);
1574
+ return auth.isAdmin() || inject(Router).parseUrl(inject(KC_AUTH_CONFIG).homeUrl);
1575
+ };
1576
+ function onAnonymousPage(url) {
1577
+ const path = url.split(/[?#]/)[0];
1578
+ return Object.values(KC_AUTH_PATHS).includes(path);
1579
+ }
1580
+ const kcAuthInterceptor = (req, next) => {
1581
+ const auth = inject(KcAuthService);
1582
+ const router = inject(Router);
1583
+ const { apiBase } = inject(KC_AUTH_CONFIG);
1584
+ const isApi = req.url === apiBase || req.url.startsWith(apiBase + "/");
1585
+ const token = auth.token();
1586
+ return next(isApi && token && !req.headers.has("Authorization") ? req.clone({ setHeaders: { Authorization: `Bearer ${token}` } }) : req).pipe(catchError((err) => {
1587
+ if (isApi && err instanceof HttpErrorResponse && err.status === 401 && !onAnonymousPage(router.url)) {
1588
+ const returnUrl = router.url;
1589
+ auth.clear();
1590
+ router.navigate([KC_AUTH_PATHS.login], { queryParams: {
1591
+ expired: 1,
1592
+ returnUrl
1593
+ } });
1594
+ }
1595
+ return throwError(() => err);
1596
+ }));
1597
+ };
1598
+ const STYLE_ID = "kcore-auth-styles";
1599
+ const AUTH_CSS = `
1600
+ .kc-auth-screen, .kc-auth-page, .kc-auth-modal-bg {
1601
+ font-size: var(--kc-auth-font-size, 14px);
1602
+ color: var(--kc-auth-text, #3a2a3a);
1603
+ line-height: 1.5;
1604
+ }
1605
+ .kc-auth-screen *, .kc-auth-page *, .kc-auth-modal-bg * { box-sizing: border-box; }
1606
+
1607
+ /* ---- หน้าเต็มจอ (login / register / sso-callback) ---- */
1608
+ .kc-auth-screen {
1609
+ min-height: 100vh;
1610
+ display: flex;
1611
+ align-items: center;
1612
+ justify-content: center;
1613
+ padding: 24px 16px;
1614
+ background: var(--kc-auth-page-bg, linear-gradient(160deg, #fff7ff 0%, #ffe6ff 100%));
1615
+ }
1616
+ .kc-auth-card {
1617
+ width: 100%;
1618
+ max-width: 380px;
1619
+ padding: 28px 28px 24px;
1620
+ background: var(--kc-auth-bg, #fff);
1621
+ border: 1px solid var(--kc-auth-border, #f3e1f3);
1622
+ border-radius: var(--kc-auth-radius, 14px);
1623
+ box-shadow: 0 12px 36px rgba(204, 102, 204, 0.12);
1624
+ }
1625
+ .kc-auth-card.wide { max-width: 560px; }
1626
+ .kc-auth-card.center { text-align: center; }
1627
+ .kc-auth-logo { display: block; max-height: 72px; max-width: 100%; margin: 0 auto 12px; }
1628
+ .kc-auth-title { margin: 0; font-size: 1.35em; font-weight: 600; text-align: center; }
1629
+ .kc-auth-subtitle { margin: 4px 0 20px; color: var(--kc-auth-muted, #806f80); text-align: center; }
1630
+ .kc-auth-footer { margin: 18px 0 0; text-align: center; color: var(--kc-auth-muted, #806f80); font-size: 0.93em; }
1631
+
1632
+ /* ---- หน้าในแอป (ข้อมูลส่วนตัว / config) ---- */
1633
+ .kc-auth-page { display: block; max-width: var(--kc-auth-page-width, 1100px); margin: 0 auto; }
1634
+ .kc-auth-page.narrow { max-width: 640px; }
1635
+ .kc-auth-page.single { max-width: 460px; }
1636
+ .kc-auth-page.single .kc-auth-page-title { margin-bottom: 16px; }
1637
+ .kc-auth-page-header {
1638
+ display: flex;
1639
+ flex-wrap: wrap;
1640
+ align-items: flex-end;
1641
+ justify-content: space-between;
1642
+ gap: 12px;
1643
+ margin-bottom: 14px;
1644
+ }
1645
+ .kc-auth-page-title { margin: 0; font-size: 1.3em; font-weight: 600; }
1646
+ .kc-auth-page-subtitle { margin: 2px 0 0; color: var(--kc-auth-muted, #806f80); }
1647
+ .kc-auth-panel {
1648
+ position: relative;
1649
+ margin-bottom: 14px;
1650
+ padding: 18px;
1651
+ background: var(--kc-auth-bg, #fff);
1652
+ border: 1px solid var(--kc-auth-border, #f3e1f3);
1653
+ border-radius: var(--kc-auth-radius, 14px);
1654
+ }
1655
+ .kc-auth-panel.flush { padding: 0; overflow: hidden; }
1656
+ .kc-auth-panel-title { margin: 0 0 12px; font-size: 1.05em; font-weight: 600; }
1657
+
1658
+ /* ---- ฟอร์ม ---- */
1659
+ .kc-auth-grid { display: grid; grid-template-columns: repeat(2, minmax(0, 1fr)); gap: 0 14px; }
1660
+ .kc-auth-grid > .full { grid-column: 1 / -1; }
1661
+ .kc-auth-field { display: flex; flex-direction: column; gap: 4px; margin-bottom: 12px; min-width: 0; }
1662
+ .kc-auth-field > span, .kc-auth-label { font-size: 0.93em; font-weight: 500; }
1663
+ .kc-auth-field .req { color: var(--kc-auth-danger, #c62828); }
1664
+ .kc-auth-input {
1665
+ width: 100%;
1666
+ height: var(--kc-auth-input-height, 40px);
1667
+ margin: 0;
1668
+ padding: 0 10px;
1669
+ font: inherit;
1670
+ color: inherit;
1671
+ background: var(--kc-auth-bg, #fff);
1672
+ border: 1px solid var(--kc-auth-input-border, #e9cde9);
1673
+ border-radius: 8px;
1674
+ outline: none;
1675
+ transition: border-color 0.15s, box-shadow 0.15s;
1676
+ }
1677
+ .kc-auth-input:focus {
1678
+ border-color: var(--kc-auth-accent, #a23ea2);
1679
+ box-shadow: 0 0 0 3px var(--kc-auth-accent-soft, rgba(230, 140, 230, 0.22));
1680
+ }
1681
+ .kc-auth-input:disabled, .kc-auth-input[readonly] {
1682
+ color: var(--kc-auth-muted, #806f80);
1683
+ background: var(--kc-auth-disabled-bg, #f8f1f8);
1684
+ }
1685
+ /* ช่องรหัสผ่าน + ปุ่มดวงตา (<kc-password>) */
1686
+ .kc-auth-pw { position: relative; display: block; }
1687
+ .kc-auth-pw > .kc-auth-input { padding-right: 42px; }
1688
+ .kc-auth-pw > input::-ms-reveal, .kc-auth-pw > input::-ms-clear { display: none; }
1689
+ .kc-auth-pw-toggle {
1690
+ position: absolute;
1691
+ top: 50%;
1692
+ right: 5px;
1693
+ display: flex;
1694
+ align-items: center;
1695
+ justify-content: center;
1696
+ width: 32px;
1697
+ height: 32px;
1698
+ padding: 0;
1699
+ color: var(--kc-auth-muted, #806f80);
1700
+ background: none;
1701
+ border: 0;
1702
+ border-radius: 6px;
1703
+ cursor: pointer;
1704
+ transform: translateY(-50%);
1705
+ }
1706
+ .kc-auth-pw-toggle:hover, .kc-auth-pw-toggle[aria-pressed="true"] { color: var(--kc-auth-accent, #a23ea2); }
1707
+ .kc-auth-pw-toggle:focus-visible { outline: 2px solid var(--kc-auth-accent, #a23ea2); outline-offset: 1px; }
1708
+ .kc-auth-pw-toggle svg { width: 19px; height: 19px; }
1709
+ .kc-auth-input.invalid { border-color: var(--kc-auth-danger, #c62828); }
1710
+ .kc-auth-field .kc-ss {
1711
+ --kc-ss-height: var(--kc-auth-input-height, 40px);
1712
+ --kc-ss-radius: 8px;
1713
+ --kc-ss-accent: var(--kc-auth-accent, #a23ea2);
1714
+ --kc-ss-accent-soft: var(--kc-auth-accent-soft, rgba(230, 140, 230, 0.22));
1715
+ --kc-ss-border: var(--kc-auth-input-border, #e9cde9);
1716
+ --kc-ss-text: var(--kc-auth-text, #3a2a3a);
1717
+ }
1718
+ .kc-auth-value { padding: 8px 0; }
1719
+ .kc-auth-field-error { color: var(--kc-auth-danger, #c62828); font-size: 0.86em; }
1720
+ .kc-auth-hint { margin: 2px 0 0; color: var(--kc-auth-muted, #806f80); font-size: 0.86em; }
1721
+ .kc-auth-check { display: inline-flex; align-items: center; gap: 6px; cursor: pointer; user-select: none; }
1722
+
1723
+ /* ---- ปุ่ม ---- */
1724
+ .kc-auth-actions { display: flex; flex-wrap: wrap; justify-content: flex-end; gap: 8px; margin-top: 6px; }
1725
+ .kc-auth-actions.center { justify-content: center; }
1726
+ .kc-auth-actions.center > .kc-auth-btn { min-width: 120px; }
1727
+ .kc-auth-btn {
1728
+ display: inline-flex;
1729
+ align-items: center;
1730
+ justify-content: center;
1731
+ gap: 6px;
1732
+ height: var(--kc-auth-input-height, 40px);
1733
+ padding: 0 16px;
1734
+ font: inherit;
1735
+ font-weight: 500;
1736
+ color: var(--kc-auth-text, #3a2a3a);
1737
+ background: var(--kc-auth-bg, #fff);
1738
+ border: 1px solid var(--kc-auth-input-border, #e9cde9);
1739
+ border-radius: 8px;
1740
+ cursor: pointer;
1741
+ text-decoration: none;
1742
+ white-space: nowrap;
1743
+ transition: filter 0.15s, background 0.15s;
1744
+ }
1745
+ .kc-auth-btn:hover:not(:disabled) { filter: brightness(0.96); }
1746
+ .kc-auth-btn:disabled, .kc-auth-btn.disabled { opacity: 0.6; cursor: not-allowed; pointer-events: none; }
1747
+ .kc-auth-btn.primary {
1748
+ color: var(--kc-auth-primary-text, #6b2c6b);
1749
+ background: var(--kc-auth-primary-bg, #ffccff);
1750
+ border-color: var(--kc-auth-primary-border, #f0aef0);
1751
+ font-weight: 600;
1752
+ box-shadow: 0 4px 12px rgba(230, 140, 230, 0.25);
1753
+ }
1754
+ .kc-auth-btn.outline {
1755
+ color: var(--kc-auth-accent, #a23ea2);
1756
+ background: var(--kc-auth-accent-tint, #fff);
1757
+ border-color: var(--kc-auth-accent-border, #f0c2f0);
1758
+ }
1759
+ /* ปุ่มเข้าสู่ระบบ + SSO บรรทัดเดียวกัน แบ่งเท่ากัน */
1760
+ .kc-auth-btn-row { display: flex; gap: 10px; margin-top: 6px; }
1761
+ .kc-auth-btn-row > .kc-auth-btn { flex: 1 1 0; min-width: 0; overflow: hidden; text-overflow: ellipsis; }
1762
+ /* ปิดรหัสผ่าน: ปุ่ม SSO ปุ่มเดียวตรงกลาง */
1763
+ .kc-auth-sso-only { display: flex; justify-content: center; padding: 8px 0 4px; }
1764
+ .kc-auth-sso-only > .kc-auth-btn { min-width: 75%; height: 46px; padding: 0 24px; font-size: 1.05em; }
1765
+ .kc-auth-btn.block { display: flex; width: 100%; }
1766
+ .kc-auth-btn.sm { height: 30px; padding: 0 10px; font-size: 0.93em; }
1767
+ .kc-auth-link { color: var(--kc-auth-accent, #a23ea2); text-decoration: none; cursor: pointer; }
1768
+ .kc-auth-link:hover { text-decoration: underline; }
1769
+
1770
+ /* ---- ข้อความแจ้ง ---- */
1771
+ .kc-auth-alert {
1772
+ margin-bottom: 14px;
1773
+ padding: 10px 12px;
1774
+ border-radius: 6px;
1775
+ border: 1px solid transparent;
1776
+ font-size: 0.93em;
1777
+ }
1778
+ .kc-auth-alert.error { color: #8a1c1c; background: #fdecea; border-color: #f5c2c0; }
1779
+ .kc-auth-alert.success { color: #1e5b2a; background: #e8f5e9; border-color: #b9dfbf; }
1780
+ .kc-auth-alert.warn { color: #6d4c1f; background: #fff8e6; border-color: #f3dfb0; }
1781
+ .kc-auth-alert.info { color: #6b2c6b; background: #fff0ff; border-color: #f5d0f5; }
1782
+ .kc-auth-loading {
1783
+ position: absolute;
1784
+ inset: 0;
1785
+ z-index: 2;
1786
+ display: flex;
1787
+ align-items: center;
1788
+ justify-content: center;
1789
+ color: var(--kc-auth-muted, #806f80);
1790
+ background: rgba(255, 255, 255, 0.7);
1791
+ }
1792
+
1793
+ /* ---- ตาราง (จอแคบเรียงเป็นการ์ด) ---- */
1794
+ .kc-auth-table-wrap { overflow-x: auto; }
1795
+ .kc-auth-table { width: 100%; border-collapse: collapse; }
1796
+ .kc-auth-table th, .kc-auth-table td {
1797
+ padding: 9px 12px;
1798
+ text-align: left;
1799
+ vertical-align: top;
1800
+ border-bottom: 1px solid var(--kc-auth-border, #f3e1f3);
1801
+ }
1802
+ .kc-auth-table th {
1803
+ font-size: 0.9em;
1804
+ font-weight: 600;
1805
+ color: var(--kc-auth-muted, #806f80);
1806
+ background: var(--kc-auth-head-bg, #fdf5fd);
1807
+ white-space: nowrap;
1808
+ }
1809
+ .kc-auth-table tbody tr.clickable { cursor: pointer; }
1810
+ .kc-auth-table tbody tr.clickable:hover { background: var(--kc-auth-row-hover, #fff0ff); }
1811
+ .kc-auth-table tbody tr.inactive { opacity: 0.55; }
1812
+ .kc-auth-table .c { text-align: center; }
1813
+ .kc-auth-table .empty { padding: 24px; text-align: center; color: var(--kc-auth-muted, #806f80); }
1814
+ .kc-auth-badge {
1815
+ display: inline-block;
1816
+ padding: 1px 10px;
1817
+ border-radius: 999px;
1818
+ font-size: 0.86em;
1819
+ white-space: nowrap;
1820
+ }
1821
+ .kc-auth-badge.on { color: #1e5b2a; background: #e8f5e9; }
1822
+ .kc-auth-badge.off { color: #806f80; background: #eceff1; }
1823
+
1824
+ /* ---- popup แก้ไข ---- */
1825
+ .kc-auth-modal-bg {
1826
+ position: fixed;
1827
+ inset: 0;
1828
+ z-index: var(--kc-auth-z-index, 9000);
1829
+ display: flex;
1830
+ align-items: flex-start;
1831
+ justify-content: center;
1832
+ padding: 6vh 16px 16px;
1833
+ overflow-y: auto;
1834
+ background: rgba(70, 30, 70, 0.30);
1835
+ }
1836
+ .kc-auth-modal {
1837
+ width: 100%;
1838
+ max-width: 620px;
1839
+ background: var(--kc-auth-bg, #fff);
1840
+ border-radius: var(--kc-auth-radius, 14px);
1841
+ box-shadow: 0 20px 50px rgba(120, 50, 120, 0.20);
1842
+ }
1843
+ .kc-auth-modal-header {
1844
+ display: flex;
1845
+ align-items: center;
1846
+ justify-content: space-between;
1847
+ gap: 10px;
1848
+ padding: 14px 18px;
1849
+ border-bottom: 1px solid var(--kc-auth-border, #f3e1f3);
1850
+ font-weight: 600;
1851
+ }
1852
+ .kc-auth-modal-body { padding: 16px 18px 18px; }
1853
+ .kc-auth-close {
1854
+ width: 30px;
1855
+ height: 30px;
1856
+ padding: 0;
1857
+ font-size: 20px;
1858
+ line-height: 1;
1859
+ color: var(--kc-auth-muted, #806f80);
1860
+ background: none;
1861
+ border: 0;
1862
+ border-radius: 6px;
1863
+ cursor: pointer;
1864
+ }
1865
+ .kc-auth-close:hover { background: var(--kc-auth-disabled-bg, #f8f1f8); }
1866
+
1867
+ @media (max-width: 640px) {
1868
+ .kc-auth-grid { grid-template-columns: minmax(0, 1fr); }
1869
+ .kc-auth-card { padding: 22px 18px 18px; }
1870
+ .kc-auth-table.stack thead { display: none; }
1871
+ .kc-auth-table.stack tr { display: block; padding: 8px 0; border-bottom: 1px solid var(--kc-auth-border, #f3e1f3); }
1872
+ .kc-auth-table.stack td { display: flex; gap: 10px; padding: 3px 12px; border: 0; text-align: left; }
1873
+ .kc-auth-table.stack td[data-label]::before {
1874
+ content: attr(data-label);
1875
+ flex: 0 0 110px;
1876
+ color: var(--kc-auth-muted, #806f80);
1877
+ }
1878
+ .kc-auth-table.stack td.empty { display: block; }
1879
+ }
1880
+ `;
1881
+ function ensureAuthStyles() {
1882
+ const doc = inject(DOCUMENT);
1883
+ if (!doc.head || doc.getElementById(STYLE_ID)) return;
1884
+ const style = doc.createElement("style");
1885
+ style.id = STYLE_ID;
1886
+ const nonce = inject(CSP_NONCE, { optional: true });
1887
+ if (nonce) style.setAttribute("nonce", nonce);
1888
+ style.textContent = AUTH_CSS;
1889
+ doc.head.appendChild(style);
1890
+ }
1891
+ var KcPasswordToggleComponent = class KcPasswordToggleComponent {
1892
+ host = inject(ElementRef);
1893
+ shown = signal(false, ...ngDevMode ? [{ debugName: "shown" }] : /* istanbul ignore next */ []);
1894
+ input = null;
1895
+ constructor() {
1896
+ ensureAuthStyles();
1897
+ }
1898
+ ngAfterContentInit() {
1899
+ this.input = this.host.nativeElement.querySelector("input");
1900
+ }
1901
+ toggle() {
1902
+ if (!this.input) return;
1903
+ this.shown.update((v) => !v);
1904
+ this.input.type = this.shown() ? "text" : "password";
1905
+ }
1906
+ static ɵfac = i0.ɵɵngDeclareFactory({
1907
+ minVersion: "12.0.0",
1908
+ version: "22.2.1",
1909
+ ngImport: i0,
1910
+ type: KcPasswordToggleComponent,
1911
+ deps: [],
1912
+ target: i0.ɵɵFactoryTarget.Component
1913
+ });
1914
+ static ɵcmp = i0.ɵɵngDeclareComponent({
1915
+ minVersion: "17.0.0",
1916
+ version: "22.2.1",
1917
+ type: KcPasswordToggleComponent,
1918
+ isStandalone: true,
1919
+ selector: "kc-password",
1920
+ host: { classAttribute: "kc-auth-pw" },
1921
+ ngImport: i0,
1922
+ template: `
1923
+ <ng-content />
1924
+ <button type="button" class="kc-auth-pw-toggle" [attr.aria-label]="shown() ? 'ซ่อนรหัสผ่าน' : 'แสดงรหัสผ่าน'"
1925
+ [attr.aria-pressed]="shown()" [attr.title]="shown() ? 'ซ่อนรหัสผ่าน' : 'แสดงรหัสผ่าน'"
1926
+ (mousedown)="$event.preventDefault()" (click)="toggle()">
1927
+ @if (shown()) {
1928
+ <!-- ตาปิด (กด = ซ่อน) -->
1929
+ <svg viewBox="0 0 24 24" aria-hidden="true" fill="none" stroke="currentColor" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round">
1930
+ <path d="M3 3l18 18" />
1931
+ <path d="M10.6 5.1A9.8 9.8 0 0 1 12 5c5 0 8.6 4.2 9.7 6.2a1.7 1.7 0 0 1 0 1.6 15 15 0 0 1-2.6 3.4" />
1932
+ <path d="M6.6 6.6A15.4 15.4 0 0 0 2.3 11.2a1.7 1.7 0 0 0 0 1.6C3.4 14.8 7 19 12 19a9.6 9.6 0 0 0 5.4-1.6" />
1933
+ <path d="M9.9 9.9a3 3 0 0 0 4.2 4.2" />
1934
+ </svg>
1935
+ } @else {
1936
+ <!-- ตาเปิด (กด = แสดง) -->
1937
+ <svg viewBox="0 0 24 24" aria-hidden="true" fill="none" stroke="currentColor" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round">
1938
+ <path d="M2.3 12.8a1.7 1.7 0 0 1 0-1.6C3.4 9.2 7 5 12 5s8.6 4.2 9.7 6.2a1.7 1.7 0 0 1 0 1.6C20.6 14.8 17 19 12 19s-8.6-4.2-9.7-6.2z" />
1939
+ <circle cx="12" cy="12" r="3" />
1940
+ </svg>
1941
+ }
1942
+ </button>
1943
+ `,
1944
+ isInline: true
1945
+ });
1946
+ };
1947
+ i0.ɵɵngDeclareClassMetadata({
1948
+ minVersion: "12.0.0",
1949
+ version: "22.2.1",
1950
+ ngImport: i0,
1951
+ type: KcPasswordToggleComponent,
1952
+ decorators: [{
1953
+ type: Component,
1954
+ args: [{
1955
+ selector: "kc-password",
1956
+ standalone: true,
1957
+ host: { class: "kc-auth-pw" },
1958
+ template: `
1959
+ <ng-content />
1960
+ <button type="button" class="kc-auth-pw-toggle" [attr.aria-label]="shown() ? 'ซ่อนรหัสผ่าน' : 'แสดงรหัสผ่าน'"
1961
+ [attr.aria-pressed]="shown()" [attr.title]="shown() ? 'ซ่อนรหัสผ่าน' : 'แสดงรหัสผ่าน'"
1962
+ (mousedown)="$event.preventDefault()" (click)="toggle()">
1963
+ @if (shown()) {
1964
+ <!-- ตาปิด (กด = ซ่อน) -->
1965
+ <svg viewBox="0 0 24 24" aria-hidden="true" fill="none" stroke="currentColor" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round">
1966
+ <path d="M3 3l18 18" />
1967
+ <path d="M10.6 5.1A9.8 9.8 0 0 1 12 5c5 0 8.6 4.2 9.7 6.2a1.7 1.7 0 0 1 0 1.6 15 15 0 0 1-2.6 3.4" />
1968
+ <path d="M6.6 6.6A15.4 15.4 0 0 0 2.3 11.2a1.7 1.7 0 0 0 0 1.6C3.4 14.8 7 19 12 19a9.6 9.6 0 0 0 5.4-1.6" />
1969
+ <path d="M9.9 9.9a3 3 0 0 0 4.2 4.2" />
1970
+ </svg>
1971
+ } @else {
1972
+ <!-- ตาเปิด (กด = แสดง) -->
1973
+ <svg viewBox="0 0 24 24" aria-hidden="true" fill="none" stroke="currentColor" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round">
1974
+ <path d="M2.3 12.8a1.7 1.7 0 0 1 0-1.6C3.4 9.2 7 5 12 5s8.6 4.2 9.7 6.2a1.7 1.7 0 0 1 0 1.6C20.6 14.8 17 19 12 19s-8.6-4.2-9.7-6.2z" />
1975
+ <circle cx="12" cy="12" r="3" />
1976
+ </svg>
1977
+ }
1978
+ </button>
1979
+ `
1980
+ }]
1981
+ }],
1982
+ ctorParameters: () => []
1983
+ });
1984
+ var KcLoginComponent = class KcLoginComponent {
1985
+ config = inject(KC_AUTH_CONFIG);
1986
+ paths = KC_AUTH_PATHS;
1987
+ auth = inject(KcAuthService);
1988
+ http = inject(HttpClient);
1989
+ router = inject(Router);
1990
+ route = inject(ActivatedRoute);
1991
+ busy = signal("", ...ngDevMode ? [{ debugName: "busy" }] : /* istanbul ignore next */ []);
1992
+ message = signal(null, ...ngDevMode ? [{ debugName: "message" }] : /* istanbul ignore next */ []);
1993
+ form = inject(FormBuilder).nonNullable.group({
1994
+ username: ["", Validators.required],
1995
+ password: ["", Validators.required]
1996
+ });
1997
+ constructor() {
1998
+ ensureAuthStyles();
1999
+ }
2000
+ get returnUrl() {
2001
+ return this.auth.safeReturnUrl(this.route.snapshot.queryParamMap.get("returnUrl"));
2002
+ }
2003
+ ngOnInit() {
2004
+ if (this.route.snapshot.queryParamMap.get("expired") === "1") this.message.set({
2005
+ text: "หมดเวลาการใช้งาน กรุณาเข้าสู่ระบบใหม่",
2006
+ error: false
2007
+ });
2008
+ else if (this.auth.isLoggedIn()) this.router.navigateByUrl(this.returnUrl);
2009
+ }
2010
+ submit() {
2011
+ if (this.busy()) return;
2012
+ const v = this.form.getRawValue();
2013
+ if (!v.username.trim() || !v.password) {
2014
+ this.message.set({
2015
+ text: "กรุณากรอกชื่อผู้ใช้และรหัสผ่าน",
2016
+ error: true
2017
+ });
2018
+ return;
2019
+ }
2020
+ this.busy.set("password");
2021
+ this.message.set(null);
2022
+ this.http.post(`${this.config.apiBase}/user/login`, {
2023
+ username: v.username.trim(),
2024
+ password: v.password
2025
+ }).subscribe({
2026
+ next: (res) => {
2027
+ this.auth.start(res);
2028
+ this.router.navigateByUrl(this.returnUrl);
2029
+ },
2030
+ error: (err) => {
2031
+ this.busy.set("");
2032
+ this.message.set({
2033
+ text: kcErrorMessage(err, "เข้าสู่ระบบไม่สำเร็จ"),
2034
+ error: true
2035
+ });
2036
+ }
2037
+ });
2038
+ }
2039
+ startSso() {
2040
+ const sso = this.config.sso;
2041
+ if (!sso || this.busy()) return;
2042
+ this.busy.set("sso");
2043
+ this.message.set(null);
2044
+ this.http.get(`${this.config.apiBase}/user/sso/authorize-url`).subscribe({
2045
+ next: ({ url, state }) => {
2046
+ this.auth.setTemp(KC_SSO_STATE_KEY, state);
2047
+ this.auth.setTemp(KC_RETURN_URL_KEY, this.returnUrl);
2048
+ this.auth.redirect(url);
2049
+ },
2050
+ error: (err) => {
2051
+ this.busy.set("");
2052
+ this.message.set({
2053
+ text: err.status === 404 ? `ระบบยังไม่ได้ตั้งค่าการเข้าสู่ระบบด้วย ${sso.label}` : kcErrorMessage(err, `ไม่สามารถเชื่อมต่อ ${sso.label} ได้ กรุณาลองใหม่อีกครั้ง`),
2054
+ error: true
2055
+ });
2056
+ }
2057
+ });
2058
+ }
2059
+ static ɵfac = i0.ɵɵngDeclareFactory({
2060
+ minVersion: "12.0.0",
2061
+ version: "22.2.1",
2062
+ ngImport: i0,
2063
+ type: KcLoginComponent,
2064
+ deps: [],
2065
+ target: i0.ɵɵFactoryTarget.Component
2066
+ });
2067
+ static ɵcmp = i0.ɵɵngDeclareComponent({
2068
+ minVersion: "17.0.0",
2069
+ version: "22.2.1",
2070
+ type: KcLoginComponent,
2071
+ isStandalone: true,
2072
+ selector: "kc-login",
2073
+ ngImport: i0,
2074
+ template: `
2075
+ <div class="kc-auth-screen">
2076
+ <div class="kc-auth-card">
2077
+ @if (config.logoUrl) {
2078
+ <img class="kc-auth-logo" [src]="config.logoUrl" alt="" />
2079
+ }
2080
+ <h1 class="kc-auth-title">{{ config.appName }}</h1>
2081
+ <p class="kc-auth-subtitle">ลงชื่อเข้าสู่ระบบ</p>
2082
+
2083
+ @if (message(); as m) {
2084
+ <div class="kc-auth-alert" [class.error]="m.error" [class.info]="!m.error" role="alert">{{ m.text }}</div>
2085
+ }
2086
+
2087
+ @if (config.passwordLogin) {
2088
+ <form [formGroup]="form" (ngSubmit)="submit()" novalidate>
2089
+ <label class="kc-auth-field">
2090
+ <span>ชื่อผู้ใช้</span>
2091
+ <input class="kc-auth-input" type="text" formControlName="username" autocomplete="username" />
2092
+ </label>
2093
+ <label class="kc-auth-field">
2094
+ <span>รหัสผ่าน</span>
2095
+ <kc-password><input class="kc-auth-input" type="password" formControlName="password" autocomplete="current-password" /></kc-password>
2096
+ </label>
2097
+ <!-- ปุ่มเข้าสู่ระบบกับ SSO อยู่บรรทัดเดียวกัน แบ่งครึ่ง — ปุ่ม SSO ใช้แค่ชื่อเพื่อให้พอดีครึ่งการ์ด -->
2098
+ <div class="kc-auth-btn-row">
2099
+ <button type="submit" class="kc-auth-btn primary" [disabled]="!!busy()">
2100
+ {{ busy() === 'password' ? 'กำลังเข้าสู่ระบบ...' : 'เข้าสู่ระบบ' }}
2101
+ </button>
2102
+ @if (config.sso; as sso) {
2103
+ <button type="button" class="kc-auth-btn outline" [disabled]="!!busy()" (click)="startSso()"
2104
+ [attr.title]="'เข้าสู่ระบบด้วย ' + sso.label">
2105
+ {{ busy() === 'sso' ? 'กำลังเชื่อมต่อ...' : sso.label }}
2106
+ </button>
2107
+ }
2108
+ </div>
2109
+ </form>
2110
+ } @else if (config.sso; as sso) {
2111
+ <!-- ปิดรหัสผ่าน: มีแต่ปุ่ม SSO ตรงกลาง -->
2112
+ <div class="kc-auth-sso-only">
2113
+ <button type="button" class="kc-auth-btn primary" [disabled]="!!busy()" (click)="startSso()">
2114
+ {{ busy() === 'sso' ? 'กำลังเชื่อมต่อ...' : 'เข้าสู่ระบบด้วย ' + sso.label }}
2115
+ </button>
2116
+ </div>
2117
+ }
2118
+
2119
+ @if (config.register === 'form') {
2120
+ <p class="kc-auth-footer">ยังไม่มีบัญชี? <a class="kc-auth-link" [routerLink]="paths.register">ลงทะเบียน</a></p>
2121
+ } @else if (config.register === 'sso' && config.sso) {
2122
+ <p class="kc-auth-footer">ผู้ใช้ใหม่ กดปุ่ม {{ config.sso.label }} เพื่อลงทะเบียน</p>
2123
+ }
2124
+ <ng-content />
2125
+ </div>
2126
+ </div>
2127
+ `,
2128
+ isInline: true,
2129
+ dependencies: [
2130
+ {
2131
+ kind: "ngmodule",
2132
+ type: ReactiveFormsModule
2133
+ },
2134
+ {
2135
+ kind: "directive",
2136
+ type: i1.ɵNgNoValidate,
2137
+ selector: "form:not([ngNoForm]):not([ngNativeValidate])"
2138
+ },
2139
+ {
2140
+ kind: "directive",
2141
+ type: i1.DefaultValueAccessor,
2142
+ selector: "input:not([type=checkbox]):not([ngNoCva])[formControlName],textarea:not([ngNoCva])[formControlName],input:not([type=checkbox]):not([ngNoCva])[formControl],textarea:not([ngNoCva])[formControl],input:not([type=checkbox]):not([ngNoCva])[ngModel],textarea:not([ngNoCva])[ngModel],[ngDefaultControl]"
2143
+ },
2144
+ {
2145
+ kind: "directive",
2146
+ type: i1.NgControlStatus,
2147
+ selector: "[formControlName],[ngModel],[formControl]"
2148
+ },
2149
+ {
2150
+ kind: "directive",
2151
+ type: i1.NgControlStatusGroup,
2152
+ selector: "[formGroupName],[formArrayName],[ngModelGroup],[formGroup],[formArray],form:not([ngNoForm]),[ngForm]"
2153
+ },
2154
+ {
2155
+ kind: "directive",
2156
+ type: i1.FormGroupDirective,
2157
+ selector: "[formGroup]",
2158
+ inputs: ["formGroup"],
2159
+ outputs: ["ngSubmit"],
2160
+ exportAs: ["ngForm"]
2161
+ },
2162
+ {
2163
+ kind: "directive",
2164
+ type: i1.FormControlName,
2165
+ selector: "[formControlName]",
2166
+ inputs: [
2167
+ "formControlName",
2168
+ "disabled",
2169
+ "ngModel"
2170
+ ],
2171
+ outputs: ["ngModelChange"]
2172
+ },
2173
+ {
2174
+ kind: "directive",
2175
+ type: RouterLink,
2176
+ selector: "[routerLink]",
2177
+ inputs: [
2178
+ "target",
2179
+ "queryParams",
2180
+ "fragment",
2181
+ "queryParamsHandling",
2182
+ "state",
2183
+ "info",
2184
+ "relativeTo",
2185
+ "preserveFragment",
2186
+ "skipLocationChange",
2187
+ "replaceUrl",
2188
+ "browserUrl",
2189
+ "routerLink"
2190
+ ]
2191
+ },
2192
+ {
2193
+ kind: "component",
2194
+ type: KcPasswordToggleComponent,
2195
+ selector: "kc-password"
2196
+ }
2197
+ ]
2198
+ });
2199
+ };
2200
+ i0.ɵɵngDeclareClassMetadata({
2201
+ minVersion: "12.0.0",
2202
+ version: "22.2.1",
2203
+ ngImport: i0,
2204
+ type: KcLoginComponent,
2205
+ decorators: [{
2206
+ type: Component,
2207
+ args: [{
2208
+ selector: "kc-login",
2209
+ standalone: true,
2210
+ imports: [
2211
+ ReactiveFormsModule,
2212
+ RouterLink,
2213
+ KcPasswordToggleComponent
2214
+ ],
2215
+ template: `
2216
+ <div class="kc-auth-screen">
2217
+ <div class="kc-auth-card">
2218
+ @if (config.logoUrl) {
2219
+ <img class="kc-auth-logo" [src]="config.logoUrl" alt="" />
2220
+ }
2221
+ <h1 class="kc-auth-title">{{ config.appName }}</h1>
2222
+ <p class="kc-auth-subtitle">ลงชื่อเข้าสู่ระบบ</p>
2223
+
2224
+ @if (message(); as m) {
2225
+ <div class="kc-auth-alert" [class.error]="m.error" [class.info]="!m.error" role="alert">{{ m.text }}</div>
2226
+ }
2227
+
2228
+ @if (config.passwordLogin) {
2229
+ <form [formGroup]="form" (ngSubmit)="submit()" novalidate>
2230
+ <label class="kc-auth-field">
2231
+ <span>ชื่อผู้ใช้</span>
2232
+ <input class="kc-auth-input" type="text" formControlName="username" autocomplete="username" />
2233
+ </label>
2234
+ <label class="kc-auth-field">
2235
+ <span>รหัสผ่าน</span>
2236
+ <kc-password><input class="kc-auth-input" type="password" formControlName="password" autocomplete="current-password" /></kc-password>
2237
+ </label>
2238
+ <!-- ปุ่มเข้าสู่ระบบกับ SSO อยู่บรรทัดเดียวกัน แบ่งครึ่ง — ปุ่ม SSO ใช้แค่ชื่อเพื่อให้พอดีครึ่งการ์ด -->
2239
+ <div class="kc-auth-btn-row">
2240
+ <button type="submit" class="kc-auth-btn primary" [disabled]="!!busy()">
2241
+ {{ busy() === 'password' ? 'กำลังเข้าสู่ระบบ...' : 'เข้าสู่ระบบ' }}
2242
+ </button>
2243
+ @if (config.sso; as sso) {
2244
+ <button type="button" class="kc-auth-btn outline" [disabled]="!!busy()" (click)="startSso()"
2245
+ [attr.title]="'เข้าสู่ระบบด้วย ' + sso.label">
2246
+ {{ busy() === 'sso' ? 'กำลังเชื่อมต่อ...' : sso.label }}
2247
+ </button>
2248
+ }
2249
+ </div>
2250
+ </form>
2251
+ } @else if (config.sso; as sso) {
2252
+ <!-- ปิดรหัสผ่าน: มีแต่ปุ่ม SSO ตรงกลาง -->
2253
+ <div class="kc-auth-sso-only">
2254
+ <button type="button" class="kc-auth-btn primary" [disabled]="!!busy()" (click)="startSso()">
2255
+ {{ busy() === 'sso' ? 'กำลังเชื่อมต่อ...' : 'เข้าสู่ระบบด้วย ' + sso.label }}
2256
+ </button>
2257
+ </div>
2258
+ }
2259
+
2260
+ @if (config.register === 'form') {
2261
+ <p class="kc-auth-footer">ยังไม่มีบัญชี? <a class="kc-auth-link" [routerLink]="paths.register">ลงทะเบียน</a></p>
2262
+ } @else if (config.register === 'sso' && config.sso) {
2263
+ <p class="kc-auth-footer">ผู้ใช้ใหม่ กดปุ่ม {{ config.sso.label }} เพื่อลงทะเบียน</p>
2264
+ }
2265
+ <ng-content />
2266
+ </div>
2267
+ </div>
2268
+ `
2269
+ }]
2270
+ }],
2271
+ ctorParameters: () => []
2272
+ });
2273
+ const EMAIL_RE = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
2274
+ const MOBILE_RE = /^0\d{9}$/;
2275
+ function kcUsernameProblem(username) {
2276
+ const u = username.trim();
2277
+ if (!u) return "กรุณากรอกชื่อผู้ใช้";
2278
+ if (/\s/.test(u)) return "ชื่อผู้ใช้ห้ามมีช่องว่าง";
2279
+ if (u.length > 50) return "ชื่อผู้ใช้ยาวได้ไม่เกิน 50 ตัวอักษร";
2280
+ return "";
2281
+ }
2282
+ function kcPasswordProblem(password, confirm, minLength, required) {
2283
+ if (!password && !required && !confirm) return "";
2284
+ if (password.length < minLength) return `กรุณากรอกรหัสผ่านอย่างน้อย ${minLength} ตัวอักษร`;
2285
+ if (password !== confirm) return "รหัสผ่านทั้งสองช่องไม่ตรงกัน";
2286
+ return "";
2287
+ }
2288
+ function kcContactProblem(email, mobile) {
2289
+ if (!EMAIL_RE.test(email.trim())) return "กรุณากรอกอีเมลให้ถูกต้อง";
2290
+ if (!MOBILE_RE.test(mobile.trim())) return "กรุณากรอกเบอร์มือถือ 10 หลัก ขึ้นต้นด้วย 0";
2291
+ return "";
2292
+ }
2293
+ var KcRegisterComponent = class KcRegisterComponent {
2294
+ config = inject(KC_AUTH_CONFIG);
2295
+ paths = KC_AUTH_PATHS;
2296
+ ssoLabel = this.config.sso?.label ?? "SSO";
2297
+ auth = inject(KcAuthService);
2298
+ http = inject(HttpClient);
2299
+ router = inject(Router);
2300
+ loading = signal(false, ...ngDevMode ? [{ debugName: "loading" }] : /* istanbul ignore next */ []);
2301
+ ready = signal(false, ...ngDevMode ? [{ debugName: "ready" }] : /* istanbul ignore next */ []);
2302
+ saving = signal(false, ...ngDevMode ? [{ debugName: "saving" }] : /* istanbul ignore next */ []);
2303
+ error = signal("", ...ngDevMode ? [{ debugName: "error" }] : /* istanbul ignore next */ []);
2304
+ done = signal("", ...ngDevMode ? [{ debugName: "done" }] : /* istanbul ignore next */ []);
2305
+ notice = signal("", ...ngDevMode ? [{ debugName: "notice" }] : /* istanbul ignore next */ []);
2306
+ fromSso = signal(false, ...ngDevMode ? [{ debugName: "fromSso" }] : /* istanbul ignore next */ []);
2307
+ cid = signal("", ...ngDevMode ? [{ debugName: "cid" }] : /* istanbul ignore next */ []);
2308
+ orgs = signal([], ...ngDevMode ? [{ debugName: "orgs" }] : /* istanbul ignore next */ []);
2309
+ orgLocked = signal(false, ...ngDevMode ? [{ debugName: "orgLocked" }] : /* istanbul ignore next */ []);
2310
+ lockedOrg = signal("", ...ngDevMode ? [{ debugName: "lockedOrg" }] : /* istanbul ignore next */ []);
2311
+ orgName = computed(() => this.orgs().find((o) => o.value === this.lockedOrg())?.label ?? this.lockedOrg(), ...ngDevMode ? [{ debugName: "orgName" }] : /* istanbul ignore next */ []);
2312
+ form = inject(FormBuilder).nonNullable.group({
2313
+ fullName: [""],
2314
+ org: [""],
2315
+ username: [""],
2316
+ password: [""],
2317
+ confirmPassword: [""]
2318
+ });
2319
+ ticket = "";
2320
+ constructor() {
2321
+ ensureAuthStyles();
2322
+ }
2323
+ ngOnInit() {
2324
+ const mode = this.config.register;
2325
+ if (mode === "none") {
2326
+ this.error.set("ระบบนี้ไม่เปิดให้ลงทะเบียนเอง กรุณาติดต่อผู้ดูแลระบบ");
2327
+ return;
2328
+ }
2329
+ if (mode === "sso") {
2330
+ this.ticket = this.auth.getTemp(KC_SSO_TICKET_KEY);
2331
+ if (!this.ticket) {
2332
+ this.error.set(`ยังไม่ได้ยืนยันตัวตน กรุณากลับไปกด "เข้าสู่ระบบด้วย ${this.ssoLabel}" ที่หน้าเข้าสู่ระบบก่อน`);
2333
+ return;
2334
+ }
2335
+ }
2336
+ this.loading.set(true);
2337
+ const params = this.ticket ? { ticket: this.ticket } : {};
2338
+ this.http.get(`${this.config.apiBase}/user/register-info`, { params }).subscribe({
2339
+ next: (info) => {
2340
+ this.orgs.set(info.orgs ?? []);
2341
+ this.cid.set(info.cid ?? "");
2342
+ this.notice.set(info.notice ?? "");
2343
+ this.form.patchValue({
2344
+ fullName: info.fullName ?? "",
2345
+ org: info.org ?? ""
2346
+ });
2347
+ if (info.fullName) {
2348
+ this.fromSso.set(true);
2349
+ this.form.controls.fullName.disable();
2350
+ }
2351
+ if (info.orgLocked && info.org) {
2352
+ this.orgLocked.set(true);
2353
+ this.lockedOrg.set(info.org);
2354
+ }
2355
+ this.loading.set(false);
2356
+ this.ready.set(true);
2357
+ },
2358
+ error: (err) => {
2359
+ this.loading.set(false);
2360
+ if (this.ticket) this.auth.takeTemp(KC_SSO_TICKET_KEY);
2361
+ this.error.set(kcErrorMessage(err, "ไม่สามารถเริ่มการลงทะเบียนได้"));
2362
+ }
2363
+ });
2364
+ }
2365
+ submit() {
2366
+ if (this.saving()) return;
2367
+ const v = this.form.getRawValue();
2368
+ const problem = (!v.fullName.trim() ? "กรุณากรอกชื่อ-สกุล" : "") || kcUsernameProblem(v.username) || kcPasswordProblem(v.password, v.confirmPassword, this.config.passwordMinLength, true) || (this.orgs().length && !v.org ? `กรุณาเลือก${this.config.orgLabel}ที่สังกัด` : "");
2369
+ this.error.set(problem);
2370
+ if (problem) return;
2371
+ const body = {
2372
+ ...this.ticket ? { ticket: this.ticket } : {},
2373
+ fullName: v.fullName.trim(),
2374
+ username: v.username.trim(),
2375
+ password: v.password,
2376
+ org: v.org
2377
+ };
2378
+ this.saving.set(true);
2379
+ this.http.post(`${this.config.apiBase}/user/register`, body).subscribe({
2380
+ next: (res) => {
2381
+ this.saving.set(false);
2382
+ if (this.ticket) this.auth.takeTemp(KC_SSO_TICKET_KEY);
2383
+ if (res?.token && res.user) {
2384
+ this.auth.start({
2385
+ token: res.token,
2386
+ user: res.user
2387
+ });
2388
+ this.router.navigateByUrl(this.config.homeUrl);
2389
+ return;
2390
+ }
2391
+ this.done.set(res?.message || "ลงทะเบียนเรียบร้อยแล้ว กรุณาเข้าสู่ระบบ");
2392
+ },
2393
+ error: (err) => {
2394
+ this.saving.set(false);
2395
+ this.error.set(kcErrorMessage(err, "ลงทะเบียนไม่สำเร็จ"));
2396
+ }
2397
+ });
2398
+ }
2399
+ static ɵfac = i0.ɵɵngDeclareFactory({
2400
+ minVersion: "12.0.0",
2401
+ version: "22.2.1",
2402
+ ngImport: i0,
2403
+ type: KcRegisterComponent,
2404
+ deps: [],
2405
+ target: i0.ɵɵFactoryTarget.Component
2406
+ });
2407
+ static ɵcmp = i0.ɵɵngDeclareComponent({
2408
+ minVersion: "17.0.0",
2409
+ version: "22.2.1",
2410
+ type: KcRegisterComponent,
2411
+ isStandalone: true,
2412
+ selector: "kc-register",
2413
+ ngImport: i0,
2414
+ template: `
2415
+ <div class="kc-auth-screen">
2416
+ <div class="kc-auth-card">
2417
+ <h1 class="kc-auth-title">ลงทะเบียนผู้ใช้ใหม่</h1>
2418
+ <p class="kc-auth-subtitle">{{ config.appName }}</p>
2419
+
2420
+ @if (loading()) {
2421
+ <p class="kc-auth-subtitle">{{ ticket ? 'กำลังตรวจสอบการยืนยันตัวตน...' : 'กำลังโหลดข้อมูล...' }}</p>
2422
+ } @else if (done()) {
2423
+ <div class="kc-auth-alert success" role="status">{{ done() }}</div>
2424
+ <div class="kc-auth-actions center">
2425
+ <a class="kc-auth-btn primary" [routerLink]="paths.login">กลับไปหน้าเข้าสู่ระบบ</a>
2426
+ </div>
2427
+ } @else if (!ready()) {
2428
+ <!-- ไม่มีตั๋ว / ตั๋วใช้ไม่ได้ / ไม่เปิดให้ลงทะเบียน — ไม่แสดงฟอร์ม เพราะกรอกไปก็ส่งไม่ผ่าน -->
2429
+ <div class="kc-auth-alert error" role="alert">{{ error() }}</div>
2430
+ <div class="kc-auth-actions center">
2431
+ <a class="kc-auth-btn" [routerLink]="paths.login">กลับไปหน้าเข้าสู่ระบบ</a>
2432
+ </div>
2433
+ } @else {
2434
+ @if (notice()) {
2435
+ <div class="kc-auth-alert warn" role="status">{{ notice() }}</div>
2436
+ }
2437
+ <form [formGroup]="form" (ngSubmit)="submit()" novalidate>
2438
+ <label class="kc-auth-field">
2439
+ <span>ชื่อ-สกุล @if (fromSso()) { (จาก {{ ssoLabel }}) } @else { <span class="req">*</span> }</span>
2440
+ <input class="kc-auth-input" type="text" formControlName="fullName" maxlength="200" autocomplete="name" />
2441
+ </label>
2442
+
2443
+ @if (cid()) {
2444
+ <label class="kc-auth-field">
2445
+ <span>เลขบัตรประชาชน</span>
2446
+ <input class="kc-auth-input" type="text" [value]="cid()" disabled />
2447
+ </label>
2448
+ }
2449
+
2450
+ @if (orgs().length) {
2451
+ @if (orgLocked()) {
2452
+ <label class="kc-auth-field">
2453
+ <span>{{ config.orgLabel }}ที่สังกัด</span>
2454
+ <input class="kc-auth-input" type="text" [value]="orgName()" disabled />
2455
+ </label>
2456
+ } @else {
2457
+ <div class="kc-auth-field">
2458
+ <label class="kc-auth-label" for="kc-reg-org">{{ config.orgLabel }}ที่สังกัด <span class="req">*</span></label>
2459
+ <app-search-select inputId="kc-reg-org" [options]="orgs()" formControlName="org"
2460
+ [placeholder]="'-- เลือก' + config.orgLabel + 'ที่สังกัด --'" />
2461
+ </div>
2462
+ }
2463
+ }
2464
+
2465
+ <label class="kc-auth-field">
2466
+ <span>ชื่อผู้ใช้ <span class="req">*</span></span>
2467
+ <input class="kc-auth-input" type="text" formControlName="username" maxlength="50" autocomplete="username" />
2468
+ </label>
2469
+ <label class="kc-auth-field">
2470
+ <span>รหัสผ่าน <span class="req">*</span></span>
2471
+ <kc-password><input class="kc-auth-input" type="password" formControlName="password" maxlength="50" autocomplete="new-password" /></kc-password>
2472
+ </label>
2473
+ <label class="kc-auth-field">
2474
+ <span>ยืนยันรหัสผ่าน <span class="req">*</span></span>
2475
+ <kc-password><input class="kc-auth-input" type="password" formControlName="confirmPassword" maxlength="50" autocomplete="new-password" /></kc-password>
2476
+ </label>
2477
+
2478
+ <div class="kc-auth-actions center">
2479
+ <button type="submit" class="kc-auth-btn primary" [disabled]="saving()">
2480
+ {{ saving() ? 'กำลังบันทึก...' : 'ลงทะเบียน' }}
2481
+ </button>
2482
+ <a class="kc-auth-btn" [routerLink]="paths.login" [class.disabled]="saving()">ยกเลิก</a>
2483
+ </div>
2484
+
2485
+ @if (error()) {
2486
+ <div class="kc-auth-alert error" role="alert" style="margin: 14px 0 0">{{ error() }}</div>
2487
+ }
2488
+ </form>
2489
+ }
2490
+ </div>
2491
+ </div>
2492
+ `,
2493
+ isInline: true,
2494
+ dependencies: [
2495
+ {
2496
+ kind: "ngmodule",
2497
+ type: ReactiveFormsModule
2498
+ },
2499
+ {
2500
+ kind: "directive",
2501
+ type: i1.ɵNgNoValidate,
2502
+ selector: "form:not([ngNoForm]):not([ngNativeValidate])"
2503
+ },
2504
+ {
2505
+ kind: "directive",
2506
+ type: i1.DefaultValueAccessor,
2507
+ selector: "input:not([type=checkbox]):not([ngNoCva])[formControlName],textarea:not([ngNoCva])[formControlName],input:not([type=checkbox]):not([ngNoCva])[formControl],textarea:not([ngNoCva])[formControl],input:not([type=checkbox]):not([ngNoCva])[ngModel],textarea:not([ngNoCva])[ngModel],[ngDefaultControl]"
2508
+ },
2509
+ {
2510
+ kind: "directive",
2511
+ type: i1.NgControlStatus,
2512
+ selector: "[formControlName],[ngModel],[formControl]"
2513
+ },
2514
+ {
2515
+ kind: "directive",
2516
+ type: i1.NgControlStatusGroup,
2517
+ selector: "[formGroupName],[formArrayName],[ngModelGroup],[formGroup],[formArray],form:not([ngNoForm]),[ngForm]"
2518
+ },
2519
+ {
2520
+ kind: "directive",
2521
+ type: i1.MaxLengthValidator,
2522
+ selector: "[maxlength][formControlName],[maxlength][formControl],[maxlength][ngModel]",
2523
+ inputs: ["maxlength"]
2524
+ },
2525
+ {
2526
+ kind: "directive",
2527
+ type: i1.FormGroupDirective,
2528
+ selector: "[formGroup]",
2529
+ inputs: ["formGroup"],
2530
+ outputs: ["ngSubmit"],
2531
+ exportAs: ["ngForm"]
2532
+ },
2533
+ {
2534
+ kind: "directive",
2535
+ type: i1.FormControlName,
2536
+ selector: "[formControlName]",
2537
+ inputs: [
2538
+ "formControlName",
2539
+ "disabled",
2540
+ "ngModel"
2541
+ ],
2542
+ outputs: ["ngModelChange"]
2543
+ },
2544
+ {
2545
+ kind: "directive",
2546
+ type: RouterLink,
2547
+ selector: "[routerLink]",
2548
+ inputs: [
2549
+ "target",
2550
+ "queryParams",
2551
+ "fragment",
2552
+ "queryParamsHandling",
2553
+ "state",
2554
+ "info",
2555
+ "relativeTo",
2556
+ "preserveFragment",
2557
+ "skipLocationChange",
2558
+ "replaceUrl",
2559
+ "browserUrl",
2560
+ "routerLink"
2561
+ ]
2562
+ },
2563
+ {
2564
+ kind: "component",
2565
+ type: SearchSelectComponent,
2566
+ selector: "app-search-select",
2567
+ inputs: [
2568
+ "options",
2569
+ "placeholder",
2570
+ "searchHint",
2571
+ "inputId"
2572
+ ]
2573
+ },
2574
+ {
2575
+ kind: "component",
2576
+ type: KcPasswordToggleComponent,
2577
+ selector: "kc-password"
2578
+ }
2579
+ ]
2580
+ });
2581
+ };
2582
+ i0.ɵɵngDeclareClassMetadata({
2583
+ minVersion: "12.0.0",
2584
+ version: "22.2.1",
2585
+ ngImport: i0,
2586
+ type: KcRegisterComponent,
2587
+ decorators: [{
2588
+ type: Component,
2589
+ args: [{
2590
+ selector: "kc-register",
2591
+ standalone: true,
2592
+ imports: [
2593
+ ReactiveFormsModule,
2594
+ RouterLink,
2595
+ SearchSelectComponent,
2596
+ KcPasswordToggleComponent
2597
+ ],
2598
+ template: `
2599
+ <div class="kc-auth-screen">
2600
+ <div class="kc-auth-card">
2601
+ <h1 class="kc-auth-title">ลงทะเบียนผู้ใช้ใหม่</h1>
2602
+ <p class="kc-auth-subtitle">{{ config.appName }}</p>
2603
+
2604
+ @if (loading()) {
2605
+ <p class="kc-auth-subtitle">{{ ticket ? 'กำลังตรวจสอบการยืนยันตัวตน...' : 'กำลังโหลดข้อมูล...' }}</p>
2606
+ } @else if (done()) {
2607
+ <div class="kc-auth-alert success" role="status">{{ done() }}</div>
2608
+ <div class="kc-auth-actions center">
2609
+ <a class="kc-auth-btn primary" [routerLink]="paths.login">กลับไปหน้าเข้าสู่ระบบ</a>
2610
+ </div>
2611
+ } @else if (!ready()) {
2612
+ <!-- ไม่มีตั๋ว / ตั๋วใช้ไม่ได้ / ไม่เปิดให้ลงทะเบียน — ไม่แสดงฟอร์ม เพราะกรอกไปก็ส่งไม่ผ่าน -->
2613
+ <div class="kc-auth-alert error" role="alert">{{ error() }}</div>
2614
+ <div class="kc-auth-actions center">
2615
+ <a class="kc-auth-btn" [routerLink]="paths.login">กลับไปหน้าเข้าสู่ระบบ</a>
2616
+ </div>
2617
+ } @else {
2618
+ @if (notice()) {
2619
+ <div class="kc-auth-alert warn" role="status">{{ notice() }}</div>
2620
+ }
2621
+ <form [formGroup]="form" (ngSubmit)="submit()" novalidate>
2622
+ <label class="kc-auth-field">
2623
+ <span>ชื่อ-สกุล @if (fromSso()) { (จาก {{ ssoLabel }}) } @else { <span class="req">*</span> }</span>
2624
+ <input class="kc-auth-input" type="text" formControlName="fullName" maxlength="200" autocomplete="name" />
2625
+ </label>
2626
+
2627
+ @if (cid()) {
2628
+ <label class="kc-auth-field">
2629
+ <span>เลขบัตรประชาชน</span>
2630
+ <input class="kc-auth-input" type="text" [value]="cid()" disabled />
2631
+ </label>
2632
+ }
2633
+
2634
+ @if (orgs().length) {
2635
+ @if (orgLocked()) {
2636
+ <label class="kc-auth-field">
2637
+ <span>{{ config.orgLabel }}ที่สังกัด</span>
2638
+ <input class="kc-auth-input" type="text" [value]="orgName()" disabled />
2639
+ </label>
2640
+ } @else {
2641
+ <div class="kc-auth-field">
2642
+ <label class="kc-auth-label" for="kc-reg-org">{{ config.orgLabel }}ที่สังกัด <span class="req">*</span></label>
2643
+ <app-search-select inputId="kc-reg-org" [options]="orgs()" formControlName="org"
2644
+ [placeholder]="'-- เลือก' + config.orgLabel + 'ที่สังกัด --'" />
2645
+ </div>
2646
+ }
2647
+ }
2648
+
2649
+ <label class="kc-auth-field">
2650
+ <span>ชื่อผู้ใช้ <span class="req">*</span></span>
2651
+ <input class="kc-auth-input" type="text" formControlName="username" maxlength="50" autocomplete="username" />
2652
+ </label>
2653
+ <label class="kc-auth-field">
2654
+ <span>รหัสผ่าน <span class="req">*</span></span>
2655
+ <kc-password><input class="kc-auth-input" type="password" formControlName="password" maxlength="50" autocomplete="new-password" /></kc-password>
2656
+ </label>
2657
+ <label class="kc-auth-field">
2658
+ <span>ยืนยันรหัสผ่าน <span class="req">*</span></span>
2659
+ <kc-password><input class="kc-auth-input" type="password" formControlName="confirmPassword" maxlength="50" autocomplete="new-password" /></kc-password>
2660
+ </label>
2661
+
2662
+ <div class="kc-auth-actions center">
2663
+ <button type="submit" class="kc-auth-btn primary" [disabled]="saving()">
2664
+ {{ saving() ? 'กำลังบันทึก...' : 'ลงทะเบียน' }}
2665
+ </button>
2666
+ <a class="kc-auth-btn" [routerLink]="paths.login" [class.disabled]="saving()">ยกเลิก</a>
2667
+ </div>
2668
+
2669
+ @if (error()) {
2670
+ <div class="kc-auth-alert error" role="alert" style="margin: 14px 0 0">{{ error() }}</div>
2671
+ }
2672
+ </form>
2673
+ }
2674
+ </div>
2675
+ </div>
2676
+ `
2677
+ }]
2678
+ }],
2679
+ ctorParameters: () => []
2680
+ });
2681
+ var KcSsoCallbackComponent = class KcSsoCallbackComponent {
2682
+ config = inject(KC_AUTH_CONFIG);
2683
+ auth = inject(KcAuthService);
2684
+ http = inject(HttpClient);
2685
+ route = inject(ActivatedRoute);
2686
+ router = inject(Router);
2687
+ paths = KC_AUTH_PATHS;
2688
+ label = this.config.sso?.label ?? "SSO";
2689
+ message = signal(`กำลังเข้าสู่ระบบผ่าน ${this.label}...`, ...ngDevMode ? [{ debugName: "message" }] : /* istanbul ignore next */ []);
2690
+ failed = signal(false, ...ngDevMode ? [{ debugName: "failed" }] : /* istanbul ignore next */ []);
2691
+ constructor() {
2692
+ ensureAuthStyles();
2693
+ }
2694
+ ngOnInit() {
2695
+ const params = this.route.snapshot.queryParamMap;
2696
+ const expectedState = this.auth.takeTemp(KC_SSO_STATE_KEY);
2697
+ const code = params.get("code");
2698
+ if (params.get("error")) {
2699
+ this.fail(`${this.label} ปฏิเสธการเข้าสู่ระบบ กรุณาลองใหม่อีกครั้ง`);
2700
+ return;
2701
+ }
2702
+ if (!code) {
2703
+ this.message.set(`กำลังเชื่อมต่อกับ ${this.label}...`);
2704
+ this.http.get(`${this.config.apiBase}/user/sso/authorize-url`).subscribe({
2705
+ next: ({ url, state }) => {
2706
+ this.auth.setTemp(KC_SSO_STATE_KEY, state);
2707
+ this.auth.redirect(url);
2708
+ },
2709
+ error: () => this.fail(`ไม่สามารถเชื่อมต่อ ${this.label} ได้ กรุณาเริ่มเข้าสู่ระบบใหม่`)
2710
+ });
2711
+ return;
2712
+ }
2713
+ if (!expectedState || expectedState !== params.get("state")) {
2714
+ this.fail("คำขอเข้าสู่ระบบไม่ถูกต้อง (state ไม่ตรงกัน) กรุณาเริ่มเข้าสู่ระบบใหม่");
2715
+ return;
2716
+ }
2717
+ this.http.post(`${this.config.apiBase}/user/sso/callback`, { code }).subscribe({
2718
+ next: (res) => {
2719
+ if (res.mode === "signed-in" && res.token && res.user) {
2720
+ this.auth.start({
2721
+ token: res.token,
2722
+ user: res.user
2723
+ });
2724
+ this.router.navigateByUrl(this.auth.safeReturnUrl(this.auth.takeTemp(KC_RETURN_URL_KEY)), { replaceUrl: true });
2725
+ return;
2726
+ }
2727
+ if (res.mode === "register" && res.ticket) {
2728
+ this.auth.setTemp(KC_SSO_TICKET_KEY, res.ticket);
2729
+ this.message.set("ยังไม่มีบัญชีในระบบ กำลังไปหน้าลงทะเบียน...");
2730
+ this.router.navigateByUrl(KC_AUTH_PATHS.register, { replaceUrl: true });
2731
+ return;
2732
+ }
2733
+ this.fail("ไม่สามารถเข้าสู่ระบบได้ กรุณาลองใหม่อีกครั้ง");
2734
+ },
2735
+ error: (err) => this.fail(kcErrorMessage(err, `ไม่สามารถเข้าสู่ระบบผ่าน ${this.label} ได้`))
2736
+ });
2737
+ }
2738
+ fail(message) {
2739
+ this.message.set(message);
2740
+ this.failed.set(true);
2741
+ }
2742
+ static ɵfac = i0.ɵɵngDeclareFactory({
2743
+ minVersion: "12.0.0",
2744
+ version: "22.2.1",
2745
+ ngImport: i0,
2746
+ type: KcSsoCallbackComponent,
2747
+ deps: [],
2748
+ target: i0.ɵɵFactoryTarget.Component
2749
+ });
2750
+ static ɵcmp = i0.ɵɵngDeclareComponent({
2751
+ minVersion: "17.0.0",
2752
+ version: "22.2.1",
2753
+ type: KcSsoCallbackComponent,
2754
+ isStandalone: true,
2755
+ selector: "kc-sso-callback",
2756
+ ngImport: i0,
2757
+ template: `
2758
+ <div class="kc-auth-screen">
2759
+ <div class="kc-auth-card center">
2760
+ <h1 class="kc-auth-title">{{ label }}</h1>
2761
+ @if (failed()) {
2762
+ <div class="kc-auth-alert error" role="alert" style="margin-top: 16px">{{ message() }}</div>
2763
+ <a class="kc-auth-btn" [routerLink]="paths.login">กลับไปหน้าเข้าสู่ระบบ</a>
2764
+ } @else {
2765
+ <p class="kc-auth-subtitle" style="margin-bottom: 0">{{ message() }}</p>
2766
+ }
2767
+ </div>
2768
+ </div>
2769
+ `,
2770
+ isInline: true,
2771
+ dependencies: [{
2772
+ kind: "directive",
2773
+ type: RouterLink,
2774
+ selector: "[routerLink]",
2775
+ inputs: [
2776
+ "target",
2777
+ "queryParams",
2778
+ "fragment",
2779
+ "queryParamsHandling",
2780
+ "state",
2781
+ "info",
2782
+ "relativeTo",
2783
+ "preserveFragment",
2784
+ "skipLocationChange",
2785
+ "replaceUrl",
2786
+ "browserUrl",
2787
+ "routerLink"
2788
+ ]
2789
+ }]
2790
+ });
2791
+ };
2792
+ i0.ɵɵngDeclareClassMetadata({
2793
+ minVersion: "12.0.0",
2794
+ version: "22.2.1",
2795
+ ngImport: i0,
2796
+ type: KcSsoCallbackComponent,
2797
+ decorators: [{
2798
+ type: Component,
2799
+ args: [{
2800
+ selector: "kc-sso-callback",
2801
+ standalone: true,
2802
+ imports: [RouterLink],
2803
+ template: `
2804
+ <div class="kc-auth-screen">
2805
+ <div class="kc-auth-card center">
2806
+ <h1 class="kc-auth-title">{{ label }}</h1>
2807
+ @if (failed()) {
2808
+ <div class="kc-auth-alert error" role="alert" style="margin-top: 16px">{{ message() }}</div>
2809
+ <a class="kc-auth-btn" [routerLink]="paths.login">กลับไปหน้าเข้าสู่ระบบ</a>
2810
+ } @else {
2811
+ <p class="kc-auth-subtitle" style="margin-bottom: 0">{{ message() }}</p>
2812
+ }
2813
+ </div>
2814
+ </div>
2815
+ `
2816
+ }]
2817
+ }],
2818
+ ctorParameters: () => []
2819
+ });
2820
+ function kcAuthRoutes() {
2821
+ return [
2822
+ {
2823
+ path: "login",
2824
+ component: KcLoginComponent,
2825
+ title: "เข้าสู่ระบบ"
2826
+ },
2827
+ {
2828
+ path: "register",
2829
+ component: KcRegisterComponent,
2830
+ title: "ลงทะเบียน"
2831
+ },
2832
+ {
2833
+ path: "sso-callback",
2834
+ component: KcSsoCallbackComponent,
2835
+ title: "เข้าสู่ระบบ"
2836
+ }
2837
+ ];
2838
+ }
2839
+ var KcProfileComponent = class KcProfileComponent {
2840
+ config = inject(KC_AUTH_CONFIG);
2841
+ auth = inject(KcAuthService);
2842
+ http = inject(HttpClient);
2843
+ loading = signal(true, ...ngDevMode ? [{ debugName: "loading" }] : /* istanbul ignore next */ []);
2844
+ loadFailed = signal(false, ...ngDevMode ? [{ debugName: "loadFailed" }] : /* istanbul ignore next */ []);
2845
+ saving = signal(false, ...ngDevMode ? [{ debugName: "saving" }] : /* istanbul ignore next */ []);
2846
+ error = signal("", ...ngDevMode ? [{ debugName: "error" }] : /* istanbul ignore next */ []);
2847
+ success = signal("", ...ngDevMode ? [{ debugName: "success" }] : /* istanbul ignore next */ []);
2848
+ fullName = signal("", ...ngDevMode ? [{ debugName: "fullName" }] : /* istanbul ignore next */ []);
2849
+ orgName = signal("", ...ngDevMode ? [{ debugName: "orgName" }] : /* istanbul ignore next */ []);
2850
+ form = inject(FormBuilder).nonNullable.group({
2851
+ email: ["", [Validators.required, Validators.email]],
2852
+ mobile: ["", [Validators.required, Validators.pattern(/^0\d{9}$/)]],
2853
+ username: ["", [Validators.required, Validators.pattern(/^\S+$/)]],
2854
+ password: [""],
2855
+ confirmPassword: [""]
2856
+ });
2857
+ constructor() {
2858
+ ensureAuthStyles();
2859
+ }
2860
+ ngOnInit() {
2861
+ this.http.get(`${this.config.apiBase}/user/me`).subscribe({
2862
+ next: (me) => {
2863
+ this.fullName.set(me.fullName);
2864
+ this.orgName.set(me.orgName || me.org);
2865
+ this.form.patchValue({
2866
+ email: me.email ?? "",
2867
+ mobile: me.mobile ?? "",
2868
+ username: me.username ?? ""
2869
+ });
2870
+ this.loading.set(false);
2871
+ },
2872
+ error: (err) => {
2873
+ this.loading.set(false);
2874
+ this.loadFailed.set(true);
2875
+ this.error.set(kcErrorMessage(err, "ไม่สามารถโหลดข้อมูลบัญชีได้"));
2876
+ }
2877
+ });
2878
+ }
2879
+ bad(name) {
2880
+ const c = this.form.controls[name];
2881
+ return c.touched && c.invalid;
2882
+ }
2883
+ submit() {
2884
+ if (this.saving() || this.loading()) return;
2885
+ this.success.set("");
2886
+ this.form.markAllAsTouched();
2887
+ const v = this.form.getRawValue();
2888
+ const problem = kcContactProblem(v.email, v.mobile) || kcUsernameProblem(v.username) || kcPasswordProblem(v.password, v.confirmPassword, this.config.passwordMinLength, false);
2889
+ this.error.set(problem);
2890
+ if (problem) return;
2891
+ const body = {
2892
+ username: v.username.trim(),
2893
+ email: v.email.trim(),
2894
+ mobile: v.mobile.trim(),
2895
+ password: v.password
2896
+ };
2897
+ this.saving.set(true);
2898
+ this.http.put(`${this.config.apiBase}/user/me`, body).subscribe({
2899
+ next: (res) => {
2900
+ this.saving.set(false);
2901
+ this.auth.updateUser({ username: body.username });
2902
+ this.form.patchValue({
2903
+ password: "",
2904
+ confirmPassword: ""
2905
+ });
2906
+ this.success.set(res?.message || "บันทึกข้อมูลเรียบร้อยแล้ว");
2907
+ },
2908
+ error: (err) => {
2909
+ this.saving.set(false);
2910
+ this.error.set(kcErrorMessage(err, "บันทึกข้อมูลไม่สำเร็จ"));
2911
+ }
2912
+ });
2913
+ }
2914
+ static ɵfac = i0.ɵɵngDeclareFactory({
2915
+ minVersion: "12.0.0",
2916
+ version: "22.2.1",
2917
+ ngImport: i0,
2918
+ type: KcProfileComponent,
2919
+ deps: [],
2920
+ target: i0.ɵɵFactoryTarget.Component
2921
+ });
2922
+ static ɵcmp = i0.ɵɵngDeclareComponent({
2923
+ minVersion: "17.0.0",
2924
+ version: "22.2.1",
2925
+ type: KcProfileComponent,
2926
+ isStandalone: true,
2927
+ selector: "kc-profile",
2928
+ ngImport: i0,
2929
+ template: `
2930
+ <div class="kc-auth-page single">
2931
+ <div class="kc-auth-panel">
2932
+ <h1 class="kc-auth-panel-title kc-auth-page-title">บัญชีของฉัน</h1>
2933
+
2934
+ @if (loading()) {
2935
+ <p class="kc-auth-hint">กำลังโหลดข้อมูล...</p>
2936
+ } @else if (loadFailed()) {
2937
+ <div class="kc-auth-alert error" role="alert">{{ error() }}</div>
2938
+ } @else {
2939
+ <form [formGroup]="form" (ngSubmit)="submit()" novalidate>
2940
+ <label class="kc-auth-field">
2941
+ <span>ชื่อ-สกุล</span>
2942
+ <input class="kc-auth-input" type="text" [value]="fullName()" disabled />
2943
+ </label>
2944
+
2945
+ <label class="kc-auth-field">
2946
+ <span>{{ config.orgLabel }}ที่สังกัด</span>
2947
+ <input class="kc-auth-input" type="text" [value]="orgName()" disabled />
2948
+ <small class="kc-auth-hint">หากต้องการย้าย{{ config.orgLabel }} กรุณาติดต่อผู้ดูแลระบบ</small>
2949
+ </label>
2950
+
2951
+ <label class="kc-auth-field">
2952
+ <span>อีเมล <span class="req">*</span></span>
2953
+ <input class="kc-auth-input" type="email" formControlName="email" maxlength="100" autocomplete="email"
2954
+ [class.invalid]="bad('email')" />
2955
+ @if (bad('email')) {
2956
+ <small class="kc-auth-field-error">* รูปแบบอีเมลไม่ถูกต้อง</small>
2957
+ }
2958
+ </label>
2959
+
2960
+ <label class="kc-auth-field">
2961
+ <span>เบอร์โทรศัพท์มือถือ <span class="req">*</span></span>
2962
+ <input class="kc-auth-input" type="tel" formControlName="mobile" maxlength="10" inputmode="numeric" autocomplete="tel"
2963
+ [class.invalid]="bad('mobile')" />
2964
+ @if (bad('mobile')) {
2965
+ <small class="kc-auth-field-error">* เบอร์มือถือต้องเป็นตัวเลข 10 หลัก และขึ้นต้นด้วย 0</small>
2966
+ }
2967
+ </label>
2968
+
2969
+ <label class="kc-auth-field">
2970
+ <span>ชื่อผู้ใช้ <span class="req">*</span></span>
2971
+ <input class="kc-auth-input" type="text" formControlName="username" maxlength="50" autocomplete="username"
2972
+ [class.invalid]="bad('username')" />
2973
+ @if (bad('username')) {
2974
+ <small class="kc-auth-field-error">* กรุณากรอกชื่อผู้ใช้ (ห้ามมีช่องว่าง)</small>
2975
+ }
2976
+ </label>
2977
+
2978
+ <label class="kc-auth-field">
2979
+ <span>รหัสผ่านใหม่ <small class="kc-auth-hint">(อย่างน้อย {{ config.passwordMinLength }} ตัวอักษร — เว้นว่างถ้าไม่เปลี่ยน)</small></span>
2980
+ <kc-password><input class="kc-auth-input" type="password" formControlName="password" maxlength="50" autocomplete="new-password" /></kc-password>
2981
+ </label>
2982
+
2983
+ <label class="kc-auth-field">
2984
+ <span>ยืนยันรหัสผ่านใหม่</span>
2985
+ <kc-password><input class="kc-auth-input" type="password" formControlName="confirmPassword" maxlength="50" autocomplete="new-password" /></kc-password>
2986
+ </label>
2987
+
2988
+ @if (error()) {
2989
+ <div class="kc-auth-alert error" role="alert">{{ error() }}</div>
2990
+ }
2991
+ @if (success()) {
2992
+ <div class="kc-auth-alert success" role="status">{{ success() }}</div>
2993
+ }
2994
+
2995
+ <button type="submit" class="kc-auth-btn primary block" [disabled]="saving()">
2996
+ {{ saving() ? 'กำลังบันทึก...' : 'บันทึกข้อมูล' }}
2997
+ </button>
2998
+ </form>
2999
+ }
3000
+ </div>
3001
+ </div>
3002
+ `,
3003
+ isInline: true,
3004
+ dependencies: [
3005
+ {
3006
+ kind: "ngmodule",
3007
+ type: ReactiveFormsModule
3008
+ },
3009
+ {
3010
+ kind: "directive",
3011
+ type: i1.ɵNgNoValidate,
3012
+ selector: "form:not([ngNoForm]):not([ngNativeValidate])"
3013
+ },
3014
+ {
3015
+ kind: "directive",
3016
+ type: i1.DefaultValueAccessor,
3017
+ selector: "input:not([type=checkbox]):not([ngNoCva])[formControlName],textarea:not([ngNoCva])[formControlName],input:not([type=checkbox]):not([ngNoCva])[formControl],textarea:not([ngNoCva])[formControl],input:not([type=checkbox]):not([ngNoCva])[ngModel],textarea:not([ngNoCva])[ngModel],[ngDefaultControl]"
3018
+ },
3019
+ {
3020
+ kind: "directive",
3021
+ type: i1.NgControlStatus,
3022
+ selector: "[formControlName],[ngModel],[formControl]"
3023
+ },
3024
+ {
3025
+ kind: "directive",
3026
+ type: i1.NgControlStatusGroup,
3027
+ selector: "[formGroupName],[formArrayName],[ngModelGroup],[formGroup],[formArray],form:not([ngNoForm]),[ngForm]"
3028
+ },
3029
+ {
3030
+ kind: "directive",
3031
+ type: i1.MaxLengthValidator,
3032
+ selector: "[maxlength][formControlName],[maxlength][formControl],[maxlength][ngModel]",
3033
+ inputs: ["maxlength"]
3034
+ },
3035
+ {
3036
+ kind: "directive",
3037
+ type: i1.FormGroupDirective,
3038
+ selector: "[formGroup]",
3039
+ inputs: ["formGroup"],
3040
+ outputs: ["ngSubmit"],
3041
+ exportAs: ["ngForm"]
3042
+ },
3043
+ {
3044
+ kind: "directive",
3045
+ type: i1.FormControlName,
3046
+ selector: "[formControlName]",
3047
+ inputs: [
3048
+ "formControlName",
3049
+ "disabled",
3050
+ "ngModel"
3051
+ ],
3052
+ outputs: ["ngModelChange"]
3053
+ },
3054
+ {
3055
+ kind: "component",
3056
+ type: KcPasswordToggleComponent,
3057
+ selector: "kc-password"
3058
+ }
3059
+ ]
3060
+ });
3061
+ };
3062
+ i0.ɵɵngDeclareClassMetadata({
3063
+ minVersion: "12.0.0",
3064
+ version: "22.2.1",
3065
+ ngImport: i0,
3066
+ type: KcProfileComponent,
3067
+ decorators: [{
3068
+ type: Component,
3069
+ args: [{
3070
+ selector: "kc-profile",
3071
+ standalone: true,
3072
+ imports: [ReactiveFormsModule, KcPasswordToggleComponent],
3073
+ template: `
3074
+ <div class="kc-auth-page single">
3075
+ <div class="kc-auth-panel">
3076
+ <h1 class="kc-auth-panel-title kc-auth-page-title">บัญชีของฉัน</h1>
3077
+
3078
+ @if (loading()) {
3079
+ <p class="kc-auth-hint">กำลังโหลดข้อมูล...</p>
3080
+ } @else if (loadFailed()) {
3081
+ <div class="kc-auth-alert error" role="alert">{{ error() }}</div>
3082
+ } @else {
3083
+ <form [formGroup]="form" (ngSubmit)="submit()" novalidate>
3084
+ <label class="kc-auth-field">
3085
+ <span>ชื่อ-สกุล</span>
3086
+ <input class="kc-auth-input" type="text" [value]="fullName()" disabled />
3087
+ </label>
3088
+
3089
+ <label class="kc-auth-field">
3090
+ <span>{{ config.orgLabel }}ที่สังกัด</span>
3091
+ <input class="kc-auth-input" type="text" [value]="orgName()" disabled />
3092
+ <small class="kc-auth-hint">หากต้องการย้าย{{ config.orgLabel }} กรุณาติดต่อผู้ดูแลระบบ</small>
3093
+ </label>
3094
+
3095
+ <label class="kc-auth-field">
3096
+ <span>อีเมล <span class="req">*</span></span>
3097
+ <input class="kc-auth-input" type="email" formControlName="email" maxlength="100" autocomplete="email"
3098
+ [class.invalid]="bad('email')" />
3099
+ @if (bad('email')) {
3100
+ <small class="kc-auth-field-error">* รูปแบบอีเมลไม่ถูกต้อง</small>
3101
+ }
3102
+ </label>
3103
+
3104
+ <label class="kc-auth-field">
3105
+ <span>เบอร์โทรศัพท์มือถือ <span class="req">*</span></span>
3106
+ <input class="kc-auth-input" type="tel" formControlName="mobile" maxlength="10" inputmode="numeric" autocomplete="tel"
3107
+ [class.invalid]="bad('mobile')" />
3108
+ @if (bad('mobile')) {
3109
+ <small class="kc-auth-field-error">* เบอร์มือถือต้องเป็นตัวเลข 10 หลัก และขึ้นต้นด้วย 0</small>
3110
+ }
3111
+ </label>
3112
+
3113
+ <label class="kc-auth-field">
3114
+ <span>ชื่อผู้ใช้ <span class="req">*</span></span>
3115
+ <input class="kc-auth-input" type="text" formControlName="username" maxlength="50" autocomplete="username"
3116
+ [class.invalid]="bad('username')" />
3117
+ @if (bad('username')) {
3118
+ <small class="kc-auth-field-error">* กรุณากรอกชื่อผู้ใช้ (ห้ามมีช่องว่าง)</small>
3119
+ }
3120
+ </label>
3121
+
3122
+ <label class="kc-auth-field">
3123
+ <span>รหัสผ่านใหม่ <small class="kc-auth-hint">(อย่างน้อย {{ config.passwordMinLength }} ตัวอักษร — เว้นว่างถ้าไม่เปลี่ยน)</small></span>
3124
+ <kc-password><input class="kc-auth-input" type="password" formControlName="password" maxlength="50" autocomplete="new-password" /></kc-password>
3125
+ </label>
3126
+
3127
+ <label class="kc-auth-field">
3128
+ <span>ยืนยันรหัสผ่านใหม่</span>
3129
+ <kc-password><input class="kc-auth-input" type="password" formControlName="confirmPassword" maxlength="50" autocomplete="new-password" /></kc-password>
3130
+ </label>
3131
+
3132
+ @if (error()) {
3133
+ <div class="kc-auth-alert error" role="alert">{{ error() }}</div>
3134
+ }
3135
+ @if (success()) {
3136
+ <div class="kc-auth-alert success" role="status">{{ success() }}</div>
3137
+ }
3138
+
3139
+ <button type="submit" class="kc-auth-btn primary block" [disabled]="saving()">
3140
+ {{ saving() ? 'กำลังบันทึก...' : 'บันทึกข้อมูล' }}
3141
+ </button>
3142
+ </form>
3143
+ }
3144
+ </div>
3145
+ </div>
3146
+ `
3147
+ }]
3148
+ }],
3149
+ ctorParameters: () => []
3150
+ });
3151
+ var KcConfigUsersComponent = class KcConfigUsersComponent {
3152
+ config = inject(KC_AUTH_CONFIG);
3153
+ auth = inject(KcAuthService);
3154
+ http = inject(HttpClient);
3155
+ users = signal([], ...ngDevMode ? [{ debugName: "users" }] : /* istanbul ignore next */ []);
3156
+ orgs = signal([], ...ngDevMode ? [{ debugName: "orgs" }] : /* istanbul ignore next */ []);
3157
+ roles = signal([], ...ngDevMode ? [{ debugName: "roles" }] : /* istanbul ignore next */ []);
3158
+ loading = signal(false, ...ngDevMode ? [{ debugName: "loading" }] : /* istanbul ignore next */ []);
3159
+ saving = signal(false, ...ngDevMode ? [{ debugName: "saving" }] : /* istanbul ignore next */ []);
3160
+ error = signal("", ...ngDevMode ? [{ debugName: "error" }] : /* istanbul ignore next */ []);
3161
+ success = signal("", ...ngDevMode ? [{ debugName: "success" }] : /* istanbul ignore next */ []);
3162
+ editError = signal("", ...ngDevMode ? [{ debugName: "editError" }] : /* istanbul ignore next */ []);
3163
+ search = signal("", ...ngDevMode ? [{ debugName: "search" }] : /* istanbul ignore next */ []);
3164
+ editing = signal(null, ...ngDevMode ? [{ debugName: "editing" }] : /* istanbul ignore next */ []);
3165
+ myId = computed(() => this.auth.user()?.id ?? "", ...ngDevMode ? [{ debugName: "myId" }] : /* istanbul ignore next */ []);
3166
+ orgOptions = computed(() => [{
3167
+ value: "",
3168
+ label: "-- ไม่ระบุ --"
3169
+ }, ...this.orgs()], ...ngDevMode ? [{ debugName: "orgOptions" }] : /* istanbul ignore next */ []);
3170
+ filtered = computed(() => {
3171
+ const words = this.search().trim().toLowerCase().split(/\s+/).filter(Boolean);
3172
+ if (!words.length) return this.users();
3173
+ return this.users().filter((u) => {
3174
+ const text = [
3175
+ u.username,
3176
+ u.fullName,
3177
+ u.orgName,
3178
+ u.org,
3179
+ this.roleName(u.role),
3180
+ u.email,
3181
+ u.mobile
3182
+ ].join(" ").toLowerCase();
3183
+ return words.every((w) => text.includes(w));
3184
+ });
3185
+ }, ...ngDevMode ? [{ debugName: "filtered" }] : /* istanbul ignore next */ []);
3186
+ constructor() {
3187
+ ensureAuthStyles();
3188
+ }
3189
+ ngOnInit() {
3190
+ this.load();
3191
+ }
3192
+ load() {
3193
+ this.loading.set(true);
3194
+ this.http.get(`${this.config.apiBase}/config/users`).subscribe({
3195
+ next: (res) => {
3196
+ this.users.set(res.users ?? []);
3197
+ this.orgs.set(res.orgs ?? []);
3198
+ this.roles.set(res.roles ?? []);
3199
+ this.loading.set(false);
3200
+ },
3201
+ error: (err) => {
3202
+ this.loading.set(false);
3203
+ this.error.set(kcErrorMessage(err, "ไม่สามารถโหลดรายชื่อผู้ใช้ได้"));
3204
+ }
3205
+ });
3206
+ }
3207
+ roleName(role) {
3208
+ return this.roles().find((r) => r.value === role)?.label ?? (role || "-");
3209
+ }
3210
+ hasRole(role) {
3211
+ return this.roles().some((r) => r.value === role);
3212
+ }
3213
+ edit(u) {
3214
+ this.editError.set("");
3215
+ this.editing.set({
3216
+ id: u.id,
3217
+ fullName: u.fullName,
3218
+ username: u.username,
3219
+ role: u.role,
3220
+ active: u.active,
3221
+ org: u.org,
3222
+ password: "",
3223
+ confirmPassword: ""
3224
+ });
3225
+ }
3226
+ close() {
3227
+ if (!this.saving()) this.editing.set(null);
3228
+ }
3229
+ save() {
3230
+ const e = this.editing();
3231
+ if (!e || this.saving()) return;
3232
+ const problem = kcUsernameProblem(e.username) || kcPasswordProblem(e.password, e.confirmPassword, this.config.passwordMinLength, false);
3233
+ this.editError.set(problem);
3234
+ if (problem) return;
3235
+ const body = {
3236
+ username: e.username.trim(),
3237
+ password: e.password,
3238
+ role: e.role,
3239
+ active: e.active,
3240
+ org: e.org
3241
+ };
3242
+ this.saving.set(true);
3243
+ this.http.put(`${this.config.apiBase}/config/users/${encodeURIComponent(e.id)}`, body).subscribe({
3244
+ next: (res) => {
3245
+ this.saving.set(false);
3246
+ this.editing.set(null);
3247
+ this.error.set("");
3248
+ this.success.set(res?.message || "บันทึกข้อมูลเรียบร้อยแล้ว");
3249
+ this.load();
3250
+ },
3251
+ error: (err) => {
3252
+ this.saving.set(false);
3253
+ this.editError.set(kcErrorMessage(err, "บันทึกข้อมูลไม่สำเร็จ"));
3254
+ }
3255
+ });
3256
+ }
3257
+ static ɵfac = i0.ɵɵngDeclareFactory({
3258
+ minVersion: "12.0.0",
3259
+ version: "22.2.1",
3260
+ ngImport: i0,
3261
+ type: KcConfigUsersComponent,
3262
+ deps: [],
3263
+ target: i0.ɵɵFactoryTarget.Component
3264
+ });
3265
+ static ɵcmp = i0.ɵɵngDeclareComponent({
3266
+ minVersion: "17.0.0",
3267
+ version: "22.2.1",
3268
+ type: KcConfigUsersComponent,
3269
+ isStandalone: true,
3270
+ selector: "kc-config-users",
3271
+ host: { listeners: { "document:keydown.escape": "close()" } },
3272
+ ngImport: i0,
3273
+ template: `
3274
+ <div class="kc-auth-page">
3275
+ <div class="kc-auth-page-header">
3276
+ <div>
3277
+ <h1 class="kc-auth-page-title">ผู้ใช้งานระบบ</h1>
3278
+ <p class="kc-auth-page-subtitle">คลิกแถวเพื่อแก้ไข — ทั้งหมด {{ users().length }} บัญชี</p>
3279
+ </div>
3280
+ </div>
3281
+
3282
+ @if (error()) {
3283
+ <div class="kc-auth-alert error" role="alert">{{ error() }}</div>
3284
+ }
3285
+ @if (success()) {
3286
+ <div class="kc-auth-alert success" role="status">{{ success() }}</div>
3287
+ }
3288
+
3289
+ <div class="kc-auth-panel">
3290
+ <input class="kc-auth-input" type="search" [placeholder]="'ค้นหาชื่อผู้ใช้ ชื่อ-สกุล ' + config.orgLabel + ' ระดับผู้ใช้ หรืออีเมล'"
3291
+ [ngModel]="search()" (ngModelChange)="search.set($event)" aria-label="ค้นหาผู้ใช้" />
3292
+ </div>
3293
+
3294
+ <div class="kc-auth-panel flush">
3295
+ @if (loading()) {
3296
+ <div class="kc-auth-loading">กำลังโหลดข้อมูล...</div>
3297
+ }
3298
+ <div class="kc-auth-table-wrap">
3299
+ <table class="kc-auth-table stack">
3300
+ <thead>
3301
+ <tr>
3302
+ <th>ชื่อผู้ใช้</th>
3303
+ <th>ชื่อ-สกุล</th>
3304
+ <th>{{ config.orgLabel }}</th>
3305
+ <th>ระดับผู้ใช้</th>
3306
+ <th class="c">สถานะ</th>
3307
+ <th>เข้าระบบล่าสุด</th>
3308
+ </tr>
3309
+ </thead>
3310
+ <tbody>
3311
+ @for (u of filtered(); track u.id) {
3312
+ <tr class="clickable" [class.inactive]="!u.active" (click)="edit(u)">
3313
+ <td><span class="kc-auth-link">{{ u.username || '-' }}</span>
3314
+ @if (u.id === myId()) { <span class="kc-auth-hint"> (ตัวคุณ)</span> }</td>
3315
+ <td data-label="ชื่อ-สกุล">{{ u.fullName || '-' }}</td>
3316
+ <td [attr.data-label]="config.orgLabel">{{ u.orgName || u.org || '-' }}</td>
3317
+ <td data-label="ระดับผู้ใช้">{{ roleName(u.role) }}</td>
3318
+ <td class="c" data-label="สถานะ">
3319
+ <span class="kc-auth-badge" [class.on]="u.active" [class.off]="!u.active">{{ u.active ? 'ใช้งาน' : 'ระงับ' }}</span>
3320
+ </td>
3321
+ <td data-label="เข้าระบบล่าสุด">{{ u.lastLogin || '-' }}</td>
3322
+ </tr>
3323
+ } @empty {
3324
+ @if (!loading()) {
3325
+ <tr><td colspan="6" class="empty">ไม่พบผู้ใช้</td></tr>
3326
+ }
3327
+ }
3328
+ </tbody>
3329
+ </table>
3330
+ </div>
3331
+ </div>
3332
+ </div>
3333
+
3334
+ @if (editing(); as e) {
3335
+ <div class="kc-auth-modal-bg" (click)="close()">
3336
+ <div class="kc-auth-modal" role="dialog" aria-modal="true" aria-labelledby="kc-cu-title" (click)="$event.stopPropagation()">
3337
+ <div class="kc-auth-modal-header">
3338
+ <span id="kc-cu-title">แก้ไขผู้ใช้ — {{ e.fullName || e.username }}</span>
3339
+ <button type="button" class="kc-auth-close" aria-label="ปิด" (click)="close()">&times;</button>
3340
+ </div>
3341
+ <div class="kc-auth-modal-body">
3342
+ @if (editError()) {
3343
+ <div class="kc-auth-alert error" role="alert">{{ editError() }}</div>
3344
+ }
3345
+ <div class="kc-auth-grid">
3346
+ <label class="kc-auth-field full">
3347
+ <span>ชื่อผู้ใช้ <span class="req">*</span></span>
3348
+ <input class="kc-auth-input" type="text" [(ngModel)]="e.username" maxlength="50" autocomplete="off" />
3349
+ </label>
3350
+ <label class="kc-auth-field">
3351
+ <span>ระดับผู้ใช้</span>
3352
+ <select class="kc-auth-input" [(ngModel)]="e.role" [disabled]="e.id === myId()">
3353
+ @if (!hasRole(e.role)) {
3354
+ <option [value]="e.role">{{ e.role || '-- ไม่ระบุ --' }}</option>
3355
+ }
3356
+ @for (r of roles(); track r.value) {
3357
+ <option [value]="r.value">{{ r.label }}</option>
3358
+ }
3359
+ </select>
3360
+ </label>
3361
+ <label class="kc-auth-field">
3362
+ <span>สถานะ</span>
3363
+ <select class="kc-auth-input" [(ngModel)]="e.active" [disabled]="e.id === myId()">
3364
+ <option [ngValue]="true">ใช้งาน</option>
3365
+ <option [ngValue]="false">ระงับการใช้งาน</option>
3366
+ </select>
3367
+ </label>
3368
+ @if (orgs().length) {
3369
+ <div class="kc-auth-field full">
3370
+ <label class="kc-auth-label" for="kc-cu-org">{{ config.orgLabel }}</label>
3371
+ <app-search-select inputId="kc-cu-org" [options]="orgOptions()" [(ngModel)]="e.org" />
3372
+ </div>
3373
+ }
3374
+ <label class="kc-auth-field">
3375
+ <span>รหัสผ่านใหม่</span>
3376
+ <kc-password><input class="kc-auth-input" type="password" [(ngModel)]="e.password" autocomplete="new-password" /></kc-password>
3377
+ </label>
3378
+ <label class="kc-auth-field">
3379
+ <span>ยืนยันรหัสผ่านใหม่</span>
3380
+ <kc-password><input class="kc-auth-input" type="password" [(ngModel)]="e.confirmPassword" autocomplete="new-password" /></kc-password>
3381
+ </label>
3382
+ </div>
3383
+ <p class="kc-auth-hint">
3384
+ เว้นว่างรหัสผ่านไว้ถ้าไม่ต้องการเปลี่ยน — ระดับผู้ใช้และ{{ config.orgLabel }}ที่เปลี่ยนมีผลเมื่อผู้ใช้เข้าสู่ระบบครั้งถัดไป
3385
+ @if (e.id === myId()) { — แก้ระดับหรือระงับบัญชีของตนเองไม่ได้ }
3386
+ </p>
3387
+ <div class="kc-auth-actions">
3388
+ <button type="button" class="kc-auth-btn" (click)="close()">ยกเลิก</button>
3389
+ <button type="button" class="kc-auth-btn primary" [disabled]="saving()" (click)="save()">
3390
+ {{ saving() ? 'กำลังบันทึก...' : 'บันทึก' }}
3391
+ </button>
3392
+ </div>
3393
+ </div>
3394
+ </div>
3395
+ </div>
3396
+ }
3397
+ `,
3398
+ isInline: true,
3399
+ dependencies: [
3400
+ {
3401
+ kind: "ngmodule",
3402
+ type: FormsModule
3403
+ },
3404
+ {
3405
+ kind: "directive",
3406
+ type: i1.NgSelectOption,
3407
+ selector: "option",
3408
+ inputs: ["ngValue", "value"]
3409
+ },
3410
+ {
3411
+ kind: "directive",
3412
+ type: i1.ɵNgSelectMultipleOption,
3413
+ selector: "option",
3414
+ inputs: ["ngValue", "value"]
3415
+ },
3416
+ {
3417
+ kind: "directive",
3418
+ type: i1.DefaultValueAccessor,
3419
+ selector: "input:not([type=checkbox]):not([ngNoCva])[formControlName],textarea:not([ngNoCva])[formControlName],input:not([type=checkbox]):not([ngNoCva])[formControl],textarea:not([ngNoCva])[formControl],input:not([type=checkbox]):not([ngNoCva])[ngModel],textarea:not([ngNoCva])[ngModel],[ngDefaultControl]"
3420
+ },
3421
+ {
3422
+ kind: "directive",
3423
+ type: i1.SelectControlValueAccessor,
3424
+ selector: "select:not([multiple]):not([ngNoCva])[formControlName],select:not([multiple]):not([ngNoCva])[formControl],select:not([multiple]):not([ngNoCva])[ngModel]",
3425
+ inputs: ["compareWith"]
3426
+ },
3427
+ {
3428
+ kind: "directive",
3429
+ type: i1.NgControlStatus,
3430
+ selector: "[formControlName],[ngModel],[formControl]"
3431
+ },
3432
+ {
3433
+ kind: "directive",
3434
+ type: i1.MaxLengthValidator,
3435
+ selector: "[maxlength][formControlName],[maxlength][formControl],[maxlength][ngModel]",
3436
+ inputs: ["maxlength"]
3437
+ },
3438
+ {
3439
+ kind: "directive",
3440
+ type: i1.NgModel,
3441
+ selector: "[ngModel]:not([formControlName]):not([formControl])",
3442
+ inputs: [
3443
+ "name",
3444
+ "disabled",
3445
+ "ngModel",
3446
+ "ngModelOptions"
3447
+ ],
3448
+ outputs: ["ngModelChange"],
3449
+ exportAs: ["ngModel"]
3450
+ },
3451
+ {
3452
+ kind: "component",
3453
+ type: SearchSelectComponent,
3454
+ selector: "app-search-select",
3455
+ inputs: [
3456
+ "options",
3457
+ "placeholder",
3458
+ "searchHint",
3459
+ "inputId"
3460
+ ]
3461
+ },
3462
+ {
3463
+ kind: "component",
3464
+ type: KcPasswordToggleComponent,
3465
+ selector: "kc-password"
3466
+ }
3467
+ ]
3468
+ });
3469
+ };
3470
+ i0.ɵɵngDeclareClassMetadata({
3471
+ minVersion: "12.0.0",
3472
+ version: "22.2.1",
3473
+ ngImport: i0,
3474
+ type: KcConfigUsersComponent,
3475
+ decorators: [{
3476
+ type: Component,
3477
+ args: [{
3478
+ selector: "kc-config-users",
3479
+ standalone: true,
3480
+ imports: [
3481
+ FormsModule,
3482
+ SearchSelectComponent,
3483
+ KcPasswordToggleComponent
3484
+ ],
3485
+ host: { "(document:keydown.escape)": "close()" },
3486
+ template: `
3487
+ <div class="kc-auth-page">
3488
+ <div class="kc-auth-page-header">
3489
+ <div>
3490
+ <h1 class="kc-auth-page-title">ผู้ใช้งานระบบ</h1>
3491
+ <p class="kc-auth-page-subtitle">คลิกแถวเพื่อแก้ไข — ทั้งหมด {{ users().length }} บัญชี</p>
3492
+ </div>
3493
+ </div>
3494
+
3495
+ @if (error()) {
3496
+ <div class="kc-auth-alert error" role="alert">{{ error() }}</div>
3497
+ }
3498
+ @if (success()) {
3499
+ <div class="kc-auth-alert success" role="status">{{ success() }}</div>
3500
+ }
3501
+
3502
+ <div class="kc-auth-panel">
3503
+ <input class="kc-auth-input" type="search" [placeholder]="'ค้นหาชื่อผู้ใช้ ชื่อ-สกุล ' + config.orgLabel + ' ระดับผู้ใช้ หรืออีเมล'"
3504
+ [ngModel]="search()" (ngModelChange)="search.set($event)" aria-label="ค้นหาผู้ใช้" />
3505
+ </div>
3506
+
3507
+ <div class="kc-auth-panel flush">
3508
+ @if (loading()) {
3509
+ <div class="kc-auth-loading">กำลังโหลดข้อมูล...</div>
3510
+ }
3511
+ <div class="kc-auth-table-wrap">
3512
+ <table class="kc-auth-table stack">
3513
+ <thead>
3514
+ <tr>
3515
+ <th>ชื่อผู้ใช้</th>
3516
+ <th>ชื่อ-สกุล</th>
3517
+ <th>{{ config.orgLabel }}</th>
3518
+ <th>ระดับผู้ใช้</th>
3519
+ <th class="c">สถานะ</th>
3520
+ <th>เข้าระบบล่าสุด</th>
3521
+ </tr>
3522
+ </thead>
3523
+ <tbody>
3524
+ @for (u of filtered(); track u.id) {
3525
+ <tr class="clickable" [class.inactive]="!u.active" (click)="edit(u)">
3526
+ <td><span class="kc-auth-link">{{ u.username || '-' }}</span>
3527
+ @if (u.id === myId()) { <span class="kc-auth-hint"> (ตัวคุณ)</span> }</td>
3528
+ <td data-label="ชื่อ-สกุล">{{ u.fullName || '-' }}</td>
3529
+ <td [attr.data-label]="config.orgLabel">{{ u.orgName || u.org || '-' }}</td>
3530
+ <td data-label="ระดับผู้ใช้">{{ roleName(u.role) }}</td>
3531
+ <td class="c" data-label="สถานะ">
3532
+ <span class="kc-auth-badge" [class.on]="u.active" [class.off]="!u.active">{{ u.active ? 'ใช้งาน' : 'ระงับ' }}</span>
3533
+ </td>
3534
+ <td data-label="เข้าระบบล่าสุด">{{ u.lastLogin || '-' }}</td>
3535
+ </tr>
3536
+ } @empty {
3537
+ @if (!loading()) {
3538
+ <tr><td colspan="6" class="empty">ไม่พบผู้ใช้</td></tr>
3539
+ }
3540
+ }
3541
+ </tbody>
3542
+ </table>
3543
+ </div>
3544
+ </div>
3545
+ </div>
3546
+
3547
+ @if (editing(); as e) {
3548
+ <div class="kc-auth-modal-bg" (click)="close()">
3549
+ <div class="kc-auth-modal" role="dialog" aria-modal="true" aria-labelledby="kc-cu-title" (click)="$event.stopPropagation()">
3550
+ <div class="kc-auth-modal-header">
3551
+ <span id="kc-cu-title">แก้ไขผู้ใช้ — {{ e.fullName || e.username }}</span>
3552
+ <button type="button" class="kc-auth-close" aria-label="ปิด" (click)="close()">&times;</button>
3553
+ </div>
3554
+ <div class="kc-auth-modal-body">
3555
+ @if (editError()) {
3556
+ <div class="kc-auth-alert error" role="alert">{{ editError() }}</div>
3557
+ }
3558
+ <div class="kc-auth-grid">
3559
+ <label class="kc-auth-field full">
3560
+ <span>ชื่อผู้ใช้ <span class="req">*</span></span>
3561
+ <input class="kc-auth-input" type="text" [(ngModel)]="e.username" maxlength="50" autocomplete="off" />
3562
+ </label>
3563
+ <label class="kc-auth-field">
3564
+ <span>ระดับผู้ใช้</span>
3565
+ <select class="kc-auth-input" [(ngModel)]="e.role" [disabled]="e.id === myId()">
3566
+ @if (!hasRole(e.role)) {
3567
+ <option [value]="e.role">{{ e.role || '-- ไม่ระบุ --' }}</option>
3568
+ }
3569
+ @for (r of roles(); track r.value) {
3570
+ <option [value]="r.value">{{ r.label }}</option>
3571
+ }
3572
+ </select>
3573
+ </label>
3574
+ <label class="kc-auth-field">
3575
+ <span>สถานะ</span>
3576
+ <select class="kc-auth-input" [(ngModel)]="e.active" [disabled]="e.id === myId()">
3577
+ <option [ngValue]="true">ใช้งาน</option>
3578
+ <option [ngValue]="false">ระงับการใช้งาน</option>
3579
+ </select>
3580
+ </label>
3581
+ @if (orgs().length) {
3582
+ <div class="kc-auth-field full">
3583
+ <label class="kc-auth-label" for="kc-cu-org">{{ config.orgLabel }}</label>
3584
+ <app-search-select inputId="kc-cu-org" [options]="orgOptions()" [(ngModel)]="e.org" />
3585
+ </div>
3586
+ }
3587
+ <label class="kc-auth-field">
3588
+ <span>รหัสผ่านใหม่</span>
3589
+ <kc-password><input class="kc-auth-input" type="password" [(ngModel)]="e.password" autocomplete="new-password" /></kc-password>
3590
+ </label>
3591
+ <label class="kc-auth-field">
3592
+ <span>ยืนยันรหัสผ่านใหม่</span>
3593
+ <kc-password><input class="kc-auth-input" type="password" [(ngModel)]="e.confirmPassword" autocomplete="new-password" /></kc-password>
3594
+ </label>
3595
+ </div>
3596
+ <p class="kc-auth-hint">
3597
+ เว้นว่างรหัสผ่านไว้ถ้าไม่ต้องการเปลี่ยน — ระดับผู้ใช้และ{{ config.orgLabel }}ที่เปลี่ยนมีผลเมื่อผู้ใช้เข้าสู่ระบบครั้งถัดไป
3598
+ @if (e.id === myId()) { — แก้ระดับหรือระงับบัญชีของตนเองไม่ได้ }
3599
+ </p>
3600
+ <div class="kc-auth-actions">
3601
+ <button type="button" class="kc-auth-btn" (click)="close()">ยกเลิก</button>
3602
+ <button type="button" class="kc-auth-btn primary" [disabled]="saving()" (click)="save()">
3603
+ {{ saving() ? 'กำลังบันทึก...' : 'บันทึก' }}
3604
+ </button>
3605
+ </div>
3606
+ </div>
3607
+ </div>
3608
+ </div>
3609
+ }
3610
+ `
3611
+ }]
3612
+ }],
3613
+ ctorParameters: () => []
3614
+ });
3615
+ var KcConfigKclluComponent = class KcConfigKclluComponent {
3616
+ config = inject(KC_AUTH_CONFIG);
3617
+ http = inject(HttpClient);
3618
+ api = `${this.config.apiBase}/config/kcllu`;
3619
+ titles = signal([], ...ngDevMode ? [{ debugName: "titles" }] : /* istanbul ignore next */ []);
3620
+ title = signal("", ...ngDevMode ? [{ debugName: "title" }] : /* istanbul ignore next */ []);
3621
+ rows = signal([], ...ngDevMode ? [{ debugName: "rows" }] : /* istanbul ignore next */ []);
3622
+ loading = signal(false, ...ngDevMode ? [{ debugName: "loading" }] : /* istanbul ignore next */ []);
3623
+ saving = signal(false, ...ngDevMode ? [{ debugName: "saving" }] : /* istanbul ignore next */ []);
3624
+ showInactive = signal(true, ...ngDevMode ? [{ debugName: "showInactive" }] : /* istanbul ignore next */ []);
3625
+ error = signal("", ...ngDevMode ? [{ debugName: "error" }] : /* istanbul ignore next */ []);
3626
+ success = signal("", ...ngDevMode ? [{ debugName: "success" }] : /* istanbul ignore next */ []);
3627
+ editError = signal("", ...ngDevMode ? [{ debugName: "editError" }] : /* istanbul ignore next */ []);
3628
+ editing = signal(null, ...ngDevMode ? [{ debugName: "editing" }] : /* istanbul ignore next */ []);
3629
+ titleOptions = computed(() => this.titles().map((t) => ({
3630
+ value: t.title,
3631
+ label: `${t.title} (${t.count})${t.readOnly ? " — ดูอย่างเดียว" : ""}`
3632
+ })), ...ngDevMode ? [{ debugName: "titleOptions" }] : /* istanbul ignore next */ []);
3633
+ readOnly = computed(() => this.titles().find((t) => t.title === this.title())?.readOnly ?? false, ...ngDevMode ? [{ debugName: "readOnly" }] : /* istanbul ignore next */ []);
3634
+ hint = computed(() => this.config.kclluHints[this.title()] ?? "", ...ngDevMode ? [{ debugName: "hint" }] : /* istanbul ignore next */ []);
3635
+ visibleRows = computed(() => this.showInactive() ? this.rows() : this.rows().filter((r) => r.active), ...ngDevMode ? [{ debugName: "visibleRows" }] : /* istanbul ignore next */ []);
3636
+ constructor() {
3637
+ ensureAuthStyles();
3638
+ }
3639
+ ngOnInit() {
3640
+ this.loadTitles();
3641
+ }
3642
+ loadTitles() {
3643
+ this.http.get(`${this.api}/titles`).subscribe({
3644
+ next: (res) => {
3645
+ this.titles.set(res ?? []);
3646
+ if (!this.title() && this.titles().length) this.select(this.titles()[0].title);
3647
+ },
3648
+ error: (err) => this.error.set(kcErrorMessage(err, "ไม่สามารถโหลดกลุ่มข้อมูลได้"))
3649
+ });
3650
+ }
3651
+ select(title) {
3652
+ if (!title || title === this.title()) return;
3653
+ this.title.set(title);
3654
+ this.success.set("");
3655
+ this.loadRows();
3656
+ }
3657
+ loadRows() {
3658
+ const title = this.title();
3659
+ this.loading.set(true);
3660
+ this.http.get(this.api, { params: { title } }).subscribe({
3661
+ next: (res) => {
3662
+ if (title !== this.title()) return;
3663
+ this.rows.set(res ?? []);
3664
+ this.loading.set(false);
3665
+ },
3666
+ error: (err) => {
3667
+ this.loading.set(false);
3668
+ this.error.set(kcErrorMessage(err, "ไม่สามารถโหลดข้อมูลได้"));
3669
+ }
3670
+ });
3671
+ }
3672
+ add() {
3673
+ this.editError.set("");
3674
+ this.editing.set({
3675
+ id: 0,
3676
+ code: "",
3677
+ name: "",
3678
+ name1: "",
3679
+ value: "",
3680
+ grp: "",
3681
+ no: "",
3682
+ mark: "",
3683
+ active: true
3684
+ });
3685
+ }
3686
+ edit(row) {
3687
+ if (this.readOnly()) return;
3688
+ this.editError.set("");
3689
+ this.editing.set({ ...row });
3690
+ }
3691
+ close() {
3692
+ if (!this.saving()) this.editing.set(null);
3693
+ }
3694
+ save() {
3695
+ const e = this.editing();
3696
+ if (!e || this.saving()) return;
3697
+ const no = String(e.no ?? "").trim();
3698
+ const problem = !e.name.trim() ? "กรุณากรอกชื่อ (k_name)" : !/^\d*$/.test(no) ? "ลำดับ (k_no) ต้องเป็นตัวเลข" : "";
3699
+ this.editError.set(problem);
3700
+ if (problem) return;
3701
+ const body = {
3702
+ ...e,
3703
+ no,
3704
+ name: e.name.trim(),
3705
+ title: this.title()
3706
+ };
3707
+ const req = e.id ? this.http.put(`${this.api}/${e.id}`, body) : this.http.post(this.api, body);
3708
+ this.saving.set(true);
3709
+ req.subscribe({
3710
+ next: (res) => {
3711
+ this.saving.set(false);
3712
+ this.editing.set(null);
3713
+ this.error.set("");
3714
+ this.success.set(res?.message || "บันทึกข้อมูลเรียบร้อยแล้ว");
3715
+ this.loadRows();
3716
+ if (!e.id) this.loadTitles();
3717
+ },
3718
+ error: (err) => {
3719
+ this.saving.set(false);
3720
+ this.editError.set(kcErrorMessage(err, "บันทึกข้อมูลไม่สำเร็จ"));
3721
+ }
3722
+ });
3723
+ }
3724
+ static ɵfac = i0.ɵɵngDeclareFactory({
3725
+ minVersion: "12.0.0",
3726
+ version: "22.2.1",
3727
+ ngImport: i0,
3728
+ type: KcConfigKclluComponent,
3729
+ deps: [],
3730
+ target: i0.ɵɵFactoryTarget.Component
3731
+ });
3732
+ static ɵcmp = i0.ɵɵngDeclareComponent({
3733
+ minVersion: "17.0.0",
3734
+ version: "22.2.1",
3735
+ type: KcConfigKclluComponent,
3736
+ isStandalone: true,
3737
+ selector: "kc-config-kcllu",
3738
+ host: { listeners: { "document:keydown.escape": "close()" } },
3739
+ ngImport: i0,
3740
+ template: `
3741
+ <div class="kc-auth-page">
3742
+ <div class="kc-auth-page-header">
3743
+ <div>
3744
+ <h1 class="kc-auth-page-title">ข้อมูลอ้างอิง (kcllu)</h1>
3745
+ <p class="kc-auth-page-subtitle">เลือกกลุ่มข้อมูล แล้วคลิกแถวเพื่อแก้ไข — ไม่มีการลบ ใช้ "ปิดใช้งาน" แทน</p>
3746
+ </div>
3747
+ @if (title() && !readOnly()) {
3748
+ <button type="button" class="kc-auth-btn primary" (click)="add()">+ เพิ่มข้อมูล</button>
3749
+ }
3750
+ </div>
3751
+
3752
+ @if (error()) {
3753
+ <div class="kc-auth-alert error" role="alert">{{ error() }}</div>
3754
+ }
3755
+ @if (success()) {
3756
+ <div class="kc-auth-alert success" role="status">{{ success() }}</div>
3757
+ }
3758
+
3759
+ <div class="kc-auth-panel">
3760
+ <div class="kc-auth-grid">
3761
+ <div class="kc-auth-field" style="margin-bottom: 0">
3762
+ <label class="kc-auth-label" for="kc-kl-title">กลุ่มข้อมูล (k_title)</label>
3763
+ <app-search-select inputId="kc-kl-title" [options]="titleOptions()" placeholder="-- เลือกกลุ่ม --"
3764
+ [ngModel]="title()" (ngModelChange)="select($event)" />
3765
+ </div>
3766
+ <div class="kc-auth-field" style="margin-bottom: 0; justify-content: flex-end">
3767
+ <label class="kc-auth-check">
3768
+ <input type="checkbox" [ngModel]="showInactive()" (ngModelChange)="showInactive.set($event)" />
3769
+ แสดงรายการที่ปิดใช้งาน
3770
+ </label>
3771
+ </div>
3772
+ </div>
3773
+ @if (hint()) {
3774
+ <p class="kc-auth-hint" style="margin-top: 10px">{{ hint() }}</p>
3775
+ }
3776
+ @if (readOnly()) {
3777
+ <p class="kc-auth-hint" style="margin-top: 6px">กลุ่มนี้ดูได้อย่างเดียว</p>
3778
+ }
3779
+ </div>
3780
+
3781
+ <div class="kc-auth-panel flush">
3782
+ @if (loading()) {
3783
+ <div class="kc-auth-loading">กำลังโหลดข้อมูล...</div>
3784
+ }
3785
+ <div class="kc-auth-table-wrap">
3786
+ <table class="kc-auth-table stack">
3787
+ <thead>
3788
+ <tr>
3789
+ <th>ลำดับ</th>
3790
+ <th>รหัส (k_code)</th>
3791
+ <th style="width: 30%">ชื่อ (k_name)</th>
3792
+ <th>ค่า (k_value)</th>
3793
+ <th>กลุ่มย่อย (k_grp)</th>
3794
+ <th>k_mark</th>
3795
+ <th class="c">สถานะ</th>
3796
+ </tr>
3797
+ </thead>
3798
+ <tbody>
3799
+ @for (r of visibleRows(); track r.id) {
3800
+ <tr [class.clickable]="!readOnly()" [class.inactive]="!r.active" (click)="edit(r)">
3801
+ <td data-label="ลำดับ">{{ r.no || '-' }}</td>
3802
+ <td data-label="รหัส">{{ r.code || '-' }}</td>
3803
+ <td data-label="ชื่อ"><span [class.kc-auth-link]="!readOnly()">{{ r.name || '-' }}</span>
3804
+ @if (r.name1) { <div class="kc-auth-hint">{{ r.name1 }}</div> }</td>
3805
+ <td data-label="ค่า">{{ r.value || '-' }}</td>
3806
+ <td data-label="กลุ่มย่อย">{{ r.grp || '-' }}</td>
3807
+ <td data-label="k_mark">{{ r.mark || '-' }}</td>
3808
+ <td class="c" data-label="สถานะ">
3809
+ <span class="kc-auth-badge" [class.on]="r.active" [class.off]="!r.active">{{ r.active ? 'ใช้งาน' : 'ปิดใช้งาน' }}</span>
3810
+ </td>
3811
+ </tr>
3812
+ } @empty {
3813
+ @if (!loading()) {
3814
+ <tr><td colspan="7" class="empty">{{ title() ? 'ไม่พบข้อมูล' : 'กรุณาเลือกกลุ่มข้อมูล' }}</td></tr>
3815
+ }
3816
+ }
3817
+ </tbody>
3818
+ </table>
3819
+ </div>
3820
+ </div>
3821
+ </div>
3822
+
3823
+ @if (editing(); as e) {
3824
+ <div class="kc-auth-modal-bg" (click)="close()">
3825
+ <div class="kc-auth-modal" role="dialog" aria-modal="true" aria-labelledby="kc-kl-edit" (click)="$event.stopPropagation()">
3826
+ <div class="kc-auth-modal-header">
3827
+ <span id="kc-kl-edit">{{ e.id ? 'แก้ไขข้อมูล' : 'เพิ่มข้อมูล' }} — กลุ่ม {{ title() }}</span>
3828
+ <button type="button" class="kc-auth-close" aria-label="ปิด" (click)="close()">&times;</button>
3829
+ </div>
3830
+ <div class="kc-auth-modal-body">
3831
+ @if (editError()) {
3832
+ <div class="kc-auth-alert error" role="alert">{{ editError() }}</div>
3833
+ }
3834
+ @if (hint()) {
3835
+ <p class="kc-auth-hint" style="margin: 0 0 12px">{{ hint() }}</p>
3836
+ }
3837
+ <div class="kc-auth-grid">
3838
+ <label class="kc-auth-field">
3839
+ <span>รหัส (k_code)</span>
3840
+ <input class="kc-auth-input" type="text" [(ngModel)]="e.code" maxlength="255" />
3841
+ </label>
3842
+ <label class="kc-auth-field">
3843
+ <span>ลำดับ (k_no)</span>
3844
+ <input class="kc-auth-input" type="text" inputmode="numeric" [(ngModel)]="e.no" maxlength="9" />
3845
+ </label>
3846
+ <label class="kc-auth-field full">
3847
+ <span>ชื่อ (k_name) <span class="req">*</span></span>
3848
+ <input class="kc-auth-input" type="text" [(ngModel)]="e.name" maxlength="255" />
3849
+ </label>
3850
+ <label class="kc-auth-field full">
3851
+ <span>ชื่อที่ 2 (k_name1)</span>
3852
+ <input class="kc-auth-input" type="text" [(ngModel)]="e.name1" maxlength="255" />
3853
+ </label>
3854
+ <label class="kc-auth-field">
3855
+ <span>ค่า (k_value)</span>
3856
+ <input class="kc-auth-input" type="text" [(ngModel)]="e.value" maxlength="255" />
3857
+ </label>
3858
+ <label class="kc-auth-field">
3859
+ <span>กลุ่มย่อย (k_grp)</span>
3860
+ <input class="kc-auth-input" type="text" [(ngModel)]="e.grp" maxlength="255" />
3861
+ </label>
3862
+ <label class="kc-auth-field">
3863
+ <span>k_mark</span>
3864
+ <input class="kc-auth-input" type="text" [(ngModel)]="e.mark" maxlength="255" />
3865
+ </label>
3866
+ <label class="kc-auth-field">
3867
+ <span>สถานะ</span>
3868
+ <select class="kc-auth-input" [(ngModel)]="e.active">
3869
+ <option [ngValue]="true">ใช้งาน</option>
3870
+ <option [ngValue]="false">ปิดใช้งาน</option>
3871
+ </select>
3872
+ </label>
3873
+ </div>
3874
+ <div class="kc-auth-actions">
3875
+ <button type="button" class="kc-auth-btn" (click)="close()">ยกเลิก</button>
3876
+ <button type="button" class="kc-auth-btn primary" [disabled]="saving()" (click)="save()">
3877
+ {{ saving() ? 'กำลังบันทึก...' : 'บันทึก' }}
3878
+ </button>
3879
+ </div>
3880
+ </div>
3881
+ </div>
3882
+ </div>
3883
+ }
3884
+ `,
3885
+ isInline: true,
3886
+ dependencies: [
3887
+ {
3888
+ kind: "ngmodule",
3889
+ type: FormsModule
3890
+ },
3891
+ {
3892
+ kind: "directive",
3893
+ type: i1.NgSelectOption,
3894
+ selector: "option",
3895
+ inputs: ["ngValue", "value"]
3896
+ },
3897
+ {
3898
+ kind: "directive",
3899
+ type: i1.ɵNgSelectMultipleOption,
3900
+ selector: "option",
3901
+ inputs: ["ngValue", "value"]
3902
+ },
3903
+ {
3904
+ kind: "directive",
3905
+ type: i1.DefaultValueAccessor,
3906
+ selector: "input:not([type=checkbox]):not([ngNoCva])[formControlName],textarea:not([ngNoCva])[formControlName],input:not([type=checkbox]):not([ngNoCva])[formControl],textarea:not([ngNoCva])[formControl],input:not([type=checkbox]):not([ngNoCva])[ngModel],textarea:not([ngNoCva])[ngModel],[ngDefaultControl]"
3907
+ },
3908
+ {
3909
+ kind: "directive",
3910
+ type: i1.CheckboxControlValueAccessor,
3911
+ selector: "input[type=checkbox]:not([ngNoCva])[formControlName],input[type=checkbox]:not([ngNoCva])[formControl],input[type=checkbox]:not([ngNoCva])[ngModel]"
3912
+ },
3913
+ {
3914
+ kind: "directive",
3915
+ type: i1.SelectControlValueAccessor,
3916
+ selector: "select:not([multiple]):not([ngNoCva])[formControlName],select:not([multiple]):not([ngNoCva])[formControl],select:not([multiple]):not([ngNoCva])[ngModel]",
3917
+ inputs: ["compareWith"]
3918
+ },
3919
+ {
3920
+ kind: "directive",
3921
+ type: i1.NgControlStatus,
3922
+ selector: "[formControlName],[ngModel],[formControl]"
3923
+ },
3924
+ {
3925
+ kind: "directive",
3926
+ type: i1.MaxLengthValidator,
3927
+ selector: "[maxlength][formControlName],[maxlength][formControl],[maxlength][ngModel]",
3928
+ inputs: ["maxlength"]
3929
+ },
3930
+ {
3931
+ kind: "directive",
3932
+ type: i1.NgModel,
3933
+ selector: "[ngModel]:not([formControlName]):not([formControl])",
3934
+ inputs: [
3935
+ "name",
3936
+ "disabled",
3937
+ "ngModel",
3938
+ "ngModelOptions"
3939
+ ],
3940
+ outputs: ["ngModelChange"],
3941
+ exportAs: ["ngModel"]
3942
+ },
3943
+ {
3944
+ kind: "component",
3945
+ type: SearchSelectComponent,
3946
+ selector: "app-search-select",
3947
+ inputs: [
3948
+ "options",
3949
+ "placeholder",
3950
+ "searchHint",
3951
+ "inputId"
3952
+ ]
3953
+ }
3954
+ ]
3955
+ });
3956
+ };
3957
+ i0.ɵɵngDeclareClassMetadata({
3958
+ minVersion: "12.0.0",
3959
+ version: "22.2.1",
3960
+ ngImport: i0,
3961
+ type: KcConfigKclluComponent,
3962
+ decorators: [{
3963
+ type: Component,
3964
+ args: [{
3965
+ selector: "kc-config-kcllu",
3966
+ standalone: true,
3967
+ imports: [FormsModule, SearchSelectComponent],
3968
+ host: { "(document:keydown.escape)": "close()" },
3969
+ template: `
3970
+ <div class="kc-auth-page">
3971
+ <div class="kc-auth-page-header">
3972
+ <div>
3973
+ <h1 class="kc-auth-page-title">ข้อมูลอ้างอิง (kcllu)</h1>
3974
+ <p class="kc-auth-page-subtitle">เลือกกลุ่มข้อมูล แล้วคลิกแถวเพื่อแก้ไข — ไม่มีการลบ ใช้ "ปิดใช้งาน" แทน</p>
3975
+ </div>
3976
+ @if (title() && !readOnly()) {
3977
+ <button type="button" class="kc-auth-btn primary" (click)="add()">+ เพิ่มข้อมูล</button>
3978
+ }
3979
+ </div>
3980
+
3981
+ @if (error()) {
3982
+ <div class="kc-auth-alert error" role="alert">{{ error() }}</div>
3983
+ }
3984
+ @if (success()) {
3985
+ <div class="kc-auth-alert success" role="status">{{ success() }}</div>
3986
+ }
3987
+
3988
+ <div class="kc-auth-panel">
3989
+ <div class="kc-auth-grid">
3990
+ <div class="kc-auth-field" style="margin-bottom: 0">
3991
+ <label class="kc-auth-label" for="kc-kl-title">กลุ่มข้อมูล (k_title)</label>
3992
+ <app-search-select inputId="kc-kl-title" [options]="titleOptions()" placeholder="-- เลือกกลุ่ม --"
3993
+ [ngModel]="title()" (ngModelChange)="select($event)" />
3994
+ </div>
3995
+ <div class="kc-auth-field" style="margin-bottom: 0; justify-content: flex-end">
3996
+ <label class="kc-auth-check">
3997
+ <input type="checkbox" [ngModel]="showInactive()" (ngModelChange)="showInactive.set($event)" />
3998
+ แสดงรายการที่ปิดใช้งาน
3999
+ </label>
4000
+ </div>
4001
+ </div>
4002
+ @if (hint()) {
4003
+ <p class="kc-auth-hint" style="margin-top: 10px">{{ hint() }}</p>
4004
+ }
4005
+ @if (readOnly()) {
4006
+ <p class="kc-auth-hint" style="margin-top: 6px">กลุ่มนี้ดูได้อย่างเดียว</p>
4007
+ }
4008
+ </div>
4009
+
4010
+ <div class="kc-auth-panel flush">
4011
+ @if (loading()) {
4012
+ <div class="kc-auth-loading">กำลังโหลดข้อมูล...</div>
4013
+ }
4014
+ <div class="kc-auth-table-wrap">
4015
+ <table class="kc-auth-table stack">
4016
+ <thead>
4017
+ <tr>
4018
+ <th>ลำดับ</th>
4019
+ <th>รหัส (k_code)</th>
4020
+ <th style="width: 30%">ชื่อ (k_name)</th>
4021
+ <th>ค่า (k_value)</th>
4022
+ <th>กลุ่มย่อย (k_grp)</th>
4023
+ <th>k_mark</th>
4024
+ <th class="c">สถานะ</th>
4025
+ </tr>
4026
+ </thead>
4027
+ <tbody>
4028
+ @for (r of visibleRows(); track r.id) {
4029
+ <tr [class.clickable]="!readOnly()" [class.inactive]="!r.active" (click)="edit(r)">
4030
+ <td data-label="ลำดับ">{{ r.no || '-' }}</td>
4031
+ <td data-label="รหัส">{{ r.code || '-' }}</td>
4032
+ <td data-label="ชื่อ"><span [class.kc-auth-link]="!readOnly()">{{ r.name || '-' }}</span>
4033
+ @if (r.name1) { <div class="kc-auth-hint">{{ r.name1 }}</div> }</td>
4034
+ <td data-label="ค่า">{{ r.value || '-' }}</td>
4035
+ <td data-label="กลุ่มย่อย">{{ r.grp || '-' }}</td>
4036
+ <td data-label="k_mark">{{ r.mark || '-' }}</td>
4037
+ <td class="c" data-label="สถานะ">
4038
+ <span class="kc-auth-badge" [class.on]="r.active" [class.off]="!r.active">{{ r.active ? 'ใช้งาน' : 'ปิดใช้งาน' }}</span>
4039
+ </td>
4040
+ </tr>
4041
+ } @empty {
4042
+ @if (!loading()) {
4043
+ <tr><td colspan="7" class="empty">{{ title() ? 'ไม่พบข้อมูล' : 'กรุณาเลือกกลุ่มข้อมูล' }}</td></tr>
4044
+ }
4045
+ }
4046
+ </tbody>
4047
+ </table>
4048
+ </div>
4049
+ </div>
4050
+ </div>
4051
+
4052
+ @if (editing(); as e) {
4053
+ <div class="kc-auth-modal-bg" (click)="close()">
4054
+ <div class="kc-auth-modal" role="dialog" aria-modal="true" aria-labelledby="kc-kl-edit" (click)="$event.stopPropagation()">
4055
+ <div class="kc-auth-modal-header">
4056
+ <span id="kc-kl-edit">{{ e.id ? 'แก้ไขข้อมูล' : 'เพิ่มข้อมูล' }} — กลุ่ม {{ title() }}</span>
4057
+ <button type="button" class="kc-auth-close" aria-label="ปิด" (click)="close()">&times;</button>
4058
+ </div>
4059
+ <div class="kc-auth-modal-body">
4060
+ @if (editError()) {
4061
+ <div class="kc-auth-alert error" role="alert">{{ editError() }}</div>
4062
+ }
4063
+ @if (hint()) {
4064
+ <p class="kc-auth-hint" style="margin: 0 0 12px">{{ hint() }}</p>
4065
+ }
4066
+ <div class="kc-auth-grid">
4067
+ <label class="kc-auth-field">
4068
+ <span>รหัส (k_code)</span>
4069
+ <input class="kc-auth-input" type="text" [(ngModel)]="e.code" maxlength="255" />
4070
+ </label>
4071
+ <label class="kc-auth-field">
4072
+ <span>ลำดับ (k_no)</span>
4073
+ <input class="kc-auth-input" type="text" inputmode="numeric" [(ngModel)]="e.no" maxlength="9" />
4074
+ </label>
4075
+ <label class="kc-auth-field full">
4076
+ <span>ชื่อ (k_name) <span class="req">*</span></span>
4077
+ <input class="kc-auth-input" type="text" [(ngModel)]="e.name" maxlength="255" />
4078
+ </label>
4079
+ <label class="kc-auth-field full">
4080
+ <span>ชื่อที่ 2 (k_name1)</span>
4081
+ <input class="kc-auth-input" type="text" [(ngModel)]="e.name1" maxlength="255" />
4082
+ </label>
4083
+ <label class="kc-auth-field">
4084
+ <span>ค่า (k_value)</span>
4085
+ <input class="kc-auth-input" type="text" [(ngModel)]="e.value" maxlength="255" />
4086
+ </label>
4087
+ <label class="kc-auth-field">
4088
+ <span>กลุ่มย่อย (k_grp)</span>
4089
+ <input class="kc-auth-input" type="text" [(ngModel)]="e.grp" maxlength="255" />
4090
+ </label>
4091
+ <label class="kc-auth-field">
4092
+ <span>k_mark</span>
4093
+ <input class="kc-auth-input" type="text" [(ngModel)]="e.mark" maxlength="255" />
4094
+ </label>
4095
+ <label class="kc-auth-field">
4096
+ <span>สถานะ</span>
4097
+ <select class="kc-auth-input" [(ngModel)]="e.active">
4098
+ <option [ngValue]="true">ใช้งาน</option>
4099
+ <option [ngValue]="false">ปิดใช้งาน</option>
4100
+ </select>
4101
+ </label>
4102
+ </div>
4103
+ <div class="kc-auth-actions">
4104
+ <button type="button" class="kc-auth-btn" (click)="close()">ยกเลิก</button>
4105
+ <button type="button" class="kc-auth-btn primary" [disabled]="saving()" (click)="save()">
4106
+ {{ saving() ? 'กำลังบันทึก...' : 'บันทึก' }}
4107
+ </button>
4108
+ </div>
4109
+ </div>
4110
+ </div>
4111
+ </div>
4112
+ }
4113
+ `
4114
+ }]
4115
+ }],
4116
+ ctorParameters: () => []
4117
+ });
4118
+ export { AnchoredPopup, ClockPickerDirective, ClockPickerPopupComponent, DatePickerDirective, DatePickerPopupComponent, KC_AUTH_CONFIG, KC_AUTH_PATHS, KC_RETURN_URL_KEY, KC_SSO_STATE_KEY, KC_SSO_TICKET_KEY, KcAuthService, KcConfigKclluComponent, KcConfigUsersComponent, KcLoginComponent, KcPasswordToggleComponent, KcProfileComponent, KcRegisterComponent, KcSsoCallbackComponent, PICKER_CSS, SearchSelectComponent, ThaiDatePickerComponent, ensurePickerStyles, fixThaiText, formatDate, kcAdminGuard, kcAuthGuard, kcAuthInterceptor, kcAuthRoutes, kcErrorMessage, kcTokenExpired, parseDate, provideKcAuth, provideThaiPasteFix, resolveKcAuthConfig, toBuddhistText, toChristianText };
1335
4119
 
1336
4120
  //# sourceMappingURL=kengcom202-kcore-ui.mjs.map