Project

General

Profile

Form List - Orders » History » Version 1

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

1 1 Lê Sĩ Quý
# Form List - Orders
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: 60%;"></div>
33
        <div id="detail" style="display: inline-block; width: 100%; height: 40%;"></div>
34
    </div>
35
36
    <style>
37
        .category-img {
38
            object-fit: cover;
39
            width: 100%;
40
            max-height: 100%;
41
        }
42
    </style>
43
44
    <script type="module">
45
        import { w2toolbar, w2grid } from "https://cdn.jsdelivr.net/npm/w2ui@2.0.0/w2ui-2.0.es6.min.js";
46
        var notifier = new AWN({ option: "top-right" });
47
        var sid = ls.get("sid");
48
        var email = ls.get("email");
49
        var $ds = {};
50
        var detailData = {};
51
        var master, detail;
52
53
        $(function () {
54
            new w2toolbar({
55
                box: "#toolbar",
56
                name: "toolbar",
57
                items: [
58
                    {
59
                        type: "menu", id: "sales", icon: "w2ui-icon-info", text: "Sales",
60
                        items: [
61
                            { id: "customers", text: "Customers", icon: "fa fa-user" },
62
                            { id: "orders", text: "Orders", icon: "fa fa-file-text" },
63
                        ]
64
                    },
65
                    { type: "break" },
66
                    {
67
                        type: "menu", id: "operations", icon: "w2ui-icon-settings", text: "Operations",
68
                        items: [
69
                            { id: "employees", text: "Employees", icon: "fa fa-address-card" },
70
                            { id: "products", text: "Products", icon: "fa fa-archive" },
71
                            { id: "categories", text: "Product Categories", icon: "fa fa-sitemap" },
72
                            { id: "suppliers", text: "Suppliers", icon: "fa fa-user" },
73
                            { id: "shippers", text: "Shippers", icon: "fa fa-shopping-cart" }
74
                        ]
75
                    },
76
                    { type: "spacer" },
77
                    {
78
                        type: "menu", id: "auth", text: email,
79
                        items: [
80
                            { text: "Logout", id: "logout", icon: "fa fa-sign-out" }
81
                        ]
82
                    }
83
                ],
84
                onClick(event) {
85
                    switch (event.target) {
86
                        case "sales:customers": {
87
                            window.open("<?= base ?>?url=/form/customers", "_top");
88
                            break;
89
                        }
90
                        case "sales:orders": {
91
                            window.open("<?= base ?>?url=/form/orders", "_top");
92
                            break;
93
                        }
94
                        case "operations:employees": {
95
                            window.open("<?= base ?>?url=/form/employees", "_top");
96
                            break;
97
                        }
98
                        case "operations:products": {
99
                            window.open("<?= base ?>?url=/form/products", "_top");
100
                            break;
101
                        }
102
                        case "operations:categories": {
103
                            window.open("<?= base ?>?url=/form/categories", "_top");
104
                            break;
105
                        }
106
                        case "operations:suppliers": {
107
                            window.open("<?= base ?>?url=/form/suppliers", "_top");
108
                            break;
109
                        }
110
                        case "operations:shippers": {
111
                            window.open("<?= base ?>?url=/form/shippers", "_top");
112
                            break;
113
                        }
114
                        case "auth:logout": {
115
                            ls.remove("sid");
116
                            ls.remove("email");
117
                            window.open("<?= base ?>?url=/form/login&callback=<?!= url ?>", "_top");
118
                            break;
119
                        }
120
                    }
121
                }
122
            });
123
            master = new w2grid({
124
                name: "master",
125
                box: "#master",
126
                header: "Orders",
127
                show: {
128
                    header: true,
129
                    footer: true,
130
                    toolbar: true,
131
                    toolbarAdd: true,
132
                    toolbarEdit: true
133
                },
134
                columns: [
135
                    { field: "recid", hidden: true },
136
                    { field: "OrderID", text: "Order ID", size: "10%", sortable: true },
137
                    { field: "CustomerID", text: "Customer", size: "15%", sortable: true },
138
                    { field: "EmployeeID", text: "Employee", size: "15%", sortable: true },
139
                    { field: "OrderDate", text: "Order Date", render: "datetime", size: "10%", sortable: true },
140
                    { field: "ShippedDate", text: "Shipped Date", render: "date", size: "10%", sortable: true },
141
                    { field: "ShipViaID", text: "Ship Via", size: "15%", sortable: true },
142
                    { field: "ShipCountry", text: "Ship Country", size: "15%", sortable: true }
143
                ],
144
                searches: [
145
                    { type: "text", field: "OrderID", label: "Order ID" },
146
                    { type: "text", field: "CustomerID", label: "Customer" },
147
                    { type: "text", field: "EmployeeID", label: "Employee" },
148
                    { type: "text", field: "OrderDate", label: "Order Date" },
149
                    { type: "text", field: "ShippedDate", label: "Shipped Date" },
150
                    { type: "text", field: "ShipViaID", label: "Ship Via" },
151
                    { type: "text", field: "ShipCountry", label: "Country" }
152
                ],
153
                onClick(event) {
154
                    let record = this.get(event.detail.recid);
155
156
                    if (record) {
157
                        detail.clear();
158
                        let orders = record.Orders;
159
                        let r = 0;
160
                        orders.forEach((o) => {
161
                            o.ProductID = o.Product["ProductName"];
162
                            o.recid = `${event.detail.recid}-${++r}`;
163
                        });
164
                        let summary = [{
165
                            w2ui: { summary: true },
166
                            recid: "S-1",
167
                            Discount: _.sumBy(orders, (o) => parseFloat(o.Discount)),
168
                            Subtotal: _.sumBy(orders, (o) => parseFloat(o.Subtotal)),
169
                        }];
170
                        detail.add(orders.concat(summary));
171
                    }
172
                },
173
                onAdd: function (event) {
174
                    window.open("<?= base ?>?url=/form/crud-order", "_blank");
175
                },
176
                onEdit: function (event) {
177
                    let recid = event.detail.recid;
178
                    recid && window.open("<?= base ?>?url=/form/crud-order/" + recid, "_blank");
179
                },
180
            });
181
            detail = new w2grid({
182
                name: "detail",
183
                box: "#detail",
184
                header: "Detail Order",
185
                show: { header: true },
186
                name: "detail",
187
                columns: [
188
                    { field: "recid", hidden: true },
189
                    { field: "ProductID", text: "Product", size: "30%", sortable: true },
190
                    { field: "Supplier", text: "Supplier", size: "15%", sortable: true },
191
                    { field: "Category", text: "Category", size: "15%", sortable: true },
192
                    { field: "Quantity", text: "Quantity", size: "10%", render: "int" },
193
                    { field: "UnitPrice", text: "Unit Price", size: "10%", render: "money" },
194
                    { field: "Discount", text: "Discount", size: "10%", render: "money" },
195
                    { field: "Subtotal", text: "Subtotal", size: "10%", render: "money" },
196
                ]
197
            })
198
199
            if (sid) {
200
                google.script.run
201
                    .withSuccessHandler(onCheckSidSuccess)
202
                    .withFailureHandler(onInvalidSid)
203
                    .checkSid(sid);
204
            }
205
            else {
206
                onInvalidSid();
207
            }
208
        });
