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