@tinya-code/gas-admin-ui 0.0.0-stage → 0.1.0-alpha.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +63 -2
- package/fesm2022/tinya-code-gas-admin-ui-api.service-CZrUeclM.mjs +281 -0
- package/fesm2022/tinya-code-gas-admin-ui-api.service-CZrUeclM.mjs.map +1 -0
- package/fesm2022/tinya-code-gas-admin-ui-badge-EeCA_l-o.mjs +84 -0
- package/fesm2022/tinya-code-gas-admin-ui-badge-EeCA_l-o.mjs.map +1 -0
- package/fesm2022/tinya-code-gas-admin-ui-bar-chart-G7sEMSBY.mjs +140 -0
- package/fesm2022/tinya-code-gas-admin-ui-bar-chart-G7sEMSBY.mjs.map +1 -0
- package/fesm2022/tinya-code-gas-admin-ui-boolean-B2ASKL0E.mjs +113 -0
- package/fesm2022/tinya-code-gas-admin-ui-boolean-B2ASKL0E.mjs.map +1 -0
- package/fesm2022/tinya-code-gas-admin-ui-button-BQjNW8KB.mjs +163 -0
- package/fesm2022/tinya-code-gas-admin-ui-button-BQjNW8KB.mjs.map +1 -0
- package/fesm2022/tinya-code-gas-admin-ui-calendar-view-r-RXDE37.mjs +1022 -0
- package/fesm2022/tinya-code-gas-admin-ui-calendar-view-r-RXDE37.mjs.map +1 -0
- package/fesm2022/tinya-code-gas-admin-ui-chart-line-BjdlKHoH.mjs +278 -0
- package/fesm2022/tinya-code-gas-admin-ui-chart-line-BjdlKHoH.mjs.map +1 -0
- package/fesm2022/tinya-code-gas-admin-ui-condition-evaluator-BBzWUuHl.mjs +101 -0
- package/fesm2022/tinya-code-gas-admin-ui-condition-evaluator-BBzWUuHl.mjs.map +1 -0
- package/fesm2022/tinya-code-gas-admin-ui-currency-BvuF3Z63.mjs +141 -0
- package/fesm2022/tinya-code-gas-admin-ui-currency-BvuF3Z63.mjs.map +1 -0
- package/fesm2022/tinya-code-gas-admin-ui-data-source.service-LD8AM3Rn.mjs +89 -0
- package/fesm2022/tinya-code-gas-admin-ui-data-source.service-LD8AM3Rn.mjs.map +1 -0
- package/fesm2022/tinya-code-gas-admin-ui-date-1SJGXZrd.mjs +107 -0
- package/fesm2022/tinya-code-gas-admin-ui-date-1SJGXZrd.mjs.map +1 -0
- package/fesm2022/tinya-code-gas-admin-ui-display-badge-fwk6sDU7.mjs +140 -0
- package/fesm2022/tinya-code-gas-admin-ui-display-badge-fwk6sDU7.mjs.map +1 -0
- package/fesm2022/tinya-code-gas-admin-ui-display-date-D7zLiDiG.mjs +119 -0
- package/fesm2022/tinya-code-gas-admin-ui-display-date-D7zLiDiG.mjs.map +1 -0
- package/fesm2022/tinya-code-gas-admin-ui-display-file-BiIhqvQC.mjs +125 -0
- package/fesm2022/tinya-code-gas-admin-ui-display-file-BiIhqvQC.mjs.map +1 -0
- package/fesm2022/tinya-code-gas-admin-ui-display-image-C1dhnZi1.mjs +152 -0
- package/fesm2022/tinya-code-gas-admin-ui-display-image-C1dhnZi1.mjs.map +1 -0
- package/fesm2022/tinya-code-gas-admin-ui-display-list-DOEuUbGe.mjs +211 -0
- package/fesm2022/tinya-code-gas-admin-ui-display-list-DOEuUbGe.mjs.map +1 -0
- package/fesm2022/tinya-code-gas-admin-ui-display-number-DDMzr4Hn.mjs +116 -0
- package/fesm2022/tinya-code-gas-admin-ui-display-number-DDMzr4Hn.mjs.map +1 -0
- package/fesm2022/tinya-code-gas-admin-ui-display-text-B8CdVNpv.mjs +111 -0
- package/fesm2022/tinya-code-gas-admin-ui-display-text-B8CdVNpv.mjs.map +1 -0
- package/fesm2022/tinya-code-gas-admin-ui-email-CT_O5ihA.mjs +107 -0
- package/fesm2022/tinya-code-gas-admin-ui-email-CT_O5ihA.mjs.map +1 -0
- package/fesm2022/tinya-code-gas-admin-ui-empty-state-BMc0QtoT.mjs +109 -0
- package/fesm2022/tinya-code-gas-admin-ui-empty-state-BMc0QtoT.mjs.map +1 -0
- package/fesm2022/tinya-code-gas-admin-ui-error-mapper.service-Ci0UpXtV.mjs +45 -0
- package/fesm2022/tinya-code-gas-admin-ui-error-mapper.service-Ci0UpXtV.mjs.map +1 -0
- package/fesm2022/tinya-code-gas-admin-ui-field-aria-BD1Wiggc.mjs +50 -0
- package/fesm2022/tinya-code-gas-admin-ui-field-aria-BD1Wiggc.mjs.map +1 -0
- package/fesm2022/tinya-code-gas-admin-ui-field-node-CIK6KNN_.mjs +24 -0
- package/fesm2022/tinya-code-gas-admin-ui-field-node-CIK6KNN_.mjs.map +1 -0
- package/fesm2022/tinya-code-gas-admin-ui-field-path-B4-FTBGZ.mjs +27 -0
- package/fesm2022/tinya-code-gas-admin-ui-field-path-B4-FTBGZ.mjs.map +1 -0
- package/fesm2022/tinya-code-gas-admin-ui-file-Dg2N3XQ2.mjs +408 -0
- package/fesm2022/tinya-code-gas-admin-ui-file-Dg2N3XQ2.mjs.map +1 -0
- package/fesm2022/tinya-code-gas-admin-ui-form-view-BwGupSbi.mjs +4514 -0
- package/fesm2022/tinya-code-gas-admin-ui-form-view-BwGupSbi.mjs.map +1 -0
- package/fesm2022/tinya-code-gas-admin-ui-format-badge-CK8ct5Wk.mjs +82 -0
- package/fesm2022/tinya-code-gas-admin-ui-format-badge-CK8ct5Wk.mjs.map +1 -0
- package/fesm2022/tinya-code-gas-admin-ui-format-boolean-ClzezZz-.mjs +105 -0
- package/fesm2022/tinya-code-gas-admin-ui-format-boolean-ClzezZz-.mjs.map +1 -0
- package/fesm2022/tinya-code-gas-admin-ui-format-currency-CVmniKml.mjs +65 -0
- package/fesm2022/tinya-code-gas-admin-ui-format-currency-CVmniKml.mjs.map +1 -0
- package/fesm2022/tinya-code-gas-admin-ui-format-date-Dr1YsDd_.mjs +65 -0
- package/fesm2022/tinya-code-gas-admin-ui-format-date-Dr1YsDd_.mjs.map +1 -0
- package/fesm2022/tinya-code-gas-admin-ui-format-number-CWCjpI4n.mjs +65 -0
- package/fesm2022/tinya-code-gas-admin-ui-format-number-CWCjpI4n.mjs.map +1 -0
- package/fesm2022/tinya-code-gas-admin-ui-format-text-DBOgJkH0.mjs +59 -0
- package/fesm2022/tinya-code-gas-admin-ui-format-text-DBOgJkH0.mjs.map +1 -0
- package/fesm2022/tinya-code-gas-admin-ui-format-thumbnail-PCRvxWKt.mjs +94 -0
- package/fesm2022/tinya-code-gas-admin-ui-format-thumbnail-PCRvxWKt.mjs.map +1 -0
- package/fesm2022/tinya-code-gas-admin-ui-gap-sorting-iTdy_DYG.mjs +72 -0
- package/fesm2022/tinya-code-gas-admin-ui-gap-sorting-iTdy_DYG.mjs.map +1 -0
- package/fesm2022/tinya-code-gas-admin-ui-group-h7-O8tt4.mjs +238 -0
- package/fesm2022/tinya-code-gas-admin-ui-group-h7-O8tt4.mjs.map +1 -0
- package/fesm2022/tinya-code-gas-admin-ui-image-Bjw9ImlS.mjs +416 -0
- package/fesm2022/tinya-code-gas-admin-ui-image-Bjw9ImlS.mjs.map +1 -0
- package/fesm2022/tinya-code-gas-admin-ui-kanban-view-CeGueI57.mjs +439 -0
- package/fesm2022/tinya-code-gas-admin-ui-kanban-view-CeGueI57.mjs.map +1 -0
- package/fesm2022/tinya-code-gas-admin-ui-key-value-nVdgg5at.mjs +513 -0
- package/fesm2022/tinya-code-gas-admin-ui-key-value-nVdgg5at.mjs.map +1 -0
- package/fesm2022/tinya-code-gas-admin-ui-list-CMzl9MkX.mjs +637 -0
- package/fesm2022/tinya-code-gas-admin-ui-list-CMzl9MkX.mjs.map +1 -0
- package/fesm2022/tinya-code-gas-admin-ui-metric-card-DnGMQL8l.mjs +127 -0
- package/fesm2022/tinya-code-gas-admin-ui-metric-card-DnGMQL8l.mjs.map +1 -0
- package/fesm2022/tinya-code-gas-admin-ui-multiselect-CuWwAfdY.mjs +251 -0
- package/fesm2022/tinya-code-gas-admin-ui-multiselect-CuWwAfdY.mjs.map +1 -0
- package/fesm2022/tinya-code-gas-admin-ui-notification.service-LjSULER6.mjs +66 -0
- package/fesm2022/tinya-code-gas-admin-ui-notification.service-LjSULER6.mjs.map +1 -0
- package/fesm2022/tinya-code-gas-admin-ui-number-JNr7glc8.mjs +114 -0
- package/fesm2022/tinya-code-gas-admin-ui-number-JNr7glc8.mjs.map +1 -0
- package/fesm2022/tinya-code-gas-admin-ui-phone-jR417hPf.mjs +116 -0
- package/fesm2022/tinya-code-gas-admin-ui-phone-jR417hPf.mjs.map +1 -0
- package/fesm2022/tinya-code-gas-admin-ui-quick-actions-DCQ6emmF.mjs +235 -0
- package/fesm2022/tinya-code-gas-admin-ui-quick-actions-DCQ6emmF.mjs.map +1 -0
- package/fesm2022/tinya-code-gas-admin-ui-radio-mode-DxvN07DY.mjs +8 -0
- package/fesm2022/tinya-code-gas-admin-ui-radio-mode-DxvN07DY.mjs.map +1 -0
- package/fesm2022/tinya-code-gas-admin-ui-readonly-text-BL7-4qie.mjs +124 -0
- package/fesm2022/tinya-code-gas-admin-ui-readonly-text-BL7-4qie.mjs.map +1 -0
- package/fesm2022/tinya-code-gas-admin-ui-record-list-BiZfnd_7.mjs +249 -0
- package/fesm2022/tinya-code-gas-admin-ui-record-list-BiZfnd_7.mjs.map +1 -0
- package/fesm2022/tinya-code-gas-admin-ui-relation-B3W3Ynsp.mjs +309 -0
- package/fesm2022/tinya-code-gas-admin-ui-relation-B3W3Ynsp.mjs.map +1 -0
- package/fesm2022/tinya-code-gas-admin-ui-relation-drawer.service-B0kT8EiW.mjs +39 -0
- package/fesm2022/tinya-code-gas-admin-ui-relation-drawer.service-B0kT8EiW.mjs.map +1 -0
- package/fesm2022/tinya-code-gas-admin-ui-render-registry-B_D5Jhp9.mjs +546 -0
- package/fesm2022/tinya-code-gas-admin-ui-render-registry-B_D5Jhp9.mjs.map +1 -0
- package/fesm2022/tinya-code-gas-admin-ui-rolldown-runtime-DpJrqANO.mjs +11 -0
- package/fesm2022/tinya-code-gas-admin-ui-schema-catalog-CDrz9pVN.mjs +131 -0
- package/fesm2022/tinya-code-gas-admin-ui-schema-catalog-CDrz9pVN.mjs.map +1 -0
- package/fesm2022/tinya-code-gas-admin-ui-seed-CNbQtREA.mjs +80 -0
- package/fesm2022/tinya-code-gas-admin-ui-seed-CNbQtREA.mjs.map +1 -0
- package/fesm2022/tinya-code-gas-admin-ui-select-DzaRfHXO.mjs +292 -0
- package/fesm2022/tinya-code-gas-admin-ui-select-DzaRfHXO.mjs.map +1 -0
- package/fesm2022/tinya-code-gas-admin-ui-skeleton-DnWK0tcZ.mjs +119 -0
- package/fesm2022/tinya-code-gas-admin-ui-skeleton-DnWK0tcZ.mjs.map +1 -0
- package/fesm2022/tinya-code-gas-admin-ui-slug-DU58cbiY.mjs +158 -0
- package/fesm2022/tinya-code-gas-admin-ui-slug-DU58cbiY.mjs.map +1 -0
- package/fesm2022/tinya-code-gas-admin-ui-source-options-B9R4mw8-.mjs +26 -0
- package/fesm2022/tinya-code-gas-admin-ui-source-options-B9R4mw8-.mjs.map +1 -0
- package/fesm2022/tinya-code-gas-admin-ui-status-progress-DmeG8htE.mjs +205 -0
- package/fesm2022/tinya-code-gas-admin-ui-status-progress-DmeG8htE.mjs.map +1 -0
- package/fesm2022/tinya-code-gas-admin-ui-string-list-gleasqzD.mjs +432 -0
- package/fesm2022/tinya-code-gas-admin-ui-string-list-gleasqzD.mjs.map +1 -0
- package/fesm2022/tinya-code-gas-admin-ui-template-interpolator-ewv7CkNX.mjs +36 -0
- package/fesm2022/tinya-code-gas-admin-ui-template-interpolator-ewv7CkNX.mjs.map +1 -0
- package/fesm2022/tinya-code-gas-admin-ui-text-CUPmL7W_.mjs +107 -0
- package/fesm2022/tinya-code-gas-admin-ui-text-CUPmL7W_.mjs.map +1 -0
- package/fesm2022/tinya-code-gas-admin-ui-textarea-CGLSIW13.mjs +223 -0
- package/fesm2022/tinya-code-gas-admin-ui-textarea-CGLSIW13.mjs.map +1 -0
- package/fesm2022/tinya-code-gas-admin-ui-time-BcHzsTWf.mjs +107 -0
- package/fesm2022/tinya-code-gas-admin-ui-time-BcHzsTWf.mjs.map +1 -0
- package/fesm2022/tinya-code-gas-admin-ui-upload.service-C1g545w5.mjs +60 -0
- package/fesm2022/tinya-code-gas-admin-ui-upload.service-C1g545w5.mjs.map +1 -0
- package/fesm2022/tinya-code-gas-admin-ui-url-GO4LZi__.mjs +141 -0
- package/fesm2022/tinya-code-gas-admin-ui-url-GO4LZi__.mjs.map +1 -0
- package/fesm2022/tinya-code-gas-admin-ui.mjs +4400 -0
- package/fesm2022/tinya-code-gas-admin-ui.mjs.map +1 -0
- package/package.json +31 -3
- package/types/tinya-code-gas-admin-ui.d.ts +3091 -0
package/README.md
CHANGED
|
@@ -1,3 +1,64 @@
|
|
|
1
|
-
#
|
|
1
|
+
# GasAdminUi
|
|
2
2
|
|
|
3
|
-
This
|
|
3
|
+
This project was generated using [Angular CLI](https://github.com/angular/angular-cli) version 22.2.0.
|
|
4
|
+
|
|
5
|
+
## Code scaffolding
|
|
6
|
+
|
|
7
|
+
Angular CLI includes powerful code scaffolding tools. To generate a new component, run:
|
|
8
|
+
|
|
9
|
+
```bash
|
|
10
|
+
ng generate component component-name
|
|
11
|
+
```
|
|
12
|
+
|
|
13
|
+
For a complete list of available schematics (such as `components`, `directives`, or `pipes`), run:
|
|
14
|
+
|
|
15
|
+
```bash
|
|
16
|
+
ng generate --help
|
|
17
|
+
```
|
|
18
|
+
|
|
19
|
+
## Building
|
|
20
|
+
|
|
21
|
+
To build the library, run:
|
|
22
|
+
|
|
23
|
+
```bash
|
|
24
|
+
ng build gas-admin-ui
|
|
25
|
+
```
|
|
26
|
+
|
|
27
|
+
This command will compile your project, and the build artifacts will be placed in the `dist/` directory.
|
|
28
|
+
|
|
29
|
+
### Publishing the Library
|
|
30
|
+
|
|
31
|
+
Once the project is built, you can publish your library by following these steps:
|
|
32
|
+
|
|
33
|
+
1. Navigate to the `dist` directory:
|
|
34
|
+
|
|
35
|
+
```bash
|
|
36
|
+
cd dist/gas-admin-ui
|
|
37
|
+
```
|
|
38
|
+
|
|
39
|
+
2. Run the `npm publish` command to publish your library to the npm registry:
|
|
40
|
+
```bash
|
|
41
|
+
npm publish
|
|
42
|
+
```
|
|
43
|
+
|
|
44
|
+
## Running unit tests
|
|
45
|
+
|
|
46
|
+
To execute unit tests with the [Vitest](https://vitest.dev/) test runner, use the following command:
|
|
47
|
+
|
|
48
|
+
```bash
|
|
49
|
+
ng test
|
|
50
|
+
```
|
|
51
|
+
|
|
52
|
+
## Running end-to-end tests
|
|
53
|
+
|
|
54
|
+
For end-to-end (e2e) testing, run:
|
|
55
|
+
|
|
56
|
+
```bash
|
|
57
|
+
ng e2e
|
|
58
|
+
```
|
|
59
|
+
|
|
60
|
+
Angular CLI does not come with an end-to-end testing framework by default. You can choose one that suits your needs.
|
|
61
|
+
|
|
62
|
+
## Additional Resources
|
|
63
|
+
|
|
64
|
+
For more information on using the Angular CLI, including detailed command references, visit the [Angular CLI Overview and Command Reference](https://angular.dev/tools/cli) page.
|
|
@@ -0,0 +1,281 @@
|
|
|
1
|
+
import * as i0 from "@angular/core";
|
|
2
|
+
import { InjectionToken, Service, computed, inject, signal } from "@angular/core";
|
|
3
|
+
import { HttpClient } from "@angular/common/http";
|
|
4
|
+
import { catchError, map, of, tap, throwError } from "rxjs";
|
|
5
|
+
import { Router } from "@angular/router";
|
|
6
|
+
var ApiError = class extends Error {
|
|
7
|
+
status;
|
|
8
|
+
fieldPath;
|
|
9
|
+
code;
|
|
10
|
+
issues;
|
|
11
|
+
constructor(status, message, fieldPath, code, issues = []) {
|
|
12
|
+
super(message);
|
|
13
|
+
this.name = "ApiError";
|
|
14
|
+
this.status = status;
|
|
15
|
+
this.fieldPath = fieldPath;
|
|
16
|
+
this.code = code;
|
|
17
|
+
this.issues = issues;
|
|
18
|
+
}
|
|
19
|
+
};
|
|
20
|
+
function renewedTokenOf(body) {
|
|
21
|
+
if (body === null || typeof body !== "object") return null;
|
|
22
|
+
const meta = body.meta;
|
|
23
|
+
if (meta === null || typeof meta !== "object") return null;
|
|
24
|
+
const token = meta.renewedToken;
|
|
25
|
+
return typeof token === "string" && token !== "" ? token : null;
|
|
26
|
+
}
|
|
27
|
+
const API_URL = new InjectionToken("API_URL");
|
|
28
|
+
const LOGIN_PATH = "/login";
|
|
29
|
+
const SESSION_TOKEN_KEY = "admin.session.token";
|
|
30
|
+
const SESSION_USER_KEY = "admin.session.user";
|
|
31
|
+
var AuthService = class AuthService {
|
|
32
|
+
http = inject(HttpClient);
|
|
33
|
+
apiUrl = inject(API_URL);
|
|
34
|
+
router = inject(Router);
|
|
35
|
+
storage = sessionStorage;
|
|
36
|
+
_user = signal(null, ...ngDevMode ? [{ debugName: "_user" }] : /* istanbul ignore next */ []);
|
|
37
|
+
_token = signal(null, ...ngDevMode ? [{ debugName: "_token" }] : /* istanbul ignore next */ []);
|
|
38
|
+
_returnUrl = signal(null, ...ngDevMode ? [{ debugName: "_returnUrl" }] : /* istanbul ignore next */ []);
|
|
39
|
+
user = this._user.asReadonly();
|
|
40
|
+
token = this._token.asReadonly();
|
|
41
|
+
returnUrl = this._returnUrl.asReadonly();
|
|
42
|
+
isAuthenticated = computed(() => this._token() !== null && this._user() !== null, ...ngDevMode ? [{ debugName: "isAuthenticated" }] : /* istanbul ignore next */ []);
|
|
43
|
+
role = computed(() => this._user()?.role ?? "", ...ngDevMode ? [{ debugName: "role" }] : /* istanbul ignore next */ []);
|
|
44
|
+
constructor() {
|
|
45
|
+
const token = this.storage.getItem(SESSION_TOKEN_KEY);
|
|
46
|
+
if (token !== null && token !== "") this._token.set(token);
|
|
47
|
+
const raw = this.storage.getItem(SESSION_USER_KEY);
|
|
48
|
+
if (raw !== null && raw !== "") try {
|
|
49
|
+
this._user.set(JSON.parse(raw));
|
|
50
|
+
} catch {
|
|
51
|
+
this.storage.removeItem(SESSION_USER_KEY);
|
|
52
|
+
this.storage.removeItem(SESSION_TOKEN_KEY);
|
|
53
|
+
this._token.set(null);
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
login(email, password) {
|
|
57
|
+
return this.post("/auth/login", {
|
|
58
|
+
email,
|
|
59
|
+
password
|
|
60
|
+
}).pipe(map((res) => {
|
|
61
|
+
this.open(res.token, res.user);
|
|
62
|
+
const renewed = renewedTokenOf(res);
|
|
63
|
+
if (renewed !== null) this.applyRenewedToken(renewed);
|
|
64
|
+
return res.user;
|
|
65
|
+
}));
|
|
66
|
+
}
|
|
67
|
+
logout() {
|
|
68
|
+
return this.post("/auth/logout").pipe(tap({
|
|
69
|
+
next: () => this.clear(),
|
|
70
|
+
error: () => this.clear()
|
|
71
|
+
}), map(() => void 0));
|
|
72
|
+
}
|
|
73
|
+
logoutAll() {
|
|
74
|
+
return this.post("/auth/logout-all").pipe(tap(() => {
|
|
75
|
+
this.clear();
|
|
76
|
+
this.goLogin();
|
|
77
|
+
}), map((res) => ({ effectiveWithinSeconds: res.effectiveWithinSeconds })));
|
|
78
|
+
}
|
|
79
|
+
restore() {
|
|
80
|
+
if (this._token() === null) return of(void 0);
|
|
81
|
+
return this.post("/auth/me").pipe(tap((res) => {
|
|
82
|
+
this.open(this._token(), res.user);
|
|
83
|
+
}), catchError(() => of(void 0)), map(() => void 0));
|
|
84
|
+
}
|
|
85
|
+
applyRenewedToken(token) {
|
|
86
|
+
if (token === "" || this._token() === null) return;
|
|
87
|
+
this._token.set(token);
|
|
88
|
+
this.storage.setItem(SESSION_TOKEN_KEY, token);
|
|
89
|
+
}
|
|
90
|
+
applyAuthCode(code) {
|
|
91
|
+
switch (code) {
|
|
92
|
+
case "AUTH_EXPIRED":
|
|
93
|
+
this._returnUrl.set(this.currentUrl());
|
|
94
|
+
this.clear();
|
|
95
|
+
this.goLogin();
|
|
96
|
+
break;
|
|
97
|
+
case "AUTH_REQUIRED":
|
|
98
|
+
case "AUTH_INVALID":
|
|
99
|
+
this.clear();
|
|
100
|
+
this.goLogin();
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
hasRole(required) {
|
|
104
|
+
if (required === "" || required === "session") return this.isAuthenticated();
|
|
105
|
+
const role = this.role();
|
|
106
|
+
if (role === "admin") return true;
|
|
107
|
+
return role !== "" && role === required;
|
|
108
|
+
}
|
|
109
|
+
canAccess(access) {
|
|
110
|
+
const requires = access?.requires ?? "session";
|
|
111
|
+
if (requires === "public") return true;
|
|
112
|
+
if (!this.isAuthenticated()) return false;
|
|
113
|
+
const role = requires.startsWith("role:") ? requires.slice(5) : "";
|
|
114
|
+
const required = role === "" ? access?.roles ?? [] : [role];
|
|
115
|
+
if (required.length === 0) return true;
|
|
116
|
+
return required.some((one) => this.hasRole(one));
|
|
117
|
+
}
|
|
118
|
+
canDoOperation(schema, operation) {
|
|
119
|
+
const ops = schema.policies?.access?.operations;
|
|
120
|
+
if (ops && ops[operation]) return this.meetsLevel(ops[operation]);
|
|
121
|
+
if (schema.policies?.access) {
|
|
122
|
+
const groupLevel = operation === "create" || operation === "update" || operation === "remove" ? schema.policies.access.write : schema.policies.access.read;
|
|
123
|
+
return this.meetsLevel(groupLevel ?? "admin");
|
|
124
|
+
}
|
|
125
|
+
const perms = schema.permissions;
|
|
126
|
+
if (perms) return {
|
|
127
|
+
list: true,
|
|
128
|
+
get: true,
|
|
129
|
+
create: perms.create,
|
|
130
|
+
update: perms.update,
|
|
131
|
+
remove: perms.remove
|
|
132
|
+
}[operation] !== false;
|
|
133
|
+
return true;
|
|
134
|
+
}
|
|
135
|
+
canReadField(field) {
|
|
136
|
+
if (!field.access || !field.access.read) return true;
|
|
137
|
+
return this.meetsLevel(field.access.read);
|
|
138
|
+
}
|
|
139
|
+
canWriteField(field) {
|
|
140
|
+
if (!field.access || !field.access.write) return true;
|
|
141
|
+
return this.meetsLevel(field.access.write);
|
|
142
|
+
}
|
|
143
|
+
meetsLevel(level) {
|
|
144
|
+
if (level === "public") return true;
|
|
145
|
+
if (level === "session") return this.isAuthenticated();
|
|
146
|
+
if (level.startsWith("role:")) {
|
|
147
|
+
const requiredRole = level.slice(5);
|
|
148
|
+
return this.hasRole(requiredRole);
|
|
149
|
+
}
|
|
150
|
+
return this.hasRole("admin");
|
|
151
|
+
}
|
|
152
|
+
open(token, user) {
|
|
153
|
+
if (token !== null && token !== "") this._token.set(token);
|
|
154
|
+
this._user.set(user);
|
|
155
|
+
if (token !== null && token !== "") this.storage.setItem(SESSION_TOKEN_KEY, token);
|
|
156
|
+
if (user !== null) this.storage.setItem(SESSION_USER_KEY, JSON.stringify(user));
|
|
157
|
+
}
|
|
158
|
+
consumeReturnUrl() {
|
|
159
|
+
const url = this._returnUrl();
|
|
160
|
+
this._returnUrl.set(null);
|
|
161
|
+
return url;
|
|
162
|
+
}
|
|
163
|
+
clear() {
|
|
164
|
+
this._token.set(null);
|
|
165
|
+
this._user.set(null);
|
|
166
|
+
this.storage.removeItem(SESSION_TOKEN_KEY);
|
|
167
|
+
this.storage.removeItem(SESSION_USER_KEY);
|
|
168
|
+
}
|
|
169
|
+
goLogin() {
|
|
170
|
+
if (this.currentUrl().startsWith("/login")) return;
|
|
171
|
+
this.router.navigate([LOGIN_PATH]).catch(() => void 0);
|
|
172
|
+
}
|
|
173
|
+
currentUrl() {
|
|
174
|
+
return this.router.url;
|
|
175
|
+
}
|
|
176
|
+
envelope(path, payload = {}) {
|
|
177
|
+
return {
|
|
178
|
+
token: this._token(),
|
|
179
|
+
method: "POST",
|
|
180
|
+
path,
|
|
181
|
+
payload
|
|
182
|
+
};
|
|
183
|
+
}
|
|
184
|
+
post(path, payload = {}) {
|
|
185
|
+
return this.http.post(this.apiUrl, this.envelope(path, payload), { headers: { "Content-Type": "text/plain" } });
|
|
186
|
+
}
|
|
187
|
+
static ɵfac = i0.ɵɵngDeclareFactory({
|
|
188
|
+
minVersion: "12.0.0",
|
|
189
|
+
version: "22.2.3",
|
|
190
|
+
ngImport: i0,
|
|
191
|
+
type: AuthService,
|
|
192
|
+
deps: [],
|
|
193
|
+
target: i0.ɵɵFactoryTarget.Service
|
|
194
|
+
});
|
|
195
|
+
static ɵprov = i0.ɵɵngDeclareService({
|
|
196
|
+
minVersion: "22.0.0",
|
|
197
|
+
version: "22.2.3",
|
|
198
|
+
ngImport: i0,
|
|
199
|
+
type: AuthService
|
|
200
|
+
});
|
|
201
|
+
};
|
|
202
|
+
i0.ɵɵngDeclareClassMetadata({
|
|
203
|
+
minVersion: "12.0.0",
|
|
204
|
+
version: "22.2.3",
|
|
205
|
+
ngImport: i0,
|
|
206
|
+
type: AuthService,
|
|
207
|
+
decorators: [{ type: Service }],
|
|
208
|
+
ctorParameters: () => []
|
|
209
|
+
});
|
|
210
|
+
var ApiService = class ApiService {
|
|
211
|
+
http = inject(HttpClient);
|
|
212
|
+
apiUrl = inject(API_URL);
|
|
213
|
+
auth = inject(AuthService);
|
|
214
|
+
list(endpoint, payload) {
|
|
215
|
+
return this.send(endpoint.list, "GET", payload).pipe(map((raw) => unwrapList(raw)));
|
|
216
|
+
}
|
|
217
|
+
get(endpoint, key) {
|
|
218
|
+
return this.send(this.withKey(endpoint.get, key), "GET");
|
|
219
|
+
}
|
|
220
|
+
create(endpoint, payload) {
|
|
221
|
+
return this.send(endpoint.create, "POST", payload);
|
|
222
|
+
}
|
|
223
|
+
update(endpoint, key, payload) {
|
|
224
|
+
return this.send(this.withKey(endpoint.update, key), "PUT", payload);
|
|
225
|
+
}
|
|
226
|
+
remove(endpoint, key) {
|
|
227
|
+
return this.send(this.withKey(endpoint.remove, key), "DELETE");
|
|
228
|
+
}
|
|
229
|
+
request(method, path, payload) {
|
|
230
|
+
return this.send(path, method, payload);
|
|
231
|
+
}
|
|
232
|
+
send(path, method, payload) {
|
|
233
|
+
if (!path) return throwError(() => /* @__PURE__ */ new Error("Operación no disponible para este recurso"));
|
|
234
|
+
const body = {
|
|
235
|
+
token: this.auth.token(),
|
|
236
|
+
method,
|
|
237
|
+
path,
|
|
238
|
+
payload: payload ?? {}
|
|
239
|
+
};
|
|
240
|
+
return this.http.post(this.apiUrl, body, { headers: { "Content-Type": "text/plain" } }).pipe(tap((res) => {
|
|
241
|
+
const renewed = renewedTokenOf(res);
|
|
242
|
+
if (renewed !== null) this.auth.applyRenewedToken(renewed);
|
|
243
|
+
}));
|
|
244
|
+
}
|
|
245
|
+
withKey(path, key) {
|
|
246
|
+
if (!path) return;
|
|
247
|
+
return path.includes("{key}") ? path.replace("{key}", encodeURIComponent(key ?? "")) : path;
|
|
248
|
+
}
|
|
249
|
+
static ɵfac = i0.ɵɵngDeclareFactory({
|
|
250
|
+
minVersion: "12.0.0",
|
|
251
|
+
version: "22.2.3",
|
|
252
|
+
ngImport: i0,
|
|
253
|
+
type: ApiService,
|
|
254
|
+
deps: [],
|
|
255
|
+
target: i0.ɵɵFactoryTarget.Service
|
|
256
|
+
});
|
|
257
|
+
static ɵprov = i0.ɵɵngDeclareService({
|
|
258
|
+
minVersion: "22.0.0",
|
|
259
|
+
version: "22.2.3",
|
|
260
|
+
ngImport: i0,
|
|
261
|
+
type: ApiService
|
|
262
|
+
});
|
|
263
|
+
};
|
|
264
|
+
i0.ɵɵngDeclareClassMetadata({
|
|
265
|
+
minVersion: "12.0.0",
|
|
266
|
+
version: "22.2.3",
|
|
267
|
+
ngImport: i0,
|
|
268
|
+
type: ApiService,
|
|
269
|
+
decorators: [{ type: Service }]
|
|
270
|
+
});
|
|
271
|
+
function unwrapList(raw) {
|
|
272
|
+
if (Array.isArray(raw)) return raw;
|
|
273
|
+
if (raw !== null && typeof raw === "object") {
|
|
274
|
+
const items = raw.items;
|
|
275
|
+
if (Array.isArray(items)) return items;
|
|
276
|
+
}
|
|
277
|
+
return [];
|
|
278
|
+
}
|
|
279
|
+
export { SESSION_USER_KEY as a, renewedTokenOf as c, SESSION_TOKEN_KEY as i, AuthService as n, API_URL as o, LOGIN_PATH as r, ApiError as s, ApiService as t };
|
|
280
|
+
|
|
281
|
+
//# sourceMappingURL=tinya-code-gas-admin-ui-api.service-CZrUeclM.mjs.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"tinya-code-gas-admin-ui-api.service-CZrUeclM.mjs","names":[],"sources":["../../../projects/gas-admin-ui/src/lib/core/models/api.model.ts","../../../projects/gas-admin-ui/src/lib/core/tokens.ts","../../../projects/gas-admin-ui/src/lib/core/services/auth.service.ts","../../../projects/gas-admin-ui/src/lib/core/services/api.service.ts"],"sourcesContent":["/**\n * Contrato HTTP con el backend (doc/baseapi.md §8 «Errores»).\n *\n * El backend responde SIEMPRE HTTP 200; los errores viajan en el body\n * con `{ error: { status, message, errors?: [{ path, message }] } }`.\n * El interceptor (Fase 3) los convierte en `ApiError`.\n */\n\n/**\n * Un error con ruta del422 (auth.md §6): `path` para pintarlo en el campo y\n * `code` MAYUS_SNAKE (`VALIDATION_MIN_LENGTH`) para mapearlo sin leer texto.\n */\nexport interface ApiIssue {\n /** Ruta del campo afectado (`faq[2].answer`, `sku`). */\n path: string;\n /** Mensaje en español, listo para pintar. */\n message: string;\n /** Código estable del validador (B11); el mensaje puede cambiar, el código no. */\n code?: string;\n}\n\n/** Shape del body de error (doc/baseapi.md §8 «Errores»). */\nexport interface ApiErrorBody {\n error: {\n status: number;\n message: string;\n /** Ruta del campo afectado cuando el backend la envía (base.md §10). */\n path?: string;\n /**\n * Código de sesión de §4.5 (R2.9): `AUTH_REQUIRED` · `AUTH_INVALID` ·\n * `AUTH_EXPIRED` · `FORBIDDEN` · `RATE_LIMITED`. Aparece SÓLO en esas\n * cinco respuestas — el sobre de error es aditivo (O7 / D10-5).\n */\n code?: string;\n /**\n * Errores de validación con ruta (base.md §5, guía §10): el 422 los manda\n * ACÁ (`error.errors[]`), no en `path`. Sin modelarlos el form veía\n * «Errores de validación» genérico y ningún campo se pintaba (D42).\n */\n errors?: ApiIssue[];\n };\n}\n\n/** Error de API estandarizado que lanza el interceptor de errores. */\nexport class ApiError extends Error {\n readonly status: number;\n /** Ruta del campo que provocó el error (ej. `faq[2].answer`), si existe. */\n readonly fieldPath?: string;\n /** Código de sesión de §4.5 si el backend lo envió; `undefined` en el resto. */\n readonly code?: string;\n /** Issues con ruta (422); vacío cuando el error no trae `errors[]`. */\n readonly issues: readonly ApiIssue[];\n\n constructor(\n status: number,\n message: string,\n fieldPath?: string,\n code?: string,\n issues: readonly ApiIssue[] = [],\n ) {\n super(message);\n this.name = 'ApiError';\n this.status = status;\n this.fieldPath = fieldPath;\n this.code = code;\n this.issues = issues;\n }\n}\n\n/**\n * `meta.renewedToken` (auth.md §3.2/§3.4): el back lo adjunta a la respuesta\n * cuando la vida del token bajó del umbral de renovación. Es el ÚNICO modo\n * de que el front se entere — el token nuevo no viaja en cabeceras ni en la\n * URL. Devuelve `null` cuando no hay renovación (que es lo normal).\n *\n * Vive acá y no en el `ApiService` porque lo consumen DOS servicios\n * (`ApiService` y `AuthService`) y éste modelo no importa a ninguno de los\n * dos: sin este archivo no hay ciclo de imports.\n */\nexport function renewedTokenOf(body: unknown): string | null {\n if (body === null || typeof body !== 'object') return null;\n const meta = (body as { meta?: unknown }).meta;\n if (meta === null || typeof meta !== 'object') return null;\n const token = (meta as { renewedToken?: unknown }).renewedToken;\n return typeof token === 'string' && token !== '' ? token : null;\n}\n","import { InjectionToken } from '@angular/core';\n\n/**\n * URL base de la API (baseapi §2.3): es la URL directa del Web App de Apps\n * Script; ahí se hace el `POST` del envelope `{ token, method, path, payload }`.\n * OBLIGATORIO proporcionarlo en `app.config.ts`:\n * `{ provide: API_URL, useValue: 'https://…' }` (Fase 3). Vacío = mismo origen.\n */\nexport const API_URL = new InjectionToken<string>('API_URL');\n","import { HttpClient } from '@angular/common/http';\nimport { computed, inject, Service, signal } from '@angular/core';\nimport { Router } from '@angular/router';\nimport { catchError, map, Observable, of, tap } from 'rxjs';\n\nimport type { FieldSchema, ResourceSchema } from '../models/schema.model';\nimport { renewedTokenOf } from '../models/api.model';\nimport { API_URL } from '../tokens';\n\n/**\n * Sesión del admin (auth.md §5.2) — **única dueña** del token y del usuario.\n *\n * Qué vive acá y por qué:\n *\n * - **Señales** `user` / `token` / `isAuthenticated` / `role`: el único\n * estado que el resto de la app lee (guards, menú, interceptor).\n * - **`login()` / `logout()` / `logoutAll()` / `restore()`**: operaciones de sesión.\n * El `AuthService` NO inyecta `ApiService` — sólo `HttpClient` — porque\n * `ApiService` inyecta a éste para leer el token, y un ciclo de DI en\n * Angular es un error en tiempo de arranque, no un warning.\n * - **`applyAuthCode()` (R2.9): EL único intérprete de la tabla de §4.5.**\n * Ningún otro archivo decide qué pasa ante `AUTH_*` o `FORBIDDEN`.\n * - **Almacenamiento (R2.12): `sessionStorage`, siempre.** `rememberMe`\n * no puede usar `localStorage` porque D10-3 lo negó — ver más abajo.\n *\n * La sesión arranca desde el storage y `restore()` la revalida contra\n * `/auth/me` en el initializer: un usuario desactivado mientras la pestaña\n * estaba abierta cae al primer reload, no en mitad de una edición.\n */\n\n/** Usuario tal y como lo sirve `publicUser_` (lista blanca, R2.17). */\nexport interface SessionUser {\n id?: string;\n email?: string;\n name?: string;\n role?: string;\n active?: boolean;\n last_login_at?: string;\n created_at?: string;\n}\n\n/** Los cinco códigos de §4.5. El resto de errores no son de sesión. */\nexport type AuthErrorCode =\n 'AUTH_REQUIRED' | 'AUTH_INVALID' | 'AUTH_EXPIRED' | 'FORBIDDEN' | 'RATE_LIMITED';\n\n/** Ruta de login: fuera del shell para que ningún guard la proteja. */\nexport const LOGIN_PATH = '/login';\n\n/**\n * Claves del `sessionStorage`. Exportadas porque **la sesión se restaura\n * desde ahí en el constructor**: un test que quiera una sesión arrancando\n * tiene que sembrarlas antes de inyectar el servicio, y el que las use con\n * otro nombre quebraría el restore en silencio.\n */\nexport const SESSION_TOKEN_KEY = 'admin.session.token';\nexport const SESSION_USER_KEY = 'admin.session.user';\n\ninterface LoginResponse {\n token: string;\n user: SessionUser;\n}\ninterface MeResponse {\n user: SessionUser;\n}\n\n@Service()\nexport class AuthService {\n private readonly http = inject(HttpClient);\n private readonly apiUrl = inject(API_URL);\n private readonly router = inject(Router);\n\n /**\n * D10-3: **sólo `sessionStorage`.** La opción «recordarme» con\n * `localStorage` quedó denegada en §9.3 — muere con la pestaña ⇒ cero\n * token legible por un XSS futuro. `authSchema.rememberMe` es `false`\n * por la misma razón: declarar `true` sería prometer un storage que\n * este servicio no usa.\n *\n * Nunca en la URL ni en el cuerpo de un GET (§5.4): el token sólo viaja\n * dentro del envelope `{ token, method, path, payload }`.\n */\n private readonly storage: Storage = sessionStorage;\n\n private readonly _user = signal<SessionUser | null>(null);\n private readonly _token = signal<string | null>(null);\n /** Ruta previa a un `AUTH_EXPIRED`, para volver tras reloguear (§4.5). */\n private readonly _returnUrl = signal<string | null>(null);\n\n readonly user = this._user.asReadonly();\n readonly token = this._token.asReadonly();\n readonly returnUrl = this._returnUrl.asReadonly();\n /** Sólo hay sesión con token Y usuario: uno sin el otro es estado corrupto. */\n readonly isAuthenticated = computed(() => this._token() !== null && this._user() !== null);\n readonly role = computed(() => this._user()?.role ?? '');\n\n constructor() {\n const token = this.storage.getItem(SESSION_TOKEN_KEY);\n if (token !== null && token !== '') this._token.set(token);\n const raw = this.storage.getItem(SESSION_USER_KEY);\n if (raw !== null && raw !== '') {\n try {\n this._user.set(JSON.parse(raw) as SessionUser);\n } catch {\n // JSON corrupto ⇒ no hay sesión que restaurar; storage sucio.\n this.storage.removeItem(SESSION_USER_KEY);\n this.storage.removeItem(SESSION_TOKEN_KEY);\n this._token.set(null);\n }\n }\n }\n\n // ── Sesión ───────────────────────────────────────────────────────────────\n\n login(email: string, password: string): Observable<SessionUser> {\n return this.post<LoginResponse>('/auth/login', { email, password }).pipe(\n map((res) => {\n this.open(res.token, res.user);\n const renewed = renewedTokenOf(res);\n if (renewed !== null) this.applyRenewedToken(renewed);\n return res.user;\n }),\n );\n }\n\n /**\n * Revocación best-effort (§9.7): se pide al back que ponga el `sid` en la\n * lista de revocados, pero **la sesión local se ciere haya o no respuesta**.\n * Un logout que «falla» por red y deja al usuario dentro es peor que uno\n * que deja una sesión muerta en el servidor.\n */\n logout(): Observable<void> {\n return this.post<{ loggedOut?: boolean }>('/auth/logout').pipe(\n tap({\n next: () => this.clear(),\n error: () => this.clear(),\n }),\n map(() => undefined),\n );\n }\n\n /** Revoca las sesiones del usuario en todos sus dispositivos. */\n logoutAll(): Observable<{ effectiveWithinSeconds: number }> {\n return this.post<{ sessionsRevoked: boolean; effectiveWithinSeconds: number }>(\n '/auth/logout-all',\n ).pipe(\n tap(() => {\n this.clear();\n this.goLogin();\n }),\n map((res) => ({ effectiveWithinSeconds: res.effectiveWithinSeconds })),\n );\n }\n\n /** Revalida la sesión restaurada contra `/auth/me`; sin token ⇒ sin viaje. */\n restore(): Observable<void> {\n if (this._token() === null) return of(undefined);\n return this.post<MeResponse>('/auth/me').pipe(\n tap((res) => {\n this.open(this._token(), res.user);\n }),\n // El interceptor ya interpretó el `AUTH_*` (R2.9) antes de llegar\n // acá: esto sólo evita que un fallo rompa el arranque de la app.\n catchError(() => of(undefined)),\n map(() => undefined),\n );\n }\n\n /**\n * Renovación deslizante (§3.2): el back manda `meta.renewedToken` cuando\n * la vida del token bajó del umbral. Se persiste el MISMO día que el\n * token original — el `sid` no cambia, sólo se emite una firma nueva.\n */\n applyRenewedToken(token: string): void {\n if (token === '' || this._token() === null) return;\n this._token.set(token);\n this.storage.setItem(SESSION_TOKEN_KEY, token);\n }\n\n // ── R2.9 · la tabla de §4.5, en un solo sitio ───────────────────────────\n\n /**\n * ÚNICO intérprete de los códigos de sesión (auth.md §4.5 / §5.2).\n * El interceptor `apiErrorInterceptor` lo llama y nada más: cualquier\n * archivo que decida «si ve AUTH_EXPIRED hago X» está fuera de contrato.\n *\n * | Código | Qué hace |\n * | --------------- | ------------------------------------------- |\n * | `AUTH_REQUIRED` | limpia sesión y va a login |\n * | `AUTH_INVALID` | limpia sesión y va a login |\n * | `AUTH_EXPIRED` | guarda la ruta previa, limpia, va a login |\n * | `FORBIDDEN` | **no** cierra sesión (el usuario sigue dentro) |\n * | `RATE_LIMITED` | no hace nada: lo espera el formulario |\n */\n applyAuthCode(code: string | undefined): void {\n switch (code) {\n case 'AUTH_EXPIRED':\n // La ruta previa se guarda ANTES de borrar: después de limpiar no\n // queda desde dónde navegar (y `router.url` ya cambió al llegar).\n this._returnUrl.set(this.currentUrl());\n this.clear();\n this.goLogin();\n break;\n case 'AUTH_REQUIRED':\n case 'AUTH_INVALID':\n // «La sesión terminó» — no hay vuelta atrás que conservar.\n this.clear();\n this.goLogin();\n break;\n case 'FORBIDDEN':\n case 'RATE_LIMITED':\n // Decisión explícita de NO tocar la sesión: §4.5 exige que el\n // usuario vea «sin permiso» o «espera» **siguiendo dentro**.\n break;\n default:\n // No es un código de sesión: nada que interpretar.\n break;\n }\n }\n\n // ── Helpers ──────────────────────────────────────────────────────────────\n\n /** ¿El rol de la sesión cubre `required`? (mínimo: admin ≥ staff, §4.4) */\n hasRole(required: string): boolean {\n if (required === '' || required === 'session') return this.isAuthenticated();\n const role = this.role();\n if (role === 'admin') return true;\n return role !== '' && role === required;\n }\n\n /**\n * §5.3 — el ÚNICO criterio de visibilidad del front (menú, rutas y\n * botones). `requires` viene del vocabulario de §4.4: `public` no pide\n * sesión; todo lo demás sí; `roles` estrecha.\n *\n * Es **pura decisión de UI**. El back re-verifica `accessLevel` en cada\n * petición (R2.6), así que filtrar acá no protege nada: sólo evita\n * enseñarle a alguien un botón que le va a devolver 403.\n */\n canAccess(access?: { requires?: string; roles?: string[] }): boolean {\n const requires = access?.requires ?? 'session';\n if (requires === 'public') return true;\n if (!this.isAuthenticated()) return false;\n\n // §4.4 escribe el requerimiento como `role:admin`; §5.3 como\n // `roles: ['admin']`. Se aceptan las dos formas en lugar de elegir una:\n // son el mismo dato visto desde el nivel de acceso y desde el recurso.\n const role = requires.startsWith('role:') ? requires.slice('role:'.length) : '';\n const required = role === '' ? (access?.roles ?? []) : [role];\n if (required.length === 0) return true;\n return required.some((one) => this.hasRole(one));\n }\n\n // ── R7.5 · Consumo de políticas en UI (F10) ───────────────────────────────\n\n /**\n * Evalúa si el usuario puede ejecutar una operación específica según\n * `policies.access.operations` del recurso (B8 · R7.4a).\n *\n * El override por operación tiene prioridad sobre el umbral del grupo.\n * Si no hay override, se usa el umbral `policies.access.read` o\n * `policies.access.write` según la operación.\n *\n * Fallback a `permissions` (legacy) si `policies` no existe: mientras\n * los schemas locales no tengan `policies`, se mantiene compatibilidad.\n *\n * Es **pura decisión de UI**. El back vuelve a verificar en cada petición\n * (24-crud), así que esto sólo evita mostrar botones que darían 403.\n */\n canDoOperation(\n schema: ResourceSchema,\n operation: 'list' | 'get' | 'create' | 'update' | 'remove',\n ): boolean {\n // R7.5: intentar policies primero\n const ops = schema.policies?.access?.operations;\n if (ops && ops[operation]) {\n return this.meetsLevel(ops[operation]);\n }\n\n // Sin override en policies: usar el umbral del grupo\n if (schema.policies?.access) {\n const isWrite = operation === 'create' || operation === 'update' || operation === 'remove';\n const groupLevel = isWrite ? schema.policies.access.write : schema.policies.access.read;\n return this.meetsLevel(groupLevel ?? 'admin');\n }\n\n // Fallback a permissions (legacy, R7.5 transición)\n const perms = schema.permissions;\n if (perms) {\n const permMap: Record<string, boolean | undefined> = {\n list: true, // list no tiene en permissions, siempre true si no hay policies\n get: true,\n create: perms.create,\n update: perms.update,\n remove: perms.remove,\n };\n return permMap[operation] !== false;\n }\n\n // Sin policies ni permissions: UX optimista (§5.3 legacy) — el back es\n // la autoridad y responde 403 si el rol no alcanza.\n return true;\n }\n\n /**\n * Evalúa si el usuario puede leer un campo según `field.access.read`\n * (B8 · R7.4b).\n *\n * Es **pura decisión de UI**. El back vuelve a filtrar la respuesta en\n * cada petición (24-crud), así que esto sólo evita mostrar datos que el\n * usuario no debería ver.\n */\n canReadField(field: FieldSchema): boolean {\n if (!field.access || !field.access.read) return true; // sin restricción ⇒ visible\n return this.meetsLevel(field.access.read);\n }\n\n /**\n * Evalúa si el usuario puede escribir un campo según `field.access.write`\n * (B8 · R7.4b).\n *\n * Es **pura decisión de UI**. El back rechaza la escritura con 422 si el\n * usuario no tiene permiso, así que esto sólo pone el campo en\n * solo-lectura en el frontend.\n */\n canWriteField(field: FieldSchema): boolean {\n if (!field.access || !field.access.write) return true; // sin restricción ⇒ editable\n return this.meetsLevel(field.access.write);\n }\n\n /**\n * Verifica si el rol actual cumple un nivel de acceso dado.\n * Usa la misma jerarquía que `ACCESS_ROLE_RANK` del back (11-auth):\n * admin ≥ staff.\n */\n private meetsLevel(level: string): boolean {\n if (level === 'public') return true;\n if (level === 'session') return this.isAuthenticated();\n if (level.startsWith('role:')) {\n const requiredRole = level.slice('role:'.length);\n return this.hasRole(requiredRole);\n }\n // fallback conservador: nivel desconocido ⇒ sólo admin\n return this.hasRole('admin');\n }\n\n private open(token: string | null, user: SessionUser | null): void {\n if (token !== null && token !== '') this._token.set(token);\n this._user.set(user);\n if (token !== null && token !== '') this.storage.setItem(SESSION_TOKEN_KEY, token);\n if (user !== null) this.storage.setItem(SESSION_USER_KEY, JSON.stringify(user));\n }\n\n /**\n * Borra token **y** usuario (§5.4) — en ese orden de importancia: un token\n * sin usuario no autentica, pero un usuario sin token engaña al menú.\n */\n /**\n * Ruta previa guardada por un `AUTH_EXPIRED` (§4.5), **consumida**: se\n * devuelve una sola vez y se borra. Sin el consume, cada login futuro\n * volvería a la misma URL vieja aunque el usuario hubiera llegado al\n * login desde el menú.\n */\n consumeReturnUrl(): string | null {\n const url = this._returnUrl();\n this._returnUrl.set(null);\n return url;\n }\n\n private clear(): void {\n this._token.set(null);\n this._user.set(null);\n this.storage.removeItem(SESSION_TOKEN_KEY);\n this.storage.removeItem(SESSION_USER_KEY);\n }\n\n private goLogin(): void {\n if (this.currentUrl().startsWith(LOGIN_PATH)) return;\n // Angular rechaza la promesa cuando no hay ruta que empate (NG04002).\n // Navegar es un efecto, no una operación cuyo resultado alguien espere:\n // el catch evita una rejection sin handler en arranques y en tests.\n void this.router.navigate([LOGIN_PATH]).catch(() => undefined);\n }\n\n private currentUrl(): string {\n return this.router.url;\n }\n\n private envelope(path: string, payload: Record<string, unknown> = {}) {\n return { token: this._token(), method: 'POST' as const, path, payload };\n }\n\n /**\n * Mismo transporte que `ApiService` (R2.8): cuerpo JSON con\n * `Content-Type: text/plain` ⇒ **simple request**, sin preflight CORS.\n * Los tres endpoints de sesión pasan por acá — si alguno se saltara el\n * header, el navegador añadiría `application/json` y la llamada empezaría\n * a pedir permiso al servidor con un OPTIONS.\n */\n private post<T>(path: string, payload: Record<string, unknown> = {}): Observable<T> {\n return this.http.post<T>(this.apiUrl, this.envelope(path, payload), {\n headers: { 'Content-Type': 'text/plain' },\n });\n }\n}\n","import { HttpClient } from '@angular/common/http';\nimport { inject, Service } from '@angular/core';\nimport { map, Observable, tap, throwError } from 'rxjs';\n\nimport { renewedTokenOf } from '../models/api.model';\nimport type { ResourceEndpoints } from '../models/schema.model';\nimport { API_URL } from '../tokens';\nimport { AuthService } from './auth.service';\n\n/** Métodos que entiende el envelope del backend (baseapi §2.3). */\nexport type EnvelopeMethod = 'GET' | 'POST' | 'PUT' | 'PATCH' | 'DELETE';\n\n/**\n * CRUD genérico sobre el contrato v2 (baseapi §2.3): TODA llamada a la API\n * es un `POST` a `API_URL` con cuerpo `text/plain` y el JSON\n * `{ token, method, path, payload }`. `method: 'GET'` también para lecturas;\n * el token viaja en el cuerpo (nunca en cabeceras: criterio 11 de §17).\n *\n * `text/plain` en lugar de `application/json` evita el preflight CORS\n * (simple request) — criterio 11: sin cabeceras custom.\n *\n * **R2.8 — convención sobre el snippet.** `auth.md` §5.2 llama a las claves\n * `{ route, op, token, payload }`, pero el backend (`parseEnvelope_`) lee\n * `path` y `method`: el snippet es un esquema a medias, no un contrato.\n * Mismo precedente que en R2.5 — **la convención del código gana**. Cambiar\n * el nombre ahora rompería el único transporte que existe.\n *\n * **R2.12 — el token sale de `AuthService`, no de configuración.** El\n * `ADMIN_TOKEN` (D21-C) se retiró: no hay credencial fija, hay sesión.\n *\n * Devuelve el JSON plano tal cual, sin transformaciones ocultas\n * (base.md §1, principio 4). Los errores del backend (200 + `{ error: {…} }`)\n * los convierte en `ApiError` el interceptor `apiErrorInterceptor`, que es\n * también quien pasa los `AUTH_*` a `AuthService.applyAuthCode` (R2.9).\n */\n@Service()\nexport class ApiService {\n private readonly http = inject(HttpClient);\n private readonly apiUrl = inject(API_URL);\n private readonly auth = inject(AuthService);\n\n /**\n * Listado. El backend envuelve la respuesta cuando la operación declara\n * `shape.envelope: 'list'` (F5) ⇒ `{ items, nextCursor, total }` (R6.4);\n * la UI consume `T[]` en todos lados (F7-3) y este es el ÚNICO punto que\n * habla de listas, así que el desenvolvimiento vive acá. `nextCursor` es\n * el token de la página siguiente y `null` en la última — éste servicio\n * lo descarta hoy porque la lista no pagina todavía (backlog-ui-ux).\n * Un listado sin envelope (arreglo plano, el contrato histórico) pasa\n * tal cual.\n *\n * `payload` sólo lo usa una fuente declarada (F8 · R6.5): `{ view, params }`\n * pide el listado filtrado por una vista y sus parámetros. Sin él, la\n * llamada es la de siempre — la lectura llana del recurso.\n */\n list<T>(endpoint: ResourceEndpoints, payload?: Record<string, unknown>): Observable<T[]> {\n return this.send<unknown>(endpoint.list, 'GET', payload).pipe(map((raw) => unwrapList<T>(raw)));\n }\n\n /** `key` solo aplica a colecciones (`{key}` en la plantilla); los singleton no la llevan. */\n get<T>(endpoint: ResourceEndpoints, key?: string): Observable<T> {\n return this.send<T>(this.withKey(endpoint.get, key), 'GET');\n }\n\n create<T>(endpoint: ResourceEndpoints, payload: Record<string, unknown>): Observable<T> {\n return this.send<T>(endpoint.create, 'POST', payload);\n }\n\n update<T>(\n endpoint: ResourceEndpoints,\n key: string | undefined,\n payload: Record<string, unknown>,\n ): Observable<T> {\n return this.send<T>(this.withKey(endpoint.update, key), 'PUT', payload);\n }\n\n remove(endpoint: ResourceEndpoints, key?: string): Observable<void> {\n return this.send<void>(this.withKey(endpoint.remove, key), 'DELETE');\n }\n\n /**\n * Operación genérica sobre una ruta ya resuelta: reorder de colección\n * (`PUT /admin/x` con `{ reorder: … }`), `/admin/schema`,\n * `/admin/upload-signature`, etc.\n */\n request<T>(\n method: EnvelopeMethod,\n path: string | undefined,\n payload?: Record<string, unknown>,\n ): Observable<T> {\n return this.send<T>(path, method, payload);\n }\n\n private send<T>(\n path: string | undefined,\n method: EnvelopeMethod,\n payload?: Record<string, unknown>,\n ): Observable<T> {\n if (!path) {\n return throwError(() => new Error('Operación no disponible para este recurso'));\n }\n // Token de la SESIÓN, leído en cada petición (R2.8): no hay credencial\n // fija que inyectar en el arranque.\n const body = { token: this.auth.token(), method, path, payload: payload ?? {} };\n return this.http.post<T>(this.apiUrl, body, { headers: { 'Content-Type': 'text/plain' } }).pipe(\n // Renovación deslizante (§3.2): si el back adjuntó `meta.renewedToken`\n // se adopta YA, con lo que la siguiente petición ya va firmada nueva.\n tap((res) => {\n const renewed = renewedTokenOf(res);\n if (renewed !== null) this.auth.applyRenewedToken(renewed);\n }),\n );\n }\n\n private withKey(path: string | undefined, key?: string): string | undefined {\n if (!path) {\n return undefined;\n }\n return path.includes('{key}') ? path.replace('{key}', encodeURIComponent(key ?? '')) : path;\n }\n}\n\n/** `{ items, nextCursor, total }` (F5/R6.4) → `T[]`; arreglo plano → tal cual; otro ⇒ []. */\nfunction unwrapList<T>(raw: unknown): T[] {\n if (Array.isArray(raw)) {\n return raw as T[];\n }\n if (raw !== null && typeof raw === 'object') {\n const items = (raw as { items?: unknown }).items;\n if (Array.isArray(items)) {\n return items as T[];\n }\n }\n return [];\n}\n"],"mappings":"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"}
|
|
@@ -0,0 +1,84 @@
|
|
|
1
|
+
import * as i0 from "@angular/core";
|
|
2
|
+
import { Component, computed, input } from "@angular/core";
|
|
3
|
+
const KINDS = {
|
|
4
|
+
success: "bg-success text-surface",
|
|
5
|
+
danger: "bg-danger text-surface",
|
|
6
|
+
warning: "bg-accent/20 text-accent-text font-semibold",
|
|
7
|
+
info: "bg-primary text-surface",
|
|
8
|
+
neutral: "bg-neutral text-surface"
|
|
9
|
+
};
|
|
10
|
+
var Badge = class Badge {
|
|
11
|
+
label = input.required(...ngDevMode ? [{ debugName: "label" }] : /* istanbul ignore next */ []);
|
|
12
|
+
kind = input("neutral", ...ngDevMode ? [{ debugName: "kind" }] : /* istanbul ignore next */ []);
|
|
13
|
+
classes = computed(() => `inline-flex items-center rounded-full px-2 py-0.5 text-xs font-medium ${KINDS[this.kind()]}`, ...ngDevMode ? [{ debugName: "classes" }] : /* istanbul ignore next */ []);
|
|
14
|
+
static ɵfac = i0.ɵɵngDeclareFactory({
|
|
15
|
+
minVersion: "12.0.0",
|
|
16
|
+
version: "22.2.3",
|
|
17
|
+
ngImport: i0,
|
|
18
|
+
type: Badge,
|
|
19
|
+
deps: [],
|
|
20
|
+
target: i0.ɵɵFactoryTarget.Component
|
|
21
|
+
});
|
|
22
|
+
static ɵcmp = i0.ɵɵngDeclareComponent({
|
|
23
|
+
minVersion: "17.1.0",
|
|
24
|
+
version: "22.2.3",
|
|
25
|
+
type: Badge,
|
|
26
|
+
isStandalone: true,
|
|
27
|
+
selector: "app-badge",
|
|
28
|
+
inputs: {
|
|
29
|
+
label: {
|
|
30
|
+
classPropertyName: "label",
|
|
31
|
+
publicName: "label",
|
|
32
|
+
isSignal: true,
|
|
33
|
+
isRequired: true,
|
|
34
|
+
transformFunction: null
|
|
35
|
+
},
|
|
36
|
+
kind: {
|
|
37
|
+
classPropertyName: "kind",
|
|
38
|
+
publicName: "kind",
|
|
39
|
+
isSignal: true,
|
|
40
|
+
isRequired: false,
|
|
41
|
+
transformFunction: null
|
|
42
|
+
}
|
|
43
|
+
},
|
|
44
|
+
host: { properties: { "class": "classes()" } },
|
|
45
|
+
ngImport: i0,
|
|
46
|
+
template: `{{ label() }}`,
|
|
47
|
+
isInline: true
|
|
48
|
+
});
|
|
49
|
+
};
|
|
50
|
+
i0.ɵɵngDeclareClassMetadata({
|
|
51
|
+
minVersion: "12.0.0",
|
|
52
|
+
version: "22.2.3",
|
|
53
|
+
ngImport: i0,
|
|
54
|
+
type: Badge,
|
|
55
|
+
decorators: [{
|
|
56
|
+
type: Component,
|
|
57
|
+
args: [{
|
|
58
|
+
selector: "app-badge",
|
|
59
|
+
host: { "[class]": "classes()" },
|
|
60
|
+
template: `{{ label() }}`
|
|
61
|
+
}]
|
|
62
|
+
}],
|
|
63
|
+
propDecorators: {
|
|
64
|
+
label: [{
|
|
65
|
+
type: i0.Input,
|
|
66
|
+
args: [{
|
|
67
|
+
isSignal: true,
|
|
68
|
+
alias: "label",
|
|
69
|
+
required: true
|
|
70
|
+
}]
|
|
71
|
+
}],
|
|
72
|
+
kind: [{
|
|
73
|
+
type: i0.Input,
|
|
74
|
+
args: [{
|
|
75
|
+
isSignal: true,
|
|
76
|
+
alias: "kind",
|
|
77
|
+
required: false
|
|
78
|
+
}]
|
|
79
|
+
}]
|
|
80
|
+
}
|
|
81
|
+
});
|
|
82
|
+
export { Badge as t };
|
|
83
|
+
|
|
84
|
+
//# sourceMappingURL=tinya-code-gas-admin-ui-badge-EeCA_l-o.mjs.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"tinya-code-gas-admin-ui-badge-EeCA_l-o.mjs","names":[],"sources":["../../../projects/gas-admin-ui/src/lib/shared/components/badge/badge.ts"],"sourcesContent":["import { Component, computed, input } from '@angular/core';\n\nexport type BadgeKind = 'success' | 'danger' | 'warning' | 'info' | 'neutral';\n\nconst KINDS: Record<BadgeKind, string> = {\n success: 'bg-success text-surface',\n danger: 'bg-danger text-surface',\n warning: 'bg-accent/20 text-accent-text font-semibold',\n info: 'bg-primary text-surface',\n neutral: 'bg-neutral text-surface',\n};\n\n/**\n * Etiqueta compacta (booleanos, estados).\n *\n * El texto va con `text-surface` y no `text-white` porque el fondo lo da\n * el token y en oscuro se aclara: blanco sobre `--color-danger` oscuro\n * sería 2,8:1. `--color-surface` vale #fff en claro, así que el render\n * actual no cambia, y en oscuro el texto se vuelve oscuro sobre el tono\n * aclarado (6,4–9,2:1).\n */\n@Component({\n selector: 'app-badge',\n host: { '[class]': 'classes()' },\n template: `{{ label() }}`,\n})\nexport class Badge {\n readonly label = input.required<string>();\n readonly kind = input<BadgeKind>('neutral');\n\n readonly classes = computed(\n () =>\n `inline-flex items-center rounded-full px-2 py-0.5 text-xs font-medium ${KINDS[this.kind()]}`,\n );\n}\n"],"mappings":";;AAIA,MAAM,QAAmC;CACvC,SAAS;CACT,QAAQ;CACR,SAAS;CACT,MAAM;CACN,SAAS;AACV;AAgBD,IAAa,QAAb,MAAa,MAAK;CACP,QAAQ,MAAM,SAAkB,GAAA,YAAA,CAAA,EAAA,WAAA,QAAA,CAAA,+BAAA,CAAA,CAAA;CAChC,OAAO,MAAiB,WAAU,GAAA,YAAA,CAAA,EAAA,WAAA,OAAA,CAAA,+BAAA,CAAA,CAAA;CAElC,UAAU,eAEf,yEAAyE,MAAM,KAAK,KAAI,MAC3F,GAAA,YAAA,CAAA,EAAA,WAAA,UAAA,CAAA,+BAAA,CAAA,CAAA;CAPU,OAAA,OAAA,GAAA,mBAAA;EAAA,YAAA;EAAA,SAAA;EAAA,UAAA;EAAA,MAAA;EAAK,MAAA,CAAA;EAAA,QAAA,GAAA,gBAAA;CAAA,CAAA;CAAL,OAAA,OAAA,GAAA,qBAAA;EAAA,YAAA;EAAA,SAAA;EAAA,MAAA;EAAK,cAAA;EAAA,UAAA;EAAA,QAAA;GAAA,OAAA;IAAA,mBAAA;IAAA,YAAA;IAAA,UAAA;IAAA,YAAA;IAAA,mBAAA;GAAA;GAAA,MAAA;IAAA,mBAAA;IAAA,YAAA;IAAA,UAAA;IAAA,YAAA;IAAA,mBAAA;GAAA;EAAA;EAAA,MAAA,EAAA,YAAA,EAAA,SAAA,YAAA,EAAA;EAAA,UAAA;EAAA,UAFN;EAAe,UAAA;CAAA,CAAA;;AAEd,GAAA,yBAAA;CAAA,YAAA;CAAA,SAAA;CAAA,UAAA;CAAA,MAAA;CAAK,YAAA,CAAA;EALjB,MAAA;EAAU,MAAA,CAAA;GACT,UAAU;GACV,MAAM,EAAE,WAAW,YAAW;GAC9B,UAAU;EACX,CAAA"}
|
|
@@ -0,0 +1,140 @@
|
|
|
1
|
+
import * as i0 from "@angular/core";
|
|
2
|
+
import { Component, computed, input } from "@angular/core";
|
|
3
|
+
var BarChart = class BarChart {
|
|
4
|
+
label = input.required(...ngDevMode ? [{ debugName: "label" }] : /* istanbul ignore next */ []);
|
|
5
|
+
data = input([], ...ngDevMode ? [{ debugName: "data" }] : /* istanbul ignore next */ []);
|
|
6
|
+
unit = input("", ...ngDevMode ? [{ debugName: "unit" }] : /* istanbul ignore next */ []);
|
|
7
|
+
max = computed(() => Math.max(0, ...this.data().map((row) => row.value)), ...ngDevMode ? [{ debugName: "max" }] : /* istanbul ignore next */ []);
|
|
8
|
+
height(value) {
|
|
9
|
+
if (value === 0) return 0;
|
|
10
|
+
const max = this.max();
|
|
11
|
+
if (max === 0) return 0;
|
|
12
|
+
return Math.max(2, value / max * 100);
|
|
13
|
+
}
|
|
14
|
+
summary = computed(() => {
|
|
15
|
+
const rows = this.data();
|
|
16
|
+
if (rows.length === 0) return "Sin datos.";
|
|
17
|
+
const top = rows.reduce((best, row) => row.value > best.value ? row : best, rows[0]);
|
|
18
|
+
const unit = this.unit();
|
|
19
|
+
return `${rows.length} categorías. Mayor: ${top.label} con ${top.value}${unit}. Menor: ${rows.reduce((low, row) => row.value < low.value ? row : low, rows[0]).label}.`;
|
|
20
|
+
}, ...ngDevMode ? [{ debugName: "summary" }] : /* istanbul ignore next */ []);
|
|
21
|
+
static ɵfac = i0.ɵɵngDeclareFactory({
|
|
22
|
+
minVersion: "12.0.0",
|
|
23
|
+
version: "22.2.3",
|
|
24
|
+
ngImport: i0,
|
|
25
|
+
type: BarChart,
|
|
26
|
+
deps: [],
|
|
27
|
+
target: i0.ɵɵFactoryTarget.Component
|
|
28
|
+
});
|
|
29
|
+
static ɵcmp = i0.ɵɵngDeclareComponent({
|
|
30
|
+
minVersion: "17.0.0",
|
|
31
|
+
version: "22.2.3",
|
|
32
|
+
type: BarChart,
|
|
33
|
+
isStandalone: true,
|
|
34
|
+
selector: "app-bar-chart",
|
|
35
|
+
inputs: {
|
|
36
|
+
label: {
|
|
37
|
+
classPropertyName: "label",
|
|
38
|
+
publicName: "label",
|
|
39
|
+
isSignal: true,
|
|
40
|
+
isRequired: true,
|
|
41
|
+
transformFunction: null
|
|
42
|
+
},
|
|
43
|
+
data: {
|
|
44
|
+
classPropertyName: "data",
|
|
45
|
+
publicName: "data",
|
|
46
|
+
isSignal: true,
|
|
47
|
+
isRequired: false,
|
|
48
|
+
transformFunction: null
|
|
49
|
+
},
|
|
50
|
+
unit: {
|
|
51
|
+
classPropertyName: "unit",
|
|
52
|
+
publicName: "unit",
|
|
53
|
+
isSignal: true,
|
|
54
|
+
isRequired: false,
|
|
55
|
+
transformFunction: null
|
|
56
|
+
}
|
|
57
|
+
},
|
|
58
|
+
ngImport: i0,
|
|
59
|
+
template: `
|
|
60
|
+
<div class="rounded-lg border border-neutral/20 bg-surface p-4">
|
|
61
|
+
<p class="mb-3 text-sm text-neutral">{{ label() }}</p>
|
|
62
|
+
|
|
63
|
+
@if (data().length === 0) {
|
|
64
|
+
<p class="text-sm text-neutral/60" data-testid="empty">Sin datos.</p>
|
|
65
|
+
} @else {
|
|
66
|
+
<div role="img" [attr.aria-label]="summary()" class="flex h-32 items-end gap-2">
|
|
67
|
+
@for (row of data(); track row.label) {
|
|
68
|
+
<div class="flex min-w-0 flex-1 flex-col items-center justify-end gap-1">
|
|
69
|
+
<span class="text-xs tabular-nums text-neutral"> {{ row.value }}{{ unit() }} </span>
|
|
70
|
+
<div class="w-full rounded-t bg-primary" [style.height.%]="height(row.value)"></div>
|
|
71
|
+
<span class="w-full truncate text-center text-xs text-neutral">{{ row.label }}</span>
|
|
72
|
+
</div>
|
|
73
|
+
}
|
|
74
|
+
</div>
|
|
75
|
+
}
|
|
76
|
+
</div>
|
|
77
|
+
`,
|
|
78
|
+
isInline: true
|
|
79
|
+
});
|
|
80
|
+
};
|
|
81
|
+
i0.ɵɵngDeclareClassMetadata({
|
|
82
|
+
minVersion: "12.0.0",
|
|
83
|
+
version: "22.2.3",
|
|
84
|
+
ngImport: i0,
|
|
85
|
+
type: BarChart,
|
|
86
|
+
decorators: [{
|
|
87
|
+
type: Component,
|
|
88
|
+
args: [{
|
|
89
|
+
selector: "app-bar-chart",
|
|
90
|
+
template: `
|
|
91
|
+
<div class="rounded-lg border border-neutral/20 bg-surface p-4">
|
|
92
|
+
<p class="mb-3 text-sm text-neutral">{{ label() }}</p>
|
|
93
|
+
|
|
94
|
+
@if (data().length === 0) {
|
|
95
|
+
<p class="text-sm text-neutral/60" data-testid="empty">Sin datos.</p>
|
|
96
|
+
} @else {
|
|
97
|
+
<div role="img" [attr.aria-label]="summary()" class="flex h-32 items-end gap-2">
|
|
98
|
+
@for (row of data(); track row.label) {
|
|
99
|
+
<div class="flex min-w-0 flex-1 flex-col items-center justify-end gap-1">
|
|
100
|
+
<span class="text-xs tabular-nums text-neutral"> {{ row.value }}{{ unit() }} </span>
|
|
101
|
+
<div class="w-full rounded-t bg-primary" [style.height.%]="height(row.value)"></div>
|
|
102
|
+
<span class="w-full truncate text-center text-xs text-neutral">{{ row.label }}</span>
|
|
103
|
+
</div>
|
|
104
|
+
}
|
|
105
|
+
</div>
|
|
106
|
+
}
|
|
107
|
+
</div>
|
|
108
|
+
`
|
|
109
|
+
}]
|
|
110
|
+
}],
|
|
111
|
+
propDecorators: {
|
|
112
|
+
label: [{
|
|
113
|
+
type: i0.Input,
|
|
114
|
+
args: [{
|
|
115
|
+
isSignal: true,
|
|
116
|
+
alias: "label",
|
|
117
|
+
required: true
|
|
118
|
+
}]
|
|
119
|
+
}],
|
|
120
|
+
data: [{
|
|
121
|
+
type: i0.Input,
|
|
122
|
+
args: [{
|
|
123
|
+
isSignal: true,
|
|
124
|
+
alias: "data",
|
|
125
|
+
required: false
|
|
126
|
+
}]
|
|
127
|
+
}],
|
|
128
|
+
unit: [{
|
|
129
|
+
type: i0.Input,
|
|
130
|
+
args: [{
|
|
131
|
+
isSignal: true,
|
|
132
|
+
alias: "unit",
|
|
133
|
+
required: false
|
|
134
|
+
}]
|
|
135
|
+
}]
|
|
136
|
+
}
|
|
137
|
+
});
|
|
138
|
+
export { BarChart };
|
|
139
|
+
|
|
140
|
+
//# sourceMappingURL=tinya-code-gas-admin-ui-bar-chart-G7sEMSBY.mjs.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"tinya-code-gas-admin-ui-bar-chart-G7sEMSBY.mjs","names":[],"sources":["../../../projects/gas-admin-ui/src/lib/widgets/bar-chart/bar-chart.ts"],"sourcesContent":["import { Component, computed, input } from '@angular/core';\n\nexport interface BarDatum {\n label: string;\n value: number;\n}\n\n/**\n * Barras por categoría, sin librería: se dibujan con CSS y un `@for` de\n * Angular (~1 K gz frente a los 61,4 K de Chart.js — viabilidad B2.1).\n *\n * Accesibilidad (plan 3.2): la grilla es `role=\"img\"` con un `aria-label`\n * que resume los datos, de modo que un lector de pantalla no recorra cada\n * barra suelta sino que escuche «8 categorías. Mayor: …».\n */\n@Component({\n selector: 'app-bar-chart',\n template: `\n <div class=\"rounded-lg border border-neutral/20 bg-surface p-4\">\n <p class=\"mb-3 text-sm text-neutral\">{{ label() }}</p>\n\n @if (data().length === 0) {\n <p class=\"text-sm text-neutral/60\" data-testid=\"empty\">Sin datos.</p>\n } @else {\n <div role=\"img\" [attr.aria-label]=\"summary()\" class=\"flex h-32 items-end gap-2\">\n @for (row of data(); track row.label) {\n <div class=\"flex min-w-0 flex-1 flex-col items-center justify-end gap-1\">\n <span class=\"text-xs tabular-nums text-neutral\"> {{ row.value }}{{ unit() }} </span>\n <div class=\"w-full rounded-t bg-primary\" [style.height.%]=\"height(row.value)\"></div>\n <span class=\"w-full truncate text-center text-xs text-neutral\">{{ row.label }}</span>\n </div>\n }\n </div>\n }\n </div>\n `,\n})\nexport class BarChart {\n /** Título del gráfico. */\n readonly label = input.required<string>();\n /** Series a dibujar; vacío ⇒ estado «Sin datos». */\n readonly data = input<BarDatum[]>([]);\n /** Sufijo de las cifras del eje («unidades», «S/»). */\n readonly unit = input('');\n\n private readonly max = computed(() => Math.max(0, ...this.data().map((row) => row.value)));\n\n /**\n * Altura relativa de una barra. `max = 0` ⇒ 0 % (nada de división por\n * cero); un valor de 0 se queda en 0, y los demás bajan a un 2 % mínimo\n * para que la barra siga siendo visible.\n */\n protected height(value: number): number {\n if (value === 0) {\n return 0;\n }\n const max = this.max();\n if (max === 0) {\n return 0;\n }\n return Math.max(2, (value / max) * 100);\n }\n\n protected readonly summary = computed(() => {\n const rows = this.data();\n if (rows.length === 0) {\n return 'Sin datos.';\n }\n const top = rows.reduce((best, row) => (row.value > best.value ? row : best), rows[0]);\n const unit = this.unit();\n return (\n `${rows.length} categorías. Mayor: ${top.label} con ${top.value}${unit}. ` +\n `Menor: ${rows.reduce((low, row) => (row.value < low.value ? row : low), rows[0]).label}.`\n );\n });\n}\n"],"mappings":";;AAqCA,IAAa,WAAb,MAAa,SAAQ;CAEV,QAAQ,MAAM,SAAkB,GAAA,YAAA,CAAA,EAAA,WAAA,QAAA,CAAA,+BAAA,CAAA,CAAA;CAEhC,OAAO,MAAkB,CAAA,GAAG,GAAA,YAAA,CAAA,EAAA,WAAA,OAAA,CAAA,+BAAA,CAAA,CAAA;CAE5B,OAAO,MAAM,IAAG,GAAA,YAAA,CAAA,EAAA,WAAA,OAAA,CAAA,+BAAA,CAAA,CAAA;CAER,MAAM,eAAe,KAAK,IAAI,GAAG,GAAG,KAAK,KAAI,CAAE,CAAC,KAAK,QAAQ,IAAI,KAAK,CAAC,GAAE,GAAA,YAAA,CAAA,EAAA,WAAA,MAAA,CAAA,+BAAA,CAAA,CAAA;CAOhF,OAAO,OAAa;EAC5B,IAAI,UAAU,GACZ,OAAO;EAET,MAAM,MAAM,KAAK,IAAG;EACpB,IAAI,QAAQ,GACV,OAAO;EAET,OAAO,KAAK,IAAI,GAAI,QAAQ,MAAO,GAAG;CACxC;CAEmB,UAAU,eAAc;EACzC,MAAM,OAAO,KAAK,KAAI;EACtB,IAAI,KAAK,WAAW,GAClB,OAAO;EAET,MAAM,MAAM,KAAK,QAAQ,MAAM,QAAS,IAAI,QAAQ,KAAK,QAAQ,MAAM,MAAO,KAAK,EAAE;EACrF,MAAM,OAAO,KAAK,KAAI;EACtB,OACE,GAAG,KAAK,OAAM,sBAAuB,IAAI,MAAK,OAAQ,IAAI,QAAQ,KAAI,WAC5D,KAAK,QAAQ,KAAK,QAAS,IAAI,QAAQ,IAAI,QAAQ,MAAM,KAAM,KAAK,EAAE,CAAC,CAAC,MAAK;CAE3F,GAAE,GAAA,YAAA,CAAA,EAAA,WAAA,UAAA,CAAA,+BAAA,CAAA,CAAA;CArCS,OAAA,OAAA,GAAA,mBAAA;EAAA,YAAA;EAAA,SAAA;EAAA,UAAA;EAAA,MAAA;EAAQ,MAAA,CAAA;EAAA,QAAA,GAAA,gBAAA;CAAA,CAAA;CAAR,OAAA,OAAA,GAAA,qBAAA;EAAA,YAAA;EAAA,SAAA;EAAA,MAAA;EAAQ,cAAA;EAAA,UAAA;EAAA,QAAA;GAAA,OAAA;IAAA,mBAAA;IAAA,YAAA;IAAA,UAAA;IAAA,YAAA;IAAA,mBAAA;GAAA;GAAA,MAAA;IAAA,mBAAA;IAAA,YAAA;IAAA,UAAA;IAAA,YAAA;IAAA,mBAAA;GAAA;GAAA,MAAA;IAAA,mBAAA;IAAA,YAAA;IAAA,UAAA;IAAA,YAAA;IAAA,mBAAA;GAAA;EAAA;EAAA,UAAA;EAAA,UApBT;;;;;;;;;;;;;;;;;;;EAkBT,UAAA;CAAA,CAAA;;AAEU,GAAA,yBAAA;CAAA,YAAA;CAAA,SAAA;CAAA,UAAA;CAAA,MAAA;CAAQ,YAAA,CAAA;EAtBpB,MAAA;EAAU,MAAA,CAAA;GACT,UAAU;GACV,UAAU;;;;;;;;;;;;;;;;;;;EAmBX,CAAA"}
|