209
210
        function onInvalidSid(err) {
211
            if (err) {
212
                notifier.warning(err.message);
213
            }
214
            else {
215
                let url = "<?!= base ?>?url=/form/login&callback=<?!= url ?>";
216
                notifier.modal(`<b>You need to login to access this page.</b><br><a href="${url}">👉 <b>Login</b></a>`)
217
            }
218
        }
219
220
        function onCheckSidSuccess(valid) {
221
            if (valid) {
222
                google.script.run.withSuccessHandler((ds) => {
223
                    $ds = ds;
224
                    google.script.run.withSuccessHandler((parents => {
225
                        master.clear();
226
                        let recs = [];
227
                        parents.forEach((parent) => {
228
                            let obj = JSON.parse(parent.Document);
229
                            obj.recid = parent.Id;
230
                            obj.CustomerID = obj.Customer["Company Name"];
231
                            obj.EmployeeID = obj.Employee["LastName"] + " " + obj.Employee["FirstName"];
232
                            obj.ShipViaID = obj.ShipVia["CompanyName"];
233
                            recs.push(obj);
234
                        });
235
                        master.add(recs);
236
                    }))
237
                        .runSql("Default", "where G = 0 and H = 'Orders' options no_format");
238
                })
239
                    .loadDatasources("<?!= datasource ?>");
240
241
            } else {
242
                onInvalidSid();
243
            }
244
        }
245
    </script>
246
247
</body>
248
249
</html>
250
```
251
252
## Giải thích Code
253
254
Form List Orders sử dụng cácthư viện sau:
255
- [jQuery](https://jquery.com/) để khởi tạo grid sau khi page load xong.
256
- [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.
257
- [Notyf](https://carlosroso.com/notyf/) hiển thị thông báo.
258
- [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...
259
- 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. 
260
- Trong page này gọi đến
261
  - 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
262
  - Hàm backend **loadDatasources** để tải các dữ liệu tham chiếu phía client, ví dụ danh sách Products
263
  - 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
264
- Form Orders phức tạp hơn form Customers/Employees vì có dạng master-detai. Khi click 1 master record sẽ thì grid detail sẽ hiển thị chi tiết Order tương ứng
265
266
*Vì Orders được lưu trong dạng document với cơ chế NoSQL with Google Forms nên chúng ta phải có bước tách các thuộc tính của Employee rồi mới thực hiện việc binding thay vì binding trực tiếp như Categories, Shippers*
267
268
``` javascript
269
let obj = JSON.parse(parent.Document);
270
```
271
272
## Demo
273
- Link: https://script.google.com/macros/s/AKfycbwIjB-hULVZdfCtsXFPg4Af_8WoKx2AFf85KMVwnsO_WkeAXW3zarT6vZNFVfwccz1_sA/exec?url=/form/orders
274
- Account: user@northwind.com
275
- Password: user