Project

General

Profile

Form List - Employees » History » Version 3

Lê Sĩ Quý, 08/29/2025 02:02 PM

1 1 Lê Sĩ Quý
# Form List - Employees
2
3
{{TOC}}
4
5
## JSON Schema
6
7
Form List không dùng FormIO vì không có input data, đơn thuần hiển thị danh sách dữ liệu từ spreadsheet. **Nếu dữ liệu chỉ giới hạn truy cập bởi ít người hoặc không cần public thì có thể quản lý data trực tiếp bằng spreadsheet.**
8
9
## HTML Template
10
11
``` html
12
<!DOCTYPE html>
13
<html lang="en">
14
15
<head>
16
    <meta name="viewport"
17
        content="user-scalable=no, initial-scale=1.0001, maximum-scale=1.0001, width=device-width, minimal-ui shrink-to-fit=no">
18
    <meta charset="utf-8" name="viewport" content="width=device-width, initial-scale=1.0" />
19
    <title>Northwind</title>
20
    <link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/awesome-notifications/3.1.0/style.min.css">
21
    <link rel="stylesheet" type="text/css" href="https://cdn.jsdelivr.net/npm/w2ui@2.0.0/w2ui-2.0.min.css">
22
    <link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/4.7.0/css/font-awesome.min.css">
23
    <script src="https://cdnjs.cloudflare.com/ajax/libs/awesome-notifications/3.1.0/modern.var.min.js"></script>
24
    <script src="https://cdn.jsdelivr.net/npm/localstorage-slim"></script>
25
    <script src="https://ajax.googleapis.com/ajax/libs/jquery/1.9.1/jquery.min.js"></script>
26
    <script src="https://cdnjs.cloudflare.com/ajax/libs/lodash.js/4.17.21/lodash.min.js"></script>
27
</head>
28
29
<body>
30
    <div id="toolbar"></div>
31
    <div style="position: relative; height: 800px;">
32
        <div id="master" style="display: inline-block; width: 100%; height: 100%;"></div>
33
    </div>
34
35
    <style>
36
        .category-img {
37
            object-fit: cover;
38
            width: 100%;
39
            max-height: 100%;
40
        }
41
    </style>
42
43
    <script type="module">
44
        import { w2toolbar, w2grid } from "https://cdn.jsdelivr.net/npm/w2ui@2.0.0/w2ui-2.0.es6.min.js";
45
        var notifier = new AWN({ option: "top-right" });
46
        var sid = ls.get("sid");
47
        var email = ls.get("email");
48
        var $ds = {};
49
        var master;
50
51
        $(function () {
52
            new w2toolbar({
53
                box: "#toolbar",
54
                name: "toolbar",
55
                items: [
56
                    {
57
                        type: "menu", id: "sales", icon: "w2ui-icon-info", text: "Sales",
58
                        items: [
59
                            { id: "customers", text: "Customers", icon: "fa fa-user" },
60
                            { id: "orders", text: "Orders", icon: "fa fa-file-text" },
61
                        ]
62
                    },
63
                    { type: 'break' },
64
                    {
65
                        type: "menu", id: "operations", icon: "w2ui-icon-settings", text: "Operations",
66
                        items: [
67
                            { id: "employees", text: "Employees", icon: "fa fa-address-card" },
68
                            { id: "products", text: "Products", icon: "fa fa-archive" },
69
                            { id: "categories", text: "Product Categories", icon: "fa fa-sitemap" },
70
                            { id: "suppliers", text: "Suppliers", icon: "fa fa-user" },
71
                            { id: "shippers", text: "Shippers", icon: "fa fa-shopping-cart" }
72
                        ]
73
                    },
74
                    { type: "spacer" },
75
                    {
76
                        type: "menu", id: "auth", text: email,
77
                        items: [
78
                            { text: "Logout", id: "logout", icon: "fa fa-sign-out" }
79
                        ]
80
                    }
81
                ],
82
                onClick(event) {
83
                    switch (event.target) {
84
                        case "sales:customers": {
85
                            window.open("<?= base ?>?url=/form/customers", "_top");
86
                            break;
87
                        }
88
                        case "sales:orders": {
89
                            window.open("<?= base ?>?url=/form/orders", "_top");
90
                            break;
91
                        }
92
                        case "operations:employees": {
93
                            window.open("<?= base ?>?url=/form/employees", "_top");
94
                            break;
95
                        }
96
                        case "operations:products": {
97
                            window.open("<?= base ?>?url=/form/products", "_top");
98
                            break;
99
                        }
100
                        case "operations:categories": {
101
                            window.open("<?= base ?>?url=/form/categories", "_top");
102
                            break;
103
                        }
104
                        case "operations:suppliers": {
105
                            window.open("<?= base ?>?url=/form/suppliers", "_top");
106
                            break;
107
                        }
108
                        case "operations:shippers": {
109
                            window.open("<?= base ?>?url=/form/shippers", "_top");
110
                            break;
111
                        }
112
                        case "auth:logout": {
113
                            ls.remove("sid");
114
                            ls.remove("email");
115
                            window.open("<?= base ?>?url=/form/login&callback=<?!= url ?>", "_top");
116
                            break;
117
                        }
118
                    }
119
                }
120
            });
121
            master = new w2grid({
122
                name: "master",
123
                box: "#master",
124 2 Lê Sĩ Quý
                header: "Employees",
125 1 Lê Sĩ Quý
                show: {
126
                    header: true,
127
                    footer: true,
128
                    toolbar: true,
129 2 Lê Sĩ Quý
                    toolbarAdd: true,
130
                    toolbarEdit: true
131 1 Lê Sĩ Quý
                },
132
                columns: [
133
                    { field: "recid", hidden: true },
134 2 Lê Sĩ Quý
                    { field: "LastName", text: "Last Name", size: "15%", sortable: true },
135
                    { field: "FirstName", text: "FirstName", size: "15%", sortable: true },
136
                    { field: "Title", text: "Title", size: "11%", sortable: true },
137
                    { field: "BirthDate", text: "BirthDate", size: "7%", render: "date", sortable: true },
138
                    { field: "HireDate", text: "HireDate", size: "7%", render: "date", sortable: true },
139
                    { field: "Address", text: "Address", size: "15%", sortable: true },
140
                    { field: "City", text: "City", size: "10%", sortable: true },
141
                    { field: "Country", text: "Country", size: "10%", sortable: true },
142
                    { field: "ReportsTo", text: "Reports To", size: "10%", sortable: true }
143 1 Lê Sĩ Quý
                ],
144
                searches: [
145 2 Lê Sĩ Quý
                    { type: "text", field: "LastName", label: "Last Name" },
146
                    { type: "text", field: "FirstName", label: "First Name" },
147
                    { type: "text", field: "BirthDate", label: "BirthDate", type: "date" },
148
                    { type: "text", field: "HireDate", label: "HireDate", type: "date" },
149
                    { type: "text", field: "Address", label: "Address" },
150
                    { type: "text", field: "City", label: "City" },
151
                    { type: "text", field: "Country", label: "Country" },
152
                    { type: "text", field: "Reports To", label: "Reports To" },
153 1 Lê Sĩ Quý
                ],
154
                onClick(event) {
155
                    let record = this.get(event.detail.recid);
156
157
                    if (record) {
158
                        // Do nothing
159
                    }
160
                },
161
                onAdd: function (event) {
162 2 Lê Sĩ Quý
                    window.open('<?= base ?>?url=/form/crud-employee', '_blank');
163 1 Lê Sĩ Quý
                },
164
                onEdit: function (event) {
165 2 Lê Sĩ Quý
                    let recid = event.detail.recid;
166
                    recid && window.open('<?= base ?>?url=/form/crud-employee/' + recid, '_blank');
167 1 Lê Sĩ Quý
                },
168
            });
169
170
            if (sid) {
171
                google.script.run
172
                    .withSuccessHandler(onCheckSidSuccess)
173
                    .withFailureHandler(onInvalidSid)
174
                    .checkSid(sid);
175
            }
176
            else {
177
                onInvalidSid();
178
            }
179
        });
180
181
        function onInvalidSid(err) {
182
            if (err) {
183
                notifier.warning(err.message);
184
            }
185
            else {
186
                let url = "<?!= base ?>?url=/form/login&callback=<?!= url ?>";
187
                notifier.modal(`<b>You need to login to access this page.</b><br><a href="${url}">👉 <b>Login</b></a>`)
188
            }
189
        }
190
191
        function onCheckSidSuccess(valid) {
192
            if (valid) {
193
                google.script.run.withSuccessHandler((ds) => {
194
                    $ds = ds;
195
                    google.script.run.withSuccessHandler((parents => {
196
                        master.clear();
197 2 Lê Sĩ Quý
                        let recs = [];
198
                        parents.forEach((parent) => {
199
                            let obj = JSON.parse(parent.Document);
200
                            obj.recid = parent.Id;
201
                            recs.push(obj);
202
                        });
203
                        master.add(recs);
204 1 Lê Sĩ Quý
                    }))
205 3 Lê Sĩ Quý
                        .runSql("Default", "where G = 0 and H = 'Employees' options no_format");
206 1 Lê Sĩ Quý
                })
207
                    .loadDatasources("<?!= datasource ?>");
208
209
            } else {
210
                onInvalidSid();
211
            }
212
        }
213
    </script>
214
215
</body>
216
217
</html>
218
```
219
220
## Giải thích Code
221
222
Form List Employees sử dụng cácthư viện sau:
223
- [jQuery](https://jquery.com/) để khởi tạo grid sau khi page load xong.
224
- [localstorage-slim](https://github.com/digitalfortress-tech/localstorage-slim) để đọc sid & email để bỏ qua bước login nếu trước đó đã login thành công. Trường hợp login không thành công thì sẽ thông báo và điều hướng đến trang login.
225
- [Notyf](https://carlosroso.com/notyf/) hiển thị thông báo.
226
- [w2ui](https://w2ui.com/web/home) cung cấp các thành phần UI cơ bản và nâng cao, ví dụ: toolbar và grid, dialog...
227
- Các thư viện đều được load từ CDN để tối ưu tốc độ, đơn giản hóa HTML Templated. Các bạn cần có kiến thức trong phần yêu cầu để hiểu logic của code JS + templated nhúng trong page. 
228
- Trong page này gọi đến
229
  - Hàm backend **checkSid** sid lưu trữ có khớp với giá trị trên server, nếu đúng thì sẽ sử dụng email trả về, còn sai sẽ điều hướng đến trang login
230
  - Hàm backend **loadDatasources** để tải các dữ liệu tham chiếu phía client, ví dụ danh sách Products
231
  - Hàm backend **runSql** thực hiện query nâng cao trên Datasource chỉ định, ví dụ chỉ hiển thị ra danh sách Orders được tạo bởi Saler đang login
232
233
## Demo
234
- Link: https://script.google.com/macros/s/AKfycbwIjB-hULVZdfCtsXFPg4Af_8WoKx2AFf85KMVwnsO_WkeAXW3zarT6vZNFVfwccz1_sA/exec?url=/form/employees
235
- Account: user@northwind.com
236
- Password: user