@firedrill-tools/netsuite 0.1.2
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/LICENSE +201 -0
- package/README.md +214 -0
- package/firedrill/agent.target.json +16 -0
- package/firedrill/baseline.scenario.json +6905 -0
- package/firedrill/concurrency-limited.scenario.json +11 -0
- package/firedrill/conformance.suite.json +21 -0
- package/firedrill/ns-analytics.drill.json +73 -0
- package/firedrill/ns-concurrency.drill.json +53 -0
- package/firedrill/ns-default-identity.drill.json +43 -0
- package/firedrill/ns-denied.drill.json +38 -0
- package/firedrill/ns-insufficient-permission.drill.json +53 -0
- package/firedrill/ns-invalid-login.drill.json +53 -0
- package/firedrill/ns-order-to-cash.drill.json +93 -0
- package/firedrill/ns-record-locked.drill.json +53 -0
- package/firedrill/ns-records.drill.json +68 -0
- package/firedrill/ns-role-ar-clerk.drill.json +58 -0
- package/firedrill/ns-role-sales-rep.drill.json +58 -0
- package/firedrill/ns-subsidiary-scope.drill.json +58 -0
- package/firedrill/ns-tight-limits.drill.json +58 -0
- package/firedrill/ns-transform-lost.drill.json +63 -0
- package/firedrill/ns-write-outage.drill.json +63 -0
- package/firedrill/record-locked.scenario.json +11 -0
- package/firedrill/tight-limits.scenario.json +17 -0
- package/firedrill/tools/netsuite/app/assets/ATTRIBUTION.md +37 -0
- package/firedrill/tools/netsuite/app/assets/fonts/LICENSE.txt +92 -0
- package/firedrill/tools/netsuite/app/assets/oracle-netsuite.svg +1 -0
- package/firedrill/tools/netsuite/app/assets/oracle.svg +1 -0
- package/firedrill/tools/netsuite/app/site/app.js +182 -0
- package/firedrill/tools/netsuite/app/site/assets/fonts/open-sans-latin-ext.woff2 +0 -0
- package/firedrill/tools/netsuite/app/site/assets/fonts/open-sans-latin.woff2 +0 -0
- package/firedrill/tools/netsuite/app/site/assets/oracle-netsuite.svg +1 -0
- package/firedrill/tools/netsuite/app/site/assets/oracle.svg +1 -0
- package/firedrill/tools/netsuite/app/site/icons.js +59 -0
- package/firedrill/tools/netsuite/app/site/index.html +68 -0
- package/firedrill/tools/netsuite/app/site/menus.js +111 -0
- package/firedrill/tools/netsuite/app/site/overlay.js +121 -0
- package/firedrill/tools/netsuite/app/site/store.js +95 -0
- package/firedrill/tools/netsuite/app/site/styles/base.css +131 -0
- package/firedrill/tools/netsuite/app/site/styles/chrome.css +183 -0
- package/firedrill/tools/netsuite/app/site/styles/pages.css +134 -0
- package/firedrill/tools/netsuite/app/site/styles/tables.css +92 -0
- package/firedrill/tools/netsuite/app/site/ui.js +119 -0
- package/firedrill/tools/netsuite/app/site/views/common.js +134 -0
- package/firedrill/tools/netsuite/app/site/views/customer-form.js +152 -0
- package/firedrill/tools/netsuite/app/site/views/customer.js +147 -0
- package/firedrill/tools/netsuite/app/site/views/customers.js +111 -0
- package/firedrill/tools/netsuite/app/site/views/home-chrome.js +75 -0
- package/firedrill/tools/netsuite/app/site/views/home-data.js +153 -0
- package/firedrill/tools/netsuite/app/site/views/home.js +162 -0
- package/firedrill/tools/netsuite/app/site/views/invoice-new.js +131 -0
- package/firedrill/tools/netsuite/app/site/views/invoice.js +232 -0
- package/firedrill/tools/netsuite/app/site/views/invoices.js +99 -0
- package/firedrill/tools/netsuite/app/site/views/items.js +95 -0
- package/firedrill/tools/netsuite/app/site/views/order-new.js +154 -0
- package/firedrill/tools/netsuite/app/site/views/order.js +214 -0
- package/firedrill/tools/netsuite/app/site/views/orders.js +93 -0
- package/firedrill/tools/netsuite/app/site/views/payments.js +65 -0
- package/firedrill/tools/netsuite/app/site/views/pickers.js +117 -0
- package/firedrill/tools/netsuite/app/site/views/subsidiaries.js +40 -0
- package/firedrill/tools/netsuite/app/site/views/suiteql.js +81 -0
- package/firedrill/tools/netsuite/behavior.mjs +13 -0
- package/firedrill/tools/netsuite/lib/body.mjs +185 -0
- package/firedrill/tools/netsuite/lib/collections.mjs +202 -0
- package/firedrill/tools/netsuite/lib/errors.mjs +91 -0
- package/firedrill/tools/netsuite/lib/handlers-meta.mjs +100 -0
- package/firedrill/tools/netsuite/lib/handlers-read.mjs +141 -0
- package/firedrill/tools/netsuite/lib/handlers-session.mjs +52 -0
- package/firedrill/tools/netsuite/lib/handlers-write.mjs +6 -0
- package/firedrill/tools/netsuite/lib/metadata.mjs +148 -0
- package/firedrill/tools/netsuite/lib/primitives.mjs +105 -0
- package/firedrill/tools/netsuite/lib/project-tran.mjs +153 -0
- package/firedrill/tools/netsuite/lib/project.mjs +178 -0
- package/firedrill/tools/netsuite/lib/q.mjs +280 -0
- package/firedrill/tools/netsuite/lib/session.mjs +188 -0
- package/firedrill/tools/netsuite/lib/suiteql-parse.mjs +382 -0
- package/firedrill/tools/netsuite/lib/suiteql-run.mjs +270 -0
- package/firedrill/tools/netsuite/lib/suiteql-select.mjs +118 -0
- package/firedrill/tools/netsuite/lib/suiteql-tables.mjs +162 -0
- package/firedrill/tools/netsuite/lib/tran.mjs +133 -0
- package/firedrill/tools/netsuite/lib/wire-decode.mjs +107 -0
- package/firedrill/tools/netsuite/lib/wire.mjs +228 -0
- package/firedrill/tools/netsuite/lib/write-customer.mjs +181 -0
- package/firedrill/tools/netsuite/lib/write-invoice.mjs +319 -0
- package/firedrill/tools/netsuite/lib/write-order.mjs +269 -0
- package/firedrill/tools/netsuite/netsuite.tool.json +4888 -0
- package/firedrill/transform-response-lost.scenario.json +11 -0
- package/firedrill/world.json +7711 -0
- package/firedrill/write-outage.scenario.json +11 -0
- package/firedrill.json +5 -0
- package/package.json +52 -0
- package/starter.json +6904 -0
- package/test/conformance.mjs +701 -0
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
// Monochrome 16-20px line glyphs drawn for this package in the plain, square-ended style of the NetSuite
|
|
2
|
+
// application bar and list toolbars. No third-party icon font is embedded.
|
|
3
|
+
const NS = "http://www.w3.org/2000/svg";
|
|
4
|
+
const P = {
|
|
5
|
+
menu: ["M3 6h18", "M3 12h18", "M3 18h18"],
|
|
6
|
+
search: ["M10.5 17a6.5 6.5 0 1 0 0-13 6.5 6.5 0 0 0 0 13z", "M15.4 15.4 20 20"],
|
|
7
|
+
clock: ["M12 21a9 9 0 1 0 0-18 9 9 0 0 0 0 18z", "M12 7v5.2l3.4 2"],
|
|
8
|
+
plus: ["M12 5v14", "M5 12h14"],
|
|
9
|
+
star: ["M12 4l2.4 5 5.6.8-4 3.9 1 5.5-5-2.7-5 2.7 1-5.5-4-3.9 5.6-.8z"],
|
|
10
|
+
help: ["M12 21a9 9 0 1 0 0-18 9 9 0 0 0 0 18z", "M9.6 9.4a2.5 2.5 0 1 1 3.5 2.3c-.7.3-1.1.9-1.1 1.7", "M12 17h.01"],
|
|
11
|
+
chevron: ["M6 9.5l6 6 6-6"],
|
|
12
|
+
chevronRight: ["M9.5 6l6 6-6 6"],
|
|
13
|
+
chevronLeft: ["M14.5 6l-6 6 6 6"],
|
|
14
|
+
close: ["M6 6l12 12", "M18 6L6 18"],
|
|
15
|
+
check: ["M5 12.5l4.5 4.5L19 7"],
|
|
16
|
+
info: ["M12 21a9 9 0 1 0 0-18 9 9 0 0 0 0 18z", "M12 11v5.5", "M12 8h.01"],
|
|
17
|
+
alert: ["M12 3.5 21.5 20h-19z", "M12 10v4.2", "M12 17.3h.01"],
|
|
18
|
+
lock: ["M5.5 11h13v9h-13z", "M8.5 11V8a3.5 3.5 0 0 1 7 0v3"],
|
|
19
|
+
edit: ["M4 20h4L19 9l-4-4L4 16z", "M13.5 6.5l4 4"],
|
|
20
|
+
trash: ["M5 7h14", "M10 7V4.5h4V7", "M7 7l1 13h8l1-13"],
|
|
21
|
+
print: ["M7 9V4h10v5", "M6 18H4v-8h16v8h-2", "M7 14h10v6H7z"],
|
|
22
|
+
mail: ["M4 6h16v12H4z", "M4 7l8 6 8-6"],
|
|
23
|
+
refresh: ["M20 12a8 8 0 1 1-2.6-5.9", "M20 4v5h-5"],
|
|
24
|
+
back: ["M20 12H5", "M11 6l-6 6 6 6"],
|
|
25
|
+
money: ["M3 6.5h18v11H3z", "M12 15.5a3.5 3.5 0 1 0 0-7 3.5 3.5 0 0 0 0 7z", "M6.5 10v4", "M17.5 10v4"],
|
|
26
|
+
doc: ["M6 3h8l4 4v14H6z", "M14 3v5h4", "M9 13h6", "M9 17h6"],
|
|
27
|
+
cart: ["M3 5h2.2l2.3 10.5h9.6L19 8H6", "M9.5 20h.01", "M16.5 20h.01"],
|
|
28
|
+
box: ["M3.5 7.5 12 3l8.5 4.5v9L12 21l-8.5-4.5z", "M3.5 7.5 12 12l8.5-4.5", "M12 12v9"],
|
|
29
|
+
people: ["M9 12a4 4 0 1 0 0-8 4 4 0 0 0 0 8z", "M2 20c.8-3.6 3.5-6 7-6s6.2 2.4 7 6", "M16 5.2a3.6 3.6 0 0 1 0 6.9", "M17.5 14.4c2.3.7 3.9 2.7 4.5 5.6"],
|
|
30
|
+
building: ["M5 21V4h9v17", "M14 10h5v11", "M8 8h3", "M8 12h3", "M8 16h3", "M17 14h.01", "M17 18h.01"],
|
|
31
|
+
table: ["M3.5 5h17v14h-17z", "M3.5 9.5h17", "M9 9.5V19", "M14.5 9.5V19"],
|
|
32
|
+
};
|
|
33
|
+
export function icon(name, size = 18) {
|
|
34
|
+
const svg = document.createElementNS(NS, "svg");
|
|
35
|
+
svg.setAttribute("viewBox", "0 0 24 24");
|
|
36
|
+
svg.setAttribute("width", String(size));
|
|
37
|
+
svg.setAttribute("height", String(size));
|
|
38
|
+
svg.setAttribute("aria-hidden", "true");
|
|
39
|
+
svg.setAttribute("focusable", "false");
|
|
40
|
+
svg.setAttribute("fill", "none");
|
|
41
|
+
svg.setAttribute("stroke", "currentColor");
|
|
42
|
+
svg.setAttribute("stroke-width", "1.7");
|
|
43
|
+
svg.setAttribute("stroke-linecap", "square");
|
|
44
|
+
svg.setAttribute("stroke-linejoin", "miter");
|
|
45
|
+
svg.classList.add("ic");
|
|
46
|
+
for (const d of P[name] ?? P.info) {
|
|
47
|
+
const path = document.createElementNS(NS, "path");
|
|
48
|
+
path.setAttribute("d", d);
|
|
49
|
+
svg.append(path);
|
|
50
|
+
}
|
|
51
|
+
return svg;
|
|
52
|
+
}
|
|
53
|
+
/** Fill every [data-icon] placeholder in the static shell markup. */
|
|
54
|
+
export function hydrateIcons(root = document) {
|
|
55
|
+
for (const host of root.querySelectorAll("[data-icon]")) {
|
|
56
|
+
host.prepend(icon(host.getAttribute("data-icon"), Number(host.getAttribute("data-icon-size") ?? 18)));
|
|
57
|
+
host.removeAttribute("data-icon");
|
|
58
|
+
}
|
|
59
|
+
}
|
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
<!doctype html>
|
|
2
|
+
<html lang="en">
|
|
3
|
+
<head>
|
|
4
|
+
<meta charset="UTF-8" />
|
|
5
|
+
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
|
6
|
+
<title>NetSuite (synthetic) · Firedrill</title>
|
|
7
|
+
<link rel="icon" href="./assets/oracle.svg" />
|
|
8
|
+
<link rel="stylesheet" href="./styles/base.css" />
|
|
9
|
+
<link rel="stylesheet" href="./styles/chrome.css" />
|
|
10
|
+
<link rel="stylesheet" href="./styles/tables.css" />
|
|
11
|
+
<link rel="stylesheet" href="./styles/pages.css" />
|
|
12
|
+
<script type="module" src="./app.js"></script>
|
|
13
|
+
</head>
|
|
14
|
+
<body>
|
|
15
|
+
<a class="skip" href="#main">Skip to main content</a>
|
|
16
|
+
|
|
17
|
+
<header class="appbar" id="appbar">
|
|
18
|
+
<button type="button" class="nav-toggle" id="menu-toggle" aria-expanded="false" aria-label="Show menu"></button>
|
|
19
|
+
<a class="brand" href="#/home" aria-label="Oracle NetSuite home">
|
|
20
|
+
<img src="./assets/oracle-netsuite.svg" alt="Oracle NetSuite" />
|
|
21
|
+
</a>
|
|
22
|
+
|
|
23
|
+
<div class="appbar-right">
|
|
24
|
+
<form class="global-search" id="global-search" role="search">
|
|
25
|
+
<label class="visually-hidden" for="global-search-input">Search</label>
|
|
26
|
+
<input type="search" id="global-search-input" name="q" placeholder="Search" autocomplete="off" />
|
|
27
|
+
<button type="submit" class="gs-go" aria-label="Search"></button>
|
|
28
|
+
</form>
|
|
29
|
+
<button type="button" class="bar-btn" id="recent-btn" aria-haspopup="true" aria-expanded="false" title="Recent Records"><span class="bar-label">Recent Records</span></button>
|
|
30
|
+
<button type="button" class="bar-btn" id="create-btn" aria-haspopup="true" aria-expanded="false" title="Create New"><span class="bar-label">Create New</span></button>
|
|
31
|
+
<button type="button" class="bar-btn" id="shortcuts-btn" aria-haspopup="true" aria-expanded="false" title="Shortcuts"><span class="bar-label">Shortcuts</span></button>
|
|
32
|
+
<button type="button" class="bar-btn" id="help-btn" title="Help"><span class="bar-label">Help</span></button>
|
|
33
|
+
<button type="button" class="user-btn" id="user-btn" aria-haspopup="true" aria-expanded="false">
|
|
34
|
+
<span class="avatar" id="user-initials" aria-hidden="true"></span>
|
|
35
|
+
<span class="user-lines">
|
|
36
|
+
<span class="user-name" id="user-name">Connecting…</span>
|
|
37
|
+
<span class="user-role" id="user-role"></span>
|
|
38
|
+
</span>
|
|
39
|
+
</button>
|
|
40
|
+
</div>
|
|
41
|
+
</header>
|
|
42
|
+
|
|
43
|
+
<nav class="menubar" id="menubar" aria-label="Global navigation"></nav>
|
|
44
|
+
|
|
45
|
+
<div class="account-strip">
|
|
46
|
+
<span class="acct-name" id="acct-name"></span>
|
|
47
|
+
<span class="acct-sep" aria-hidden="true">·</span>
|
|
48
|
+
<button type="button" class="acct-role" id="role-btn" aria-haspopup="true" aria-expanded="false"><span id="role-label">Role</span></button>
|
|
49
|
+
<span class="acct-chip" id="acct-chip" title="This account is simulated locally by a Firedrill Tool">Firedrill synthetic account</span>
|
|
50
|
+
<span class="acct-date" id="acct-date"></span>
|
|
51
|
+
</div>
|
|
52
|
+
|
|
53
|
+
<main id="main" class="page" tabindex="-1"></main>
|
|
54
|
+
|
|
55
|
+
<footer class="ns-footer">
|
|
56
|
+
<span id="footer-account"></span>
|
|
57
|
+
<span class="foot-links">
|
|
58
|
+
<button type="button" class="foot-link" data-not-simulated="Release Preview">Release Preview</button>
|
|
59
|
+
<button type="button" class="foot-link" data-not-simulated="Privacy Policy">Privacy Policy</button>
|
|
60
|
+
<button type="button" class="foot-link" data-not-simulated="Terms of Service">Terms of Service</button>
|
|
61
|
+
</span>
|
|
62
|
+
<span class="foot-note">Synthetic NetSuite account served by a Firedrill Tool. No Oracle service is contacted.</span>
|
|
63
|
+
</footer>
|
|
64
|
+
|
|
65
|
+
<div id="toast" class="toast" role="status" aria-live="polite" hidden></div>
|
|
66
|
+
<div id="overlay-root"></div>
|
|
67
|
+
</body>
|
|
68
|
+
</html>
|
|
@@ -0,0 +1,111 @@
|
|
|
1
|
+
// The global menu bar and Create New menu, in NetSuite's own order. Entries without `run` are part of the real
|
|
2
|
+
// product but outside this Tool's scope: they stay visible and open the "Not simulated by this Tool" panel.
|
|
3
|
+
import { go } from "./store.js";
|
|
4
|
+
|
|
5
|
+
export const MENUS = {
|
|
6
|
+
Activities: [
|
|
7
|
+
{ heading: "Scheduling" },
|
|
8
|
+
{ label: "Calendar" }, { label: "Tasks" }, { label: "Phone Calls" }, { label: "Events" },
|
|
9
|
+
{ divider: true },
|
|
10
|
+
{ label: "Mass Update Task Records" },
|
|
11
|
+
],
|
|
12
|
+
Payments: [
|
|
13
|
+
{ heading: "Customers" },
|
|
14
|
+
{ label: "Accept Customer Payments", run: () => go("#/payments") },
|
|
15
|
+
{ label: "Customer Payments", run: () => go("#/payments") },
|
|
16
|
+
{ label: "Customer Deposits" }, { label: "Customer Refunds" },
|
|
17
|
+
{ divider: true },
|
|
18
|
+
{ heading: "Vendors" },
|
|
19
|
+
{ label: "Pay Single Vendor" }, { label: "Pay Bills" },
|
|
20
|
+
],
|
|
21
|
+
Transactions: [
|
|
22
|
+
{ heading: "Sales" },
|
|
23
|
+
{ label: "Enter Sales Orders", run: () => go("#/orders/new") },
|
|
24
|
+
{ label: "Sales Orders List", run: () => go("#/orders") },
|
|
25
|
+
{ label: "Create Invoices", run: () => go("#/invoices/new") },
|
|
26
|
+
{ label: "Invoices List", run: () => go("#/invoices") },
|
|
27
|
+
{ label: "Enter Estimates" }, { label: "Enter Cash Sales" }, { label: "Issue Credit Memos" },
|
|
28
|
+
{ divider: true },
|
|
29
|
+
{ heading: "Purchases / Inventory" },
|
|
30
|
+
{ label: "Enter Purchase Orders" }, { label: "Enter Bills" }, { label: "Fulfill Orders" }, { label: "Receive Orders" },
|
|
31
|
+
{ divider: true },
|
|
32
|
+
{ heading: "Financial" },
|
|
33
|
+
{ label: "Make Journal Entries" }, { label: "Reconcile Account Statement" }, { label: "Manage Accounting Periods" },
|
|
34
|
+
],
|
|
35
|
+
Lists: [
|
|
36
|
+
{ heading: "Relationships" },
|
|
37
|
+
{ label: "Customers", run: () => go("#/customers") },
|
|
38
|
+
{ label: "Contacts" }, { label: "Vendors" }, { label: "Partners" }, { label: "Employees" },
|
|
39
|
+
{ divider: true },
|
|
40
|
+
{ heading: "Accounting" },
|
|
41
|
+
{ label: "Items", run: () => go("#/items") },
|
|
42
|
+
{ label: "Chart of Accounts" }, { label: "Currencies" }, { label: "Tax Schedules" },
|
|
43
|
+
{ divider: true },
|
|
44
|
+
{ heading: "Marketing" },
|
|
45
|
+
{ label: "Campaigns" }, { label: "Promotions" },
|
|
46
|
+
],
|
|
47
|
+
Reports: [
|
|
48
|
+
{ heading: "Financial" },
|
|
49
|
+
{ label: "Income Statement" }, { label: "Balance Sheet" }, { label: "Trial Balance" },
|
|
50
|
+
{ divider: true },
|
|
51
|
+
{ heading: "Customers/Receivables" },
|
|
52
|
+
{ label: "A/R Aging Summary" }, { label: "Customer Balance" }, { label: "Sales by Customer" },
|
|
53
|
+
{ divider: true },
|
|
54
|
+
{ label: "Saved Reports" }, { label: "New Report" },
|
|
55
|
+
],
|
|
56
|
+
Analytics: [
|
|
57
|
+
{ heading: "SuiteAnalytics" },
|
|
58
|
+
{ label: "SuiteQL Query Tool", run: () => go("#/suiteql") },
|
|
59
|
+
{ label: "Workbooks" }, { label: "Datasets" }, { label: "Analytics Home" },
|
|
60
|
+
],
|
|
61
|
+
Documents: [
|
|
62
|
+
{ heading: "Files" },
|
|
63
|
+
{ label: "File Cabinet" }, { label: "Images" }, { label: "Templates" },
|
|
64
|
+
{ divider: true },
|
|
65
|
+
{ label: "Mail Merge" },
|
|
66
|
+
],
|
|
67
|
+
Setup: [
|
|
68
|
+
{ heading: "Company" },
|
|
69
|
+
{ label: "Subsidiaries", run: () => go("#/subsidiaries") },
|
|
70
|
+
{ label: "Company Information" }, { label: "Enable Features" }, { label: "General Preferences" },
|
|
71
|
+
{ divider: true },
|
|
72
|
+
{ heading: "Users/Roles" },
|
|
73
|
+
{ label: "Manage Users" }, { label: "Manage Roles" }, { label: "Login Audit Trail" },
|
|
74
|
+
{ divider: true },
|
|
75
|
+
{ heading: "Accounting" },
|
|
76
|
+
{ label: "Accounting Preferences" }, { label: "Accounting Periods" }, { label: "Taxes" },
|
|
77
|
+
],
|
|
78
|
+
Customization: [
|
|
79
|
+
{ heading: "Lists, Records, & Fields" },
|
|
80
|
+
{ label: "Record Types" }, { label: "Entity Fields" }, { label: "Transaction Body Fields" },
|
|
81
|
+
{ divider: true },
|
|
82
|
+
{ heading: "Scripting" },
|
|
83
|
+
{ label: "Scripts" }, { label: "Script Deployments" }, { label: "SuiteBundler" },
|
|
84
|
+
{ divider: true },
|
|
85
|
+
{ label: "Forms" }, { label: "Centers and Tabs" },
|
|
86
|
+
],
|
|
87
|
+
Commerce: [
|
|
88
|
+
{ heading: "Websites" },
|
|
89
|
+
{ label: "Website List" }, { label: "Site Management Tools" }, { label: "Web Store Setup" },
|
|
90
|
+
{ divider: true },
|
|
91
|
+
{ label: "Marketing Campaigns" },
|
|
92
|
+
],
|
|
93
|
+
Support: [
|
|
94
|
+
{ heading: "Customer Service" },
|
|
95
|
+
{ label: "Cases" }, { label: "Issues" }, { label: "Solutions" },
|
|
96
|
+
{ divider: true },
|
|
97
|
+
{ label: "SuiteAnswers" }, { label: "Go to SuiteAnswers" },
|
|
98
|
+
],
|
|
99
|
+
};
|
|
100
|
+
|
|
101
|
+
export const CREATE_NEW = [
|
|
102
|
+
{ heading: "Create New" },
|
|
103
|
+
{ label: "Sales Order", run: () => go("#/orders/new") },
|
|
104
|
+
{ label: "Invoice", run: () => go("#/invoices/new") },
|
|
105
|
+
{ label: "Customer", run: () => go("#/customers?new=1") },
|
|
106
|
+
{ label: "Customer Payment", run: () => go("#/payments") },
|
|
107
|
+
{ divider: true },
|
|
108
|
+
{ label: "Estimate" }, { label: "Cash Sale" }, { label: "Credit Memo" },
|
|
109
|
+
{ label: "Purchase Order" }, { label: "Vendor Bill" }, { label: "Journal Entry" },
|
|
110
|
+
{ label: "Task" }, { label: "Phone Call" }, { label: "Event" },
|
|
111
|
+
];
|
|
@@ -0,0 +1,121 @@
|
|
|
1
|
+
// Dialogs, confirmations, dropdown menus, the "not simulated" panel and world-revision refresh.
|
|
2
|
+
import { getContext } from "/_firedrill/client.js";
|
|
3
|
+
import { $, el, icon, isPending } from "./ui.js";
|
|
4
|
+
|
|
5
|
+
let dialogSeq = 0;
|
|
6
|
+
export function dialog({ title, body, actions = [], wide = false, onClose }) {
|
|
7
|
+
const root = $("#overlay-root");
|
|
8
|
+
const previous = document.activeElement;
|
|
9
|
+
const close = () => { wrap.remove(); document.removeEventListener("keydown", onKey); onClose?.(); previous?.focus?.(); };
|
|
10
|
+
const onKey = (event) => { if (event.key === "Escape") close(); };
|
|
11
|
+
const titleId = `dlg-${(dialogSeq += 1)}`;
|
|
12
|
+
const box = el("div", { class: `dialog${wide ? " wide" : ""}`, role: "dialog", "aria-modal": "true", "aria-labelledby": titleId }, [
|
|
13
|
+
el("div", { class: "dialog-head" }, [
|
|
14
|
+
el("h2", { id: titleId, text: title }),
|
|
15
|
+
el("button", { type: "button", class: "icon-btn", "aria-label": "Close", onclick: close }, icon("close", 16)),
|
|
16
|
+
]),
|
|
17
|
+
el("div", { class: "dialog-body" }, body),
|
|
18
|
+
actions.length
|
|
19
|
+
? el("div", { class: "dialog-foot" }, actions.map((a) =>
|
|
20
|
+
el("button", { type: "button", class: `btn ${a.kind ?? ""}`, text: a.label, onclick: () => a.run(close) })))
|
|
21
|
+
: null,
|
|
22
|
+
]);
|
|
23
|
+
const wrap = el("div", { class: "scrim", onclick: (event) => { if (event.target === wrap) close(); } }, box);
|
|
24
|
+
root.append(wrap);
|
|
25
|
+
document.addEventListener("keydown", onKey);
|
|
26
|
+
(box.querySelector("input, select, textarea") ?? box.querySelector(".dialog-foot .btn") ?? box.querySelector("button"))?.focus();
|
|
27
|
+
return { close, box };
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
export function confirmDialog(title, message, confirmLabel, kind = "primary") {
|
|
31
|
+
return new Promise((resolve) => {
|
|
32
|
+
let answered = false;
|
|
33
|
+
dialog({
|
|
34
|
+
title,
|
|
35
|
+
body: el("p", { text: message }),
|
|
36
|
+
onClose: () => { if (!answered) resolve(false); },
|
|
37
|
+
actions: [
|
|
38
|
+
{ label: "Cancel", run: (close) => { answered = true; resolve(false); close(); } },
|
|
39
|
+
{ label: confirmLabel, kind, run: (close) => { answered = true; resolve(true); close(); } },
|
|
40
|
+
],
|
|
41
|
+
});
|
|
42
|
+
});
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
const SCOPE = "This Tool simulates customers, sales orders, invoices, customer payments, items, subsidiaries and SuiteQL queries.";
|
|
46
|
+
export function notSimulated(feature) {
|
|
47
|
+
dialog({
|
|
48
|
+
title: feature,
|
|
49
|
+
body: [
|
|
50
|
+
el("p", { text: `${feature} is part of NetSuite but is not simulated by this Firedrill Tool.` }),
|
|
51
|
+
el("p", { class: "muted", text: SCOPE }),
|
|
52
|
+
],
|
|
53
|
+
actions: [{ label: "OK", kind: "primary", run: (close) => close() }],
|
|
54
|
+
});
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
let closeCurrent;
|
|
58
|
+
export function closeMenu() { closeCurrent?.(); closeCurrent = undefined; }
|
|
59
|
+
/** items: [{ label, run?, disabled?, hint?, heading?, divider? }] — entries without run open the not-simulated panel. */
|
|
60
|
+
export function openMenu(anchor, items, { align = "left" } = {}) {
|
|
61
|
+
const reopening = anchor.classList.contains("open");
|
|
62
|
+
closeMenu();
|
|
63
|
+
if (reopening) return null;
|
|
64
|
+
const pop = el("div", { class: "menu-pop", role: "menu" });
|
|
65
|
+
for (const item of items) {
|
|
66
|
+
if (item.heading) { pop.append(el("div", { class: "menu-head", text: item.heading })); continue; }
|
|
67
|
+
if (item.divider) { pop.append(el("div", { class: "menu-divider", role: "separator" })); continue; }
|
|
68
|
+
const button = el("button", {
|
|
69
|
+
type: "button",
|
|
70
|
+
class: `mi${item.run ? "" : " mi-ns"}`,
|
|
71
|
+
role: "menuitem",
|
|
72
|
+
disabled: item.disabled,
|
|
73
|
+
title: item.run ? item.hint : "Not simulated by this Tool",
|
|
74
|
+
}, [el("span", { text: item.label }), item.hint ? el("span", { class: "mi-hint", text: item.hint }) : null]);
|
|
75
|
+
button.addEventListener("click", () => { closeMenu(); if (item.run) item.run(); else notSimulated(item.label); });
|
|
76
|
+
pop.append(button);
|
|
77
|
+
}
|
|
78
|
+
document.body.append(pop);
|
|
79
|
+
const rect = anchor.getBoundingClientRect();
|
|
80
|
+
pop.style.top = `${Math.max(4, Math.min(rect.bottom + 2, window.innerHeight - pop.offsetHeight - 8))}px`;
|
|
81
|
+
const left = align === "right" ? rect.right - pop.offsetWidth : rect.left;
|
|
82
|
+
pop.style.left = `${Math.max(8, Math.min(left, window.innerWidth - pop.offsetWidth - 8))}px`;
|
|
83
|
+
anchor.setAttribute("aria-expanded", "true");
|
|
84
|
+
anchor.classList.add("open");
|
|
85
|
+
const away = (event) => { if (!pop.contains(event.target) && !anchor.contains(event.target)) closeMenu(); };
|
|
86
|
+
const key = (event) => {
|
|
87
|
+
if (event.key === "Escape") { closeMenu(); anchor.focus(); return; }
|
|
88
|
+
if (event.key !== "ArrowDown" && event.key !== "ArrowUp") return;
|
|
89
|
+
const all = [...pop.querySelectorAll(".mi:not(:disabled)")];
|
|
90
|
+
const index = all.indexOf(document.activeElement);
|
|
91
|
+
all[(index + (event.key === "ArrowDown" ? 1 : all.length - 1)) % all.length]?.focus();
|
|
92
|
+
event.preventDefault();
|
|
93
|
+
};
|
|
94
|
+
setTimeout(() => document.addEventListener("click", away), 0);
|
|
95
|
+
document.addEventListener("keydown", key);
|
|
96
|
+
closeCurrent = () => {
|
|
97
|
+
pop.remove();
|
|
98
|
+
anchor.setAttribute("aria-expanded", "false");
|
|
99
|
+
anchor.classList.remove("open");
|
|
100
|
+
document.removeEventListener("click", away);
|
|
101
|
+
document.removeEventListener("keydown", key);
|
|
102
|
+
};
|
|
103
|
+
pop.querySelector(".mi:not(:disabled)")?.focus();
|
|
104
|
+
return pop;
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
/** Poll getContext().revision; refresh when the world moved and the page has no unsaved input or open dialog. */
|
|
108
|
+
export function watchWorld(refresh, mayRefresh = () => true) {
|
|
109
|
+
let revision;
|
|
110
|
+
const check = async () => {
|
|
111
|
+
if (isPending() || document.visibilityState !== "visible") return;
|
|
112
|
+
try {
|
|
113
|
+
const context = await getContext();
|
|
114
|
+
const stamp = JSON.stringify(context.revision);
|
|
115
|
+
if (revision === undefined) { revision = stamp; return; }
|
|
116
|
+
if (stamp !== revision && mayRefresh()) { revision = stamp; await refresh(); }
|
|
117
|
+
} catch { /* the environment closed; leave the page as it is */ }
|
|
118
|
+
};
|
|
119
|
+
setInterval(check, 2500);
|
|
120
|
+
return check;
|
|
121
|
+
}
|
|
@@ -0,0 +1,95 @@
|
|
|
1
|
+
// Shared app state: the signed-in NetSuite session (account, role, world clock), list paging over the
|
|
2
|
+
// record collections, status vocabulary and hash routing.
|
|
3
|
+
import { call, dayNum } from "./ui.js";
|
|
4
|
+
|
|
5
|
+
export const app = {
|
|
6
|
+
pages: Object.create(null),
|
|
7
|
+
session: null,
|
|
8
|
+
sessionError: null,
|
|
9
|
+
today: "",
|
|
10
|
+
dirty: false,
|
|
11
|
+
recent: [],
|
|
12
|
+
};
|
|
13
|
+
|
|
14
|
+
/** session.get carries the account, the role's permission levels and the account server time (world time). */
|
|
15
|
+
export async function loadSession() {
|
|
16
|
+
try {
|
|
17
|
+
const value = await call("session.get", {});
|
|
18
|
+
app.session = value;
|
|
19
|
+
app.today = String(value.accountDate ?? "").slice(0, 10);
|
|
20
|
+
app.sessionError = null;
|
|
21
|
+
} catch (error) {
|
|
22
|
+
app.session = null;
|
|
23
|
+
app.sessionError = error;
|
|
24
|
+
}
|
|
25
|
+
return app.session;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
/** Permission level of the signed-in role, "none" when the session could not be read. */
|
|
29
|
+
export function level(permission) {
|
|
30
|
+
const entry = (app.session?.role?.permissions ?? []).find((p) => p.name === permission);
|
|
31
|
+
return entry ? entry.level : "none";
|
|
32
|
+
}
|
|
33
|
+
const RANK = ["none", "view", "create", "edit", "full"];
|
|
34
|
+
export const can = (permission, minimum = "view") => RANK.indexOf(level(permission)) >= RANK.indexOf(minimum);
|
|
35
|
+
|
|
36
|
+
/** One page of a NetSuite record collection: items carry only links and id, so ids are fetched then expanded. */
|
|
37
|
+
export async function listIds(operationId, { q, limit = 25, offset = 0 } = {}) {
|
|
38
|
+
const args = { limit, offset };
|
|
39
|
+
if (q !== undefined && q !== "") args.q = q;
|
|
40
|
+
const value = await call(operationId, args);
|
|
41
|
+
return {
|
|
42
|
+
ids: (value.items ?? []).map((item) => item.id),
|
|
43
|
+
count: value.count ?? 0,
|
|
44
|
+
offset: value.offset ?? offset,
|
|
45
|
+
hasMore: value.hasMore === true,
|
|
46
|
+
totalResults: value.totalResults ?? 0,
|
|
47
|
+
};
|
|
48
|
+
}
|
|
49
|
+
/** Expand a page of ids into records with the matching singular read. */
|
|
50
|
+
export async function expand(operationId, ids, extra = {}) {
|
|
51
|
+
return Promise.all(ids.map((id) => call(operationId, { recordId: id, ...extra })));
|
|
52
|
+
}
|
|
53
|
+
/** A page of full records for a collection: { rows, page } in one call site. */
|
|
54
|
+
export async function listPage(listOperation, getOperation, options = {}) {
|
|
55
|
+
const page = await listIds(listOperation, options);
|
|
56
|
+
const rows = await expand(getOperation, page.ids, options.expand === true ? { expandSubResources: true } : {});
|
|
57
|
+
return { rows, page };
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
export const PAGE_SIZES = [10, 25, 50];
|
|
61
|
+
|
|
62
|
+
// ---- status vocabulary (refName values projected by the Tool) ----
|
|
63
|
+
export function statusClass(refName) {
|
|
64
|
+
const name = String(refName ?? "");
|
|
65
|
+
if (name === "Paid In Full" || name === "Billed" || name === "Deposited") return "paid";
|
|
66
|
+
if (name === "Pending Approval") return "pending";
|
|
67
|
+
if (name.startsWith("Pending Billing") || name === "Partially Fulfilled" || name === "Pending Fulfillment") return "billing";
|
|
68
|
+
if (name === "Voided") return "void";
|
|
69
|
+
if (name === "Closed") return "closed";
|
|
70
|
+
return "open";
|
|
71
|
+
}
|
|
72
|
+
export const isOverdue = (invoice, today = app.today) =>
|
|
73
|
+
String(invoice?.status?.refName) === "Open"
|
|
74
|
+
&& Number(invoice?.amountRemaining) > 0
|
|
75
|
+
&& !Number.isNaN(dayNum(invoice?.dueDate))
|
|
76
|
+
&& dayNum(invoice.dueDate) < dayNum(today);
|
|
77
|
+
|
|
78
|
+
/** Remember a visited record for the Recent Records menu (in-memory, per app session). */
|
|
79
|
+
export function remember(entry) {
|
|
80
|
+
app.recent = [entry, ...app.recent.filter((item) => item.href !== entry.href)].slice(0, 10);
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
// ---- routing ----
|
|
84
|
+
export function parseRoute(hash = location.hash) {
|
|
85
|
+
const [path, query = ""] = hash.replace(/^#\/?/, "").split("?");
|
|
86
|
+
const parts = path.split("/").filter(Boolean).map((part) => {
|
|
87
|
+
try { return decodeURIComponent(part); } catch { return ""; }
|
|
88
|
+
});
|
|
89
|
+
return { page: parts[0] || "home", id: parts[1], sub: parts[2], params: new URLSearchParams(query) };
|
|
90
|
+
}
|
|
91
|
+
export function go(hash) {
|
|
92
|
+
if (location.hash === hash) window.dispatchEvent(new HashChangeEvent("hashchange"));
|
|
93
|
+
else location.hash = hash;
|
|
94
|
+
}
|
|
95
|
+
export function register(name, render) { app.pages[name] = render; }
|
|
@@ -0,0 +1,131 @@
|
|
|
1
|
+
/* Design tokens, bundled face and the shared form/button vocabulary of the NetSuite application shell. */
|
|
2
|
+
@font-face {
|
|
3
|
+
font-family: "NS Sans";
|
|
4
|
+
src: url("../assets/fonts/open-sans-latin.woff2") format("woff2");
|
|
5
|
+
font-weight: 300 800;
|
|
6
|
+
font-style: normal;
|
|
7
|
+
font-display: swap;
|
|
8
|
+
unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA, U+02DC, U+2000-206F, U+20AC, U+2122, U+2212, U+FEFF, U+FFFD;
|
|
9
|
+
}
|
|
10
|
+
@font-face {
|
|
11
|
+
font-family: "NS Sans";
|
|
12
|
+
src: url("../assets/fonts/open-sans-latin-ext.woff2") format("woff2");
|
|
13
|
+
font-weight: 300 800;
|
|
14
|
+
font-style: normal;
|
|
15
|
+
font-display: swap;
|
|
16
|
+
unicode-range: U+0100-02BA, U+02BD-02C5, U+02C7-02CC, U+1E00-1E9F, U+2020, U+20A0-20AB, U+20AD-20C0, U+2C60-2C7F, U+A720-A7FF;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
:root {
|
|
20
|
+
--bar: #16283c;
|
|
21
|
+
--bar-2: #1d3450;
|
|
22
|
+
--menu: #2c4f7c;
|
|
23
|
+
--menu-hover: #3d6494;
|
|
24
|
+
--ground: #eef0f3;
|
|
25
|
+
--panel: #ffffff;
|
|
26
|
+
--line: #c3cbd6;
|
|
27
|
+
--line-soft: #dde2e9;
|
|
28
|
+
--band: #dfe7f0;
|
|
29
|
+
--band-2: #eef2f7;
|
|
30
|
+
--ink: #1f2933;
|
|
31
|
+
--ink-soft: #55606d;
|
|
32
|
+
--link: #14538e;
|
|
33
|
+
--link-hover: #0b3c6b;
|
|
34
|
+
--red: #b3231f;
|
|
35
|
+
--amber: #a56200;
|
|
36
|
+
--green: #1f7a3d;
|
|
37
|
+
--teal: #0f6d78;
|
|
38
|
+
--grey: #6b7683;
|
|
39
|
+
--focus: #1a73c8;
|
|
40
|
+
--radius: 2px;
|
|
41
|
+
--font: "NS Sans", "Helvetica Neue", Helvetica, Arial, sans-serif;
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
* { box-sizing: border-box; }
|
|
45
|
+
|
|
46
|
+
body {
|
|
47
|
+
margin: 0;
|
|
48
|
+
background: var(--ground);
|
|
49
|
+
color: var(--ink);
|
|
50
|
+
font-family: var(--font);
|
|
51
|
+
font-size: 12px;
|
|
52
|
+
line-height: 1.45;
|
|
53
|
+
min-height: 100vh;
|
|
54
|
+
display: flex;
|
|
55
|
+
flex-direction: column;
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
a { color: var(--link); text-decoration: none; }
|
|
59
|
+
a:hover { color: var(--link-hover); text-decoration: underline; }
|
|
60
|
+
|
|
61
|
+
.visually-hidden {
|
|
62
|
+
position: absolute;
|
|
63
|
+
width: 1px;
|
|
64
|
+
height: 1px;
|
|
65
|
+
padding: 0;
|
|
66
|
+
margin: -1px;
|
|
67
|
+
overflow: hidden;
|
|
68
|
+
clip: rect(0 0 0 0);
|
|
69
|
+
white-space: nowrap;
|
|
70
|
+
}
|
|
71
|
+
.skip { position: absolute; left: -9999px; top: 0; background: #fff; padding: 8px 12px; z-index: 40; }
|
|
72
|
+
.skip:focus { left: 8px; top: 8px; }
|
|
73
|
+
|
|
74
|
+
:focus-visible { outline: 2px solid var(--focus); outline-offset: 1px; }
|
|
75
|
+
|
|
76
|
+
/* ---- buttons: NetSuite's small grey gradient control ---- */
|
|
77
|
+
.btn {
|
|
78
|
+
font: inherit;
|
|
79
|
+
font-size: 11px;
|
|
80
|
+
font-weight: 700;
|
|
81
|
+
color: #1d2b3a;
|
|
82
|
+
background: linear-gradient(#fdfdfd, #e3e7ec);
|
|
83
|
+
border: 1px solid #9fabb9;
|
|
84
|
+
border-radius: var(--radius);
|
|
85
|
+
padding: 3px 11px;
|
|
86
|
+
min-height: 22px;
|
|
87
|
+
cursor: pointer;
|
|
88
|
+
white-space: nowrap;
|
|
89
|
+
}
|
|
90
|
+
.btn:hover { background: linear-gradient(#ffffff, #d5dbe3); }
|
|
91
|
+
.btn:active { background: linear-gradient(#d5dbe3, #e8ecf1); }
|
|
92
|
+
.btn[disabled] { color: #94a0ad; cursor: default; background: #f1f3f5; }
|
|
93
|
+
.btn.primary[disabled], .btn.danger[disabled] { color: #f0f2f4; background: linear-gradient(#a8b4c1, #8e9cab); border-color: #8795a4; }
|
|
94
|
+
.btn.primary { color: #fff; background: linear-gradient(#3f76b4, #28598f); border-color: #1e4770; }
|
|
95
|
+
.btn.primary:hover { background: linear-gradient(#4a83c3, #2f6299); }
|
|
96
|
+
.btn.danger { color: #fff; background: linear-gradient(#c8403c, #a71f1b); border-color: #8c1a16; }
|
|
97
|
+
.btn.link { background: none; border: none; color: var(--link); padding: 0; font-weight: 400; }
|
|
98
|
+
.btn.link:hover { text-decoration: underline; background: none; }
|
|
99
|
+
.btn-row { display: flex; gap: 6px; flex-wrap: wrap; align-items: center; }
|
|
100
|
+
|
|
101
|
+
.icon-btn {
|
|
102
|
+
background: none; border: none; cursor: pointer; color: inherit;
|
|
103
|
+
padding: 2px; display: inline-flex; align-items: center; border-radius: var(--radius);
|
|
104
|
+
}
|
|
105
|
+
.icon-btn:hover { background: rgba(0, 0, 0, 0.07); }
|
|
106
|
+
.ic { vertical-align: middle; }
|
|
107
|
+
|
|
108
|
+
/* ---- fields ---- */
|
|
109
|
+
label.fl { display: block; font-size: 11px; color: #3a4654; margin-bottom: 2px; }
|
|
110
|
+
label.fl .req { color: var(--red); margin-left: 2px; }
|
|
111
|
+
input[type="text"], input[type="search"], input[type="email"], input[type="tel"],
|
|
112
|
+
input[type="number"], input[type="date"], select, textarea {
|
|
113
|
+
font: inherit;
|
|
114
|
+
font-size: 12px;
|
|
115
|
+
color: var(--ink);
|
|
116
|
+
background: #fff;
|
|
117
|
+
border: 1px solid #8f9daa;
|
|
118
|
+
border-radius: var(--radius);
|
|
119
|
+
padding: 2px 5px;
|
|
120
|
+
min-height: 22px;
|
|
121
|
+
width: 100%;
|
|
122
|
+
}
|
|
123
|
+
textarea { min-height: 54px; resize: vertical; }
|
|
124
|
+
input[readonly], input[disabled], select[disabled], textarea[disabled] { background: #eef0f3; color: #626d7a; }
|
|
125
|
+
.field { margin-bottom: 10px; }
|
|
126
|
+
.field.inline { display: flex; align-items: center; gap: 6px; }
|
|
127
|
+
.field.inline input[type="checkbox"] { width: auto; min-height: 0; }
|
|
128
|
+
.field-error { color: var(--red); font-size: 11px; margin-top: 2px; }
|
|
129
|
+
.muted { color: var(--ink-soft); }
|
|
130
|
+
.mono { font-family: "SFMono-Regular", Menlo, Consolas, monospace; font-size: 11px; }
|
|
131
|
+
.num { text-align: right; font-variant-numeric: tabular-nums; }
|