Actions
Form List - Orders¶
- Table of contents
- Form List - Orders
JSON Schema¶
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.
HTML Template¶
<!DOCTYPE html>
<html lang="en">
<head>
<meta name="viewport"
content="user-scalable=no, initial-scale=1.0001, maximum-scale=1.0001, width=device-width, minimal-ui shrink-to-fit=no">
<meta charset="utf-8" name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Northwind</title>
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/awesome-notifications/3.1.0/style.min.css">
<link rel="stylesheet" type="text/css" href="https://cdn.jsdelivr.net/npm/w2ui@2.0.0/w2ui-2.0.min.css">
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/4.7.0/css/font-awesome.min.css">
<script src="https://cdnjs.cloudflare.com/ajax/libs/awesome-notifications/3.1.0/modern.var.min.js"></script>
<script src="https://cdn.jsdelivr.net/npm/localstorage-slim"></script>
<script src="https://ajax.googleapis.com/ajax/libs/jquery/1.9.1/jquery.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/lodash.js/4.17.21/lodash.min.js"></script>
</head>
<body>
<div id="toolbar"></div>
<div style="position: relative; height: 800px;">
<div id="master" style="display: inline-block; width: 100%; height: 60%;"></div>
<div id="detail" style="display: inline-block; width: 100%; height: 40%;"></div>
</div>
<style>
.category-img {
object-fit: cover;
width: 100%;
max-height: 100%;
}
</style>
<script type="module">
import { w2toolbar, w2grid } from "https://cdn.jsdelivr.net/npm/w2ui@2.0.0/w2ui-2.0.es6.min.js";
var notifier = new AWN({ option: "top-right" });
var sid = ls.get("sid");
var email = ls.get("email");
var $ds = {};
var detailData = {};
var master, detail;
$(function () {
new w2toolbar({
box: "#toolbar",
name: "toolbar",
items: [
{
type: "menu", id: "sales", icon: "w2ui-icon-info", text: "Sales",
items: [
{ id: "customers", text: "Customers", icon: "fa fa-user" },
{ id: "orders", text: "Orders", icon: "fa fa-file-text" },
]
},
{ type: "break" },
{
type: "menu", id: "operations", icon: "w2ui-icon-settings", text: "Operations",
items: [
{ id: "employees", text: "Employees", icon: "fa fa-address-card" },
{ id: "products", text: "Products", icon: "fa fa-archive" },
{ id: "categories", text: "Product Categories", icon: "fa fa-sitemap" },
{ id: "suppliers", text: "Suppliers", icon: "fa fa-user" },
{ id: "shippers", text: "Shippers", icon: "fa fa-shopping-cart" }
]
},
{ type: "spacer" },
{
type: "menu", id: "auth", text: email,
items: [
{ text: "Logout", id: "logout", icon: "fa fa-sign-out" }
]
}
],
onClick(event) {
switch (event.target) {
case "sales:customers": {
window.open("<?= base ?>?url=/form/customers", "_top");
break;
}
case "sales:orders": {
window.open("<?= base ?>?url=/form/orders", "_top");
break;
}
case "operations:employees": {
window.open("<?= base ?>?url=/form/employees", "_top");
break;
}
case "operations:products": {
window.open("<?= base ?>?url=/form/products", "_top");
break;
}
case "operations:categories": {
window.open("<?= base ?>?url=/form/categories", "_top");
break;
}
case "operations:suppliers": {
window.open("<?= base ?>?url=/form/suppliers", "_top");
break;
}
case "operations:shippers": {
window.open("<?= base ?>?url=/form/shippers", "_top");
break;
}
case "auth:logout": {
ls.remove("sid");
ls.remove("email");
window.open("<?= base ?>?url=/form/login&callback=<?!= url ?>", "_top");
break;
}
}
}
});
master = new w2grid({
name: "master",
box: "#master",
header: "Orders",
show: {
header: true,
footer: true,
toolbar: true,
toolbarAdd: true,
toolbarEdit: true
},
columns: [
{ field: "recid", hidden: true },
{ field: "OrderID", text: "Order ID", size: "10%", sortable: true },
{ field: "CustomerID", text: "Customer", size: "15%", sortable: true },
{ field: "EmployeeID", text: "Employee", size: "15%", sortable: true },
{ field: "OrderDate", text: "Order Date", render: "datetime", size: "10%", sortable: true },
{ field: "ShippedDate", text: "Shipped Date", render: "date", size: "10%", sortable: true },
{ field: "ShipViaID", text: "Ship Via", size: "15%", sortable: true },
{ field: "ShipCountry", text: "Ship Country", size: "15%", sortable: true }
],
searches: [
{ type: "text", field: "OrderID", label: "Order ID" },
{ type: "text", field: "CustomerID", label: "Customer" },
{ type: "text", field: "EmployeeID", label: "Employee" },
{ type: "text", field: "OrderDate", label: "Order Date" },
{ type: "text", field: "ShippedDate", label: "Shipped Date" },
{ type: "text", field: "ShipViaID", label: "Ship Via" },
{ type: "text", field: "ShipCountry", label: "Country" }
],
onClick(event) {
let record = this.get(event.detail.recid);
if (record) {
detail.clear();
let orders = record.Orders;
let r = 0;
orders.forEach((o) => {
o.ProductID = o.Product["ProductName"];
o.recid = `${event.detail.recid}-${++r}`;
});
let summary = [{
w2ui: { summary: true },
recid: "S-1",
Discount: _.sumBy(orders, (o) => parseFloat(o.Discount)),
Subtotal: _.sumBy(orders, (o) => parseFloat(o.Subtotal)),
}];
detail.add(orders.concat(summary));
}
},
onAdd: function (event) {
window.open("<?= base ?>?url=/form/crud-order", "_blank");
},
onEdit: function (event) {
let recid = event.detail.recid;
recid && window.open("<?= base ?>?url=/form/crud-order/" + recid, "_blank");
},
});
detail = new w2grid({
name: "detail",
box: "#detail",
header: "Detail Order",
show: { header: true },
name: "detail",
columns: [
{ field: "recid", hidden: true },
{ field: "ProductID", text: "Product", size: "30%", sortable: true },
{ field: "Supplier", text: "Supplier", size: "15%", sortable: true },
{ field: "Category", text: "Category", size: "15%", sortable: true },
{ field: "Quantity", text: "Quantity", size: "10%", render: "int" },
{ field: "UnitPrice", text: "Unit Price", size: "10%", render: "money" },
{ field: "Discount", text: "Discount", size: "10%", render: "money" },
{ field: "Subtotal", text: "Subtotal", size: "10%", render: "money" },
]
})
if (sid) {
google.script.run
.withSuccessHandler(onCheckSidSuccess)
.withFailureHandler(onInvalidSid)
.checkSid(sid);
}
else {
onInvalidSid();
}
});
function onInvalidSid(err) {
if (err) {
notifier.warning(err.message);
}
else {
let url = "<?!= base ?>?url=/form/login&callback=<?!= url ?>";
notifier.modal(`<b>You need to login to access this page.</b><br><a href="${url}">👉 <b>Login</b></a>`)
}
}
function onCheckSidSuccess(valid) {
if (valid) {
google.script.run.withSuccessHandler((ds) => {
$ds = ds;
google.script.run.withSuccessHandler((parents => {
master.clear();
let recs = [];
parents.forEach((parent) => {
let obj = JSON.parse(parent.Document);
obj.recid = parent.Id;
obj.CustomerID = obj.Customer["Company Name"];
obj.EmployeeID = obj.Employee["LastName"] + " " + obj.Employee["FirstName"];
obj.ShipViaID = obj.ShipVia["CompanyName"];
recs.push(obj);
});
master.add(recs);
}))
.runSql("Default", "where G = 0 and H = 'Orders' options no_format");
})
.loadDatasources("<?!= datasource ?>");
} else {
onInvalidSid();
}
}
</script>
</body>
</html>
Giải thích Code¶
Form List Orders sử dụng cácthư viện sau:
- jQuery để khởi tạo grid sau khi page load xong.
- 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.
- Notyf hiển thị thông báo.
- w2ui cung cấp các thành phần UI cơ bản và nâng cao, ví dụ: toolbar và grid, dialog...
- 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.
- Trong page này gọi đến
- 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
- Hàm backend loadDatasources để tải các dữ liệu tham chiếu phía client, ví dụ danh sách Products
- 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
- 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
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
let obj = JSON.parse(parent.Document);
Demo¶
Updated by Lê Sĩ Quý 8 months ago · 1 revisions