suiteportal 0.3.0 → 0.5.0
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 +97 -0
- package/dist/next.cjs +380 -0
- package/dist/next.cjs.map +1 -0
- package/dist/next.d.cts +189 -0
- package/dist/next.d.ts +189 -0
- package/dist/next.js +338 -0
- package/dist/next.js.map +1 -0
- package/dist/react.cjs +180 -0
- package/dist/react.cjs.map +1 -0
- package/dist/react.d.cts +35 -0
- package/dist/react.d.ts +35 -0
- package/dist/react.js +156 -0
- package/dist/react.js.map +1 -0
- package/package.json +35 -5
package/README.md
ADDED
|
@@ -0,0 +1,97 @@
|
|
|
1
|
+
# suiteportal
|
|
2
|
+
|
|
3
|
+
**The Prisma for NetSuite** — a typed, metadata-driven ORM over NetSuite's SuiteQL and REST APIs.
|
|
4
|
+
|
|
5
|
+
[](https://www.npmjs.com/package/suiteportal)
|
|
6
|
+
[](https://github.com/Suite-Portal/netsuite-orm/blob/main/LICENSE)
|
|
7
|
+
|
|
8
|
+
## Install
|
|
9
|
+
|
|
10
|
+
```bash
|
|
11
|
+
npm install suiteportal
|
|
12
|
+
```
|
|
13
|
+
|
|
14
|
+
## Quick Start
|
|
15
|
+
|
|
16
|
+
```bash
|
|
17
|
+
# Initialize config and .env
|
|
18
|
+
npx suiteportal init
|
|
19
|
+
|
|
20
|
+
# Introspect your NetSuite account schema
|
|
21
|
+
npx suiteportal introspect
|
|
22
|
+
|
|
23
|
+
# Generate typed client
|
|
24
|
+
npx suiteportal generate
|
|
25
|
+
```
|
|
26
|
+
|
|
27
|
+
## Usage
|
|
28
|
+
|
|
29
|
+
```ts
|
|
30
|
+
import { createClient } from './.suiteportal/client';
|
|
31
|
+
|
|
32
|
+
const ns = await createClient({
|
|
33
|
+
accountId: process.env.NETSUITE_ACCOUNT_ID!,
|
|
34
|
+
consumerKey: process.env.NETSUITE_CONSUMER_KEY!,
|
|
35
|
+
consumerSecret: process.env.NETSUITE_CONSUMER_SECRET!,
|
|
36
|
+
tokenId: process.env.NETSUITE_TOKEN_ID!,
|
|
37
|
+
tokenSecret: process.env.NETSUITE_TOKEN_SECRET!,
|
|
38
|
+
});
|
|
39
|
+
|
|
40
|
+
// Find records with typed filters
|
|
41
|
+
const customers = await ns.customer.findMany({
|
|
42
|
+
where: { balance: { gt: 1000 }, isInactive: { equals: false } },
|
|
43
|
+
select: { companyName: true, email: true, balance: true },
|
|
44
|
+
orderBy: { balance: 'desc' },
|
|
45
|
+
take: 50,
|
|
46
|
+
});
|
|
47
|
+
|
|
48
|
+
// Get a single record
|
|
49
|
+
const order = await ns.salesorder.findFirst({
|
|
50
|
+
where: { entity: { equals: 42 } },
|
|
51
|
+
});
|
|
52
|
+
|
|
53
|
+
// Count records
|
|
54
|
+
const total = await ns.customer.count({
|
|
55
|
+
where: { isInactive: { equals: false } },
|
|
56
|
+
});
|
|
57
|
+
|
|
58
|
+
// Create a record
|
|
59
|
+
const newCustomer = await ns.customer.create({
|
|
60
|
+
data: { companyName: 'Acme Corp', email: 'info@acme.com' },
|
|
61
|
+
});
|
|
62
|
+
|
|
63
|
+
// Update a record
|
|
64
|
+
await ns.customer.update({
|
|
65
|
+
where: { id: 123 },
|
|
66
|
+
data: { email: 'new@acme.com' },
|
|
67
|
+
});
|
|
68
|
+
|
|
69
|
+
// Delete a record
|
|
70
|
+
await ns.customer.delete({
|
|
71
|
+
where: { id: 123 },
|
|
72
|
+
});
|
|
73
|
+
|
|
74
|
+
// Raw SuiteQL escape hatch
|
|
75
|
+
const raw = await ns.$queryRaw<{ id: string; companyname: string }>(
|
|
76
|
+
'SELECT id, companyname FROM customer WHERE balance > 1000'
|
|
77
|
+
);
|
|
78
|
+
```
|
|
79
|
+
|
|
80
|
+
## Features
|
|
81
|
+
|
|
82
|
+
- **Prisma-like API** — `findMany`, `findFirst`, `count`, `create`, `update`, `delete`
|
|
83
|
+
- **Full TypeScript types** — generated from your NetSuite schema with autocomplete
|
|
84
|
+
- **Schema introspection** — auto-discovers record types, fields, and relations
|
|
85
|
+
- **SuiteQL query builder** — `where`, `select`, `orderBy`, `take`, `skip`, `include`
|
|
86
|
+
- **REST Record CRUD** — create, update, delete via NetSuite REST API
|
|
87
|
+
- **Raw SuiteQL** — `$queryRaw<T>()` for complex queries
|
|
88
|
+
- **Local Studio** — `npx suiteportal studio` for a web UI to browse data
|
|
89
|
+
- **Zero runtime deps** — connector uses only Node.js built-ins
|
|
90
|
+
|
|
91
|
+
## Documentation
|
|
92
|
+
|
|
93
|
+
Visit [suiteportal.io/docs](https://suiteportal.io/docs) for full documentation.
|
|
94
|
+
|
|
95
|
+
## License
|
|
96
|
+
|
|
97
|
+
[MIT](https://github.com/Suite-Portal/netsuite-orm/blob/main/LICENSE)
|
package/dist/next.cjs
ADDED
|
@@ -0,0 +1,380 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __create = Object.create;
|
|
3
|
+
var __defProp = Object.defineProperty;
|
|
4
|
+
var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
|
|
5
|
+
var __getOwnPropNames = Object.getOwnPropertyNames;
|
|
6
|
+
var __getProtoOf = Object.getPrototypeOf;
|
|
7
|
+
var __hasOwnProp = Object.prototype.hasOwnProperty;
|
|
8
|
+
var __export = (target, all) => {
|
|
9
|
+
for (var name in all)
|
|
10
|
+
__defProp(target, name, { get: all[name], enumerable: true });
|
|
11
|
+
};
|
|
12
|
+
var __copyProps = (to, from, except, desc) => {
|
|
13
|
+
if (from && typeof from === "object" || typeof from === "function") {
|
|
14
|
+
for (let key of __getOwnPropNames(from))
|
|
15
|
+
if (!__hasOwnProp.call(to, key) && key !== except)
|
|
16
|
+
__defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
|
|
17
|
+
}
|
|
18
|
+
return to;
|
|
19
|
+
};
|
|
20
|
+
var __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__getProtoOf(mod)) : {}, __copyProps(
|
|
21
|
+
// If the importer is in node compatibility mode or this is not an ESM
|
|
22
|
+
// file that has been converted to a CommonJS file using a Babel-
|
|
23
|
+
// compatible transform (i.e. "__esModule" has not been set), then set
|
|
24
|
+
// "default" to the CommonJS "module.exports" for node compatibility.
|
|
25
|
+
isNodeMode || !mod || !mod.__esModule ? __defProp(target, "default", { value: mod, enumerable: true }) : target,
|
|
26
|
+
mod
|
|
27
|
+
));
|
|
28
|
+
var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
|
|
29
|
+
|
|
30
|
+
// src/next.tsx
|
|
31
|
+
var next_exports = {};
|
|
32
|
+
__export(next_exports, {
|
|
33
|
+
RecordTable: () => RecordTable,
|
|
34
|
+
createNS: () => createNS,
|
|
35
|
+
extractError: () => extractError,
|
|
36
|
+
getFormFields: () => getFormFields,
|
|
37
|
+
getRecordColumns: () => getRecordColumns,
|
|
38
|
+
ns: () => ns,
|
|
39
|
+
processFormData: () => processFormData,
|
|
40
|
+
recordAction: () => recordAction
|
|
41
|
+
});
|
|
42
|
+
module.exports = __toCommonJS(next_exports);
|
|
43
|
+
var import_server_only = require("server-only");
|
|
44
|
+
var import_react = require("react");
|
|
45
|
+
var import_link = __toESM(require("next/link"), 1);
|
|
46
|
+
var import_node_fs = require("fs");
|
|
47
|
+
var import_node_path = require("path");
|
|
48
|
+
var import_cache = require("next/cache");
|
|
49
|
+
var import_navigation = require("next/navigation");
|
|
50
|
+
var import_client_runtime = require("@suiteportal/client-runtime");
|
|
51
|
+
var import_jsx_runtime = require("react/jsx-runtime");
|
|
52
|
+
function configFromEnv(overrides) {
|
|
53
|
+
const accountId = overrides?.accountId ?? process.env.NETSUITE_ACCOUNT_ID;
|
|
54
|
+
const consumerKey = overrides?.consumerKey ?? process.env.NETSUITE_CONSUMER_KEY;
|
|
55
|
+
const consumerSecret = overrides?.consumerSecret ?? process.env.NETSUITE_CONSUMER_SECRET;
|
|
56
|
+
const tokenId = overrides?.tokenId ?? process.env.NETSUITE_TOKEN_ID;
|
|
57
|
+
const tokenSecret = overrides?.tokenSecret ?? process.env.NETSUITE_TOKEN_SECRET;
|
|
58
|
+
if (!accountId || !consumerKey || !consumerSecret || !tokenId || !tokenSecret) {
|
|
59
|
+
const missing = [
|
|
60
|
+
!accountId && "NETSUITE_ACCOUNT_ID",
|
|
61
|
+
!consumerKey && "NETSUITE_CONSUMER_KEY",
|
|
62
|
+
!consumerSecret && "NETSUITE_CONSUMER_SECRET",
|
|
63
|
+
!tokenId && "NETSUITE_TOKEN_ID",
|
|
64
|
+
!tokenSecret && "NETSUITE_TOKEN_SECRET"
|
|
65
|
+
].filter(Boolean);
|
|
66
|
+
throw new Error(
|
|
67
|
+
`Missing required NetSuite environment variable(s): ${missing.join(", ")}. Add them to your .env.local file.`
|
|
68
|
+
);
|
|
69
|
+
}
|
|
70
|
+
return {
|
|
71
|
+
accountId,
|
|
72
|
+
consumerKey,
|
|
73
|
+
consumerSecret,
|
|
74
|
+
tokenId,
|
|
75
|
+
tokenSecret,
|
|
76
|
+
...overrides
|
|
77
|
+
};
|
|
78
|
+
}
|
|
79
|
+
function createNS(options) {
|
|
80
|
+
const { schemaPath, ...configOverrides } = options ?? {};
|
|
81
|
+
let clientPromise = null;
|
|
82
|
+
function ensure() {
|
|
83
|
+
if (!clientPromise) {
|
|
84
|
+
const config = configFromEnv(configOverrides);
|
|
85
|
+
const clientOpts = schemaPath ? { schemaPath } : void 0;
|
|
86
|
+
clientPromise = (0, import_client_runtime.getClient)(config, clientOpts);
|
|
87
|
+
}
|
|
88
|
+
return clientPromise;
|
|
89
|
+
}
|
|
90
|
+
const handler = {
|
|
91
|
+
get(_, prop) {
|
|
92
|
+
if (prop === "then" || typeof prop === "symbol") return void 0;
|
|
93
|
+
if (prop === "$queryRaw") {
|
|
94
|
+
return async (sql) => {
|
|
95
|
+
const c = await ensure();
|
|
96
|
+
return c.$queryRaw(sql);
|
|
97
|
+
};
|
|
98
|
+
}
|
|
99
|
+
if (prop === "$disconnect") {
|
|
100
|
+
return () => {
|
|
101
|
+
clientPromise = null;
|
|
102
|
+
(0, import_client_runtime.destroySingleton)();
|
|
103
|
+
};
|
|
104
|
+
}
|
|
105
|
+
if (prop === "$loadSchema") {
|
|
106
|
+
return async () => {
|
|
107
|
+
const c = await ensure();
|
|
108
|
+
return c.$loadSchema();
|
|
109
|
+
};
|
|
110
|
+
}
|
|
111
|
+
const recordName = prop;
|
|
112
|
+
return new Proxy({}, {
|
|
113
|
+
get(__, method) {
|
|
114
|
+
if (typeof method === "symbol" || method === "then") return void 0;
|
|
115
|
+
return async (...args) => {
|
|
116
|
+
const c = await ensure();
|
|
117
|
+
const delegate = c[recordName];
|
|
118
|
+
if (!delegate || typeof delegate[method] !== "function") {
|
|
119
|
+
throw new Error(`ns.${recordName}.${String(method)} is not a function`);
|
|
120
|
+
}
|
|
121
|
+
return delegate[method](...args);
|
|
122
|
+
};
|
|
123
|
+
}
|
|
124
|
+
});
|
|
125
|
+
}
|
|
126
|
+
};
|
|
127
|
+
return new Proxy({}, handler);
|
|
128
|
+
}
|
|
129
|
+
var SYSTEM_FIELDS = /* @__PURE__ */ new Set(["id", "internalid", "externalid", "links"]);
|
|
130
|
+
function isRefField(field) {
|
|
131
|
+
if (field.referenceTarget) return true;
|
|
132
|
+
return field.nativeType === "reference" || field.type === "select" || field.type === "multiselect";
|
|
133
|
+
}
|
|
134
|
+
function getRefTarget(record, field) {
|
|
135
|
+
if (field.referenceTarget) return field.referenceTarget;
|
|
136
|
+
const rel = Object.values(record.relations).find((r) => r.foreignKey === field.id);
|
|
137
|
+
return rel?.target;
|
|
138
|
+
}
|
|
139
|
+
function readSchema(schemaPath) {
|
|
140
|
+
const path = schemaPath ?? (0, import_node_path.join)(process.cwd(), ".suiteportal", "schema.json");
|
|
141
|
+
if (!(0, import_node_fs.existsSync)(path)) return {};
|
|
142
|
+
const raw = JSON.parse((0, import_node_fs.readFileSync)(path, "utf-8"));
|
|
143
|
+
const out = {};
|
|
144
|
+
for (const [key, rec] of Object.entries(raw.records ?? {})) {
|
|
145
|
+
const r = rec;
|
|
146
|
+
out[key] = { fields: r.fields ?? {}, relations: r.relations ?? {} };
|
|
147
|
+
}
|
|
148
|
+
return out;
|
|
149
|
+
}
|
|
150
|
+
async function getFormFields(ns2, recordType, options) {
|
|
151
|
+
const records = readSchema(options?.schemaPath);
|
|
152
|
+
const rec = records[recordType];
|
|
153
|
+
if (!rec) return [];
|
|
154
|
+
const writable = Object.values(rec.fields).filter(
|
|
155
|
+
(f) => !f.readOnly && !SYSTEM_FIELDS.has(f.id.toLowerCase())
|
|
156
|
+
);
|
|
157
|
+
const mandatory = writable.filter((f) => f.required);
|
|
158
|
+
const includeSet = new Set(options?.include ?? ["email", "phone", "url", "comments"]);
|
|
159
|
+
const extras = writable.filter((f) => !f.required && includeSet.has(f.id));
|
|
160
|
+
const allFields = [...mandatory, ...extras];
|
|
161
|
+
const fields = await Promise.all(
|
|
162
|
+
allFields.map(async (f) => {
|
|
163
|
+
const isRef = isRefField(f);
|
|
164
|
+
let fieldOptions = [];
|
|
165
|
+
if (isRef) {
|
|
166
|
+
if (f.enumValues && f.enumValues.length > 0) {
|
|
167
|
+
fieldOptions = f.enumValues.map((e) => ({ value: e.value, label: e.label }));
|
|
168
|
+
} else {
|
|
169
|
+
const target = getRefTarget(rec, f);
|
|
170
|
+
if (target) {
|
|
171
|
+
try {
|
|
172
|
+
const rows = await ns2.$queryRaw(
|
|
173
|
+
`SELECT id, name FROM ${target} ORDER BY name FETCH FIRST 200 ROWS ONLY`
|
|
174
|
+
);
|
|
175
|
+
fieldOptions = rows.map((r) => ({
|
|
176
|
+
value: String(r.id),
|
|
177
|
+
label: String(r.name ?? r.id)
|
|
178
|
+
}));
|
|
179
|
+
} catch {
|
|
180
|
+
}
|
|
181
|
+
}
|
|
182
|
+
}
|
|
183
|
+
}
|
|
184
|
+
return {
|
|
185
|
+
id: f.id,
|
|
186
|
+
label: f.label,
|
|
187
|
+
type: f.type,
|
|
188
|
+
required: mandatory.includes(f),
|
|
189
|
+
isRef,
|
|
190
|
+
options: fieldOptions
|
|
191
|
+
};
|
|
192
|
+
})
|
|
193
|
+
);
|
|
194
|
+
return fields;
|
|
195
|
+
}
|
|
196
|
+
function processFormData(formData) {
|
|
197
|
+
const refFieldIds = new Set(
|
|
198
|
+
(formData.get("__refFields")?.toString() ?? "").split(",").filter(Boolean)
|
|
199
|
+
);
|
|
200
|
+
const data = {};
|
|
201
|
+
for (const [key, value] of formData.entries()) {
|
|
202
|
+
if (typeof value !== "string" || !value.trim()) continue;
|
|
203
|
+
if (key.startsWith("_")) continue;
|
|
204
|
+
const trimmed = value.trim();
|
|
205
|
+
if (refFieldIds.has(key)) {
|
|
206
|
+
data[key] = { id: Number(trimmed) };
|
|
207
|
+
} else {
|
|
208
|
+
data[key] = trimmed;
|
|
209
|
+
}
|
|
210
|
+
}
|
|
211
|
+
const addr1 = formData.get("_addr1")?.toString().trim();
|
|
212
|
+
if (addr1) {
|
|
213
|
+
const address = { addr1 };
|
|
214
|
+
for (const f of ["addr2", "city", "state", "zip", "addressee"]) {
|
|
215
|
+
const v = formData.get(`_${f}`)?.toString().trim();
|
|
216
|
+
if (v) address[f] = v;
|
|
217
|
+
}
|
|
218
|
+
const country = formData.get("_country")?.toString().trim() || "US";
|
|
219
|
+
address.country = { id: country };
|
|
220
|
+
data.addressBook = {
|
|
221
|
+
items: [{ defaultBilling: true, defaultShipping: true, addressBookAddress: address }]
|
|
222
|
+
};
|
|
223
|
+
}
|
|
224
|
+
return data;
|
|
225
|
+
}
|
|
226
|
+
function extractError(err) {
|
|
227
|
+
const message = err instanceof Error ? err.message : "Operation failed";
|
|
228
|
+
const details = err?.details;
|
|
229
|
+
const detail = details ? typeof details === "string" ? details : JSON.stringify(details) : "";
|
|
230
|
+
return detail ? `${message} \u2014 ${detail}` : message;
|
|
231
|
+
}
|
|
232
|
+
var _ns = null;
|
|
233
|
+
function ns() {
|
|
234
|
+
if (!_ns) _ns = createNS();
|
|
235
|
+
return _ns;
|
|
236
|
+
}
|
|
237
|
+
function recordAction(record, redirectTo) {
|
|
238
|
+
async function action(_prev, formData) {
|
|
239
|
+
const client = ns();
|
|
240
|
+
const data = processFormData(formData);
|
|
241
|
+
try {
|
|
242
|
+
await client[record].create({ data });
|
|
243
|
+
} catch (err) {
|
|
244
|
+
return { error: extractError(err) };
|
|
245
|
+
}
|
|
246
|
+
(0, import_cache.revalidatePath)(redirectTo);
|
|
247
|
+
(0, import_navigation.redirect)(redirectTo);
|
|
248
|
+
}
|
|
249
|
+
return action;
|
|
250
|
+
}
|
|
251
|
+
function getRecordColumns(recordType, schemaPath) {
|
|
252
|
+
const records = readSchema(schemaPath);
|
|
253
|
+
const rec = records[recordType];
|
|
254
|
+
if (!rec) return [];
|
|
255
|
+
return Object.values(rec.fields).filter((f) => !SYSTEM_FIELDS.has(f.id.toLowerCase())).map((f) => ({ id: f.id, label: f.label, type: f.type }));
|
|
256
|
+
}
|
|
257
|
+
async function RecordTable({
|
|
258
|
+
record,
|
|
259
|
+
searchParams,
|
|
260
|
+
title,
|
|
261
|
+
columns,
|
|
262
|
+
take: takeProp,
|
|
263
|
+
searchField = "companyname",
|
|
264
|
+
createHref
|
|
265
|
+
}) {
|
|
266
|
+
const client = ns();
|
|
267
|
+
const params = await searchParams;
|
|
268
|
+
const take = Math.min(Number(params.take) || takeProp || 25, 100);
|
|
269
|
+
const skip = Number(params.skip) || 0;
|
|
270
|
+
const basePath = `/${record}s`;
|
|
271
|
+
const heading = title ?? record.charAt(0).toUpperCase() + record.slice(1) + "s";
|
|
272
|
+
const singular = heading.replace(/s$/, "");
|
|
273
|
+
const schemaCols = getRecordColumns(record);
|
|
274
|
+
const displayCols = columns ? columns.map((id) => schemaCols.find((c) => c.id === id) ?? { id, label: id, type: "string" }) : schemaCols.slice(0, 4);
|
|
275
|
+
const where = {};
|
|
276
|
+
if (params.q) where[searchField] = { contains: params.q };
|
|
277
|
+
const delegate = client[record];
|
|
278
|
+
const rows = await delegate.findMany({ where, take, skip, orderBy: { id: "asc" } });
|
|
279
|
+
function href(newSkip) {
|
|
280
|
+
const p = new URLSearchParams();
|
|
281
|
+
p.set("take", String(take));
|
|
282
|
+
p.set("skip", String(newSkip));
|
|
283
|
+
if (params.q) p.set("q", params.q);
|
|
284
|
+
return `${basePath}?${p.toString()}`;
|
|
285
|
+
}
|
|
286
|
+
return /* @__PURE__ */ (0, import_jsx_runtime.jsx)("div", { className: "p-6", children: /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("div", { className: "rounded-lg border bg-white shadow-sm", children: [
|
|
287
|
+
/* @__PURE__ */ (0, import_jsx_runtime.jsxs)("div", { className: "flex items-center justify-between border-b px-6 py-4", children: [
|
|
288
|
+
/* @__PURE__ */ (0, import_jsx_runtime.jsx)("h2", { className: "text-lg font-semibold", children: heading }),
|
|
289
|
+
/* @__PURE__ */ (0, import_jsx_runtime.jsxs)(
|
|
290
|
+
import_link.default,
|
|
291
|
+
{
|
|
292
|
+
href: createHref ?? `${basePath}/new`,
|
|
293
|
+
className: "rounded-md bg-gray-900 px-3 py-1.5 text-sm font-medium text-white hover:bg-gray-800",
|
|
294
|
+
children: [
|
|
295
|
+
"New ",
|
|
296
|
+
singular
|
|
297
|
+
]
|
|
298
|
+
}
|
|
299
|
+
)
|
|
300
|
+
] }),
|
|
301
|
+
/* @__PURE__ */ (0, import_jsx_runtime.jsxs)("div", { className: "px-6 py-4 space-y-4", children: [
|
|
302
|
+
/* @__PURE__ */ (0, import_jsx_runtime.jsxs)("form", { method: "get", className: "flex items-center gap-2", children: [
|
|
303
|
+
/* @__PURE__ */ (0, import_jsx_runtime.jsx)(
|
|
304
|
+
"input",
|
|
305
|
+
{
|
|
306
|
+
name: "q",
|
|
307
|
+
defaultValue: params.q,
|
|
308
|
+
placeholder: "Search...",
|
|
309
|
+
className: "max-w-sm rounded-md border px-3 py-1.5 text-sm outline-none focus:ring-2 focus:ring-gray-300"
|
|
310
|
+
}
|
|
311
|
+
),
|
|
312
|
+
/* @__PURE__ */ (0, import_jsx_runtime.jsx)(
|
|
313
|
+
"button",
|
|
314
|
+
{
|
|
315
|
+
type: "submit",
|
|
316
|
+
className: "rounded-md border px-3 py-1.5 text-sm font-medium hover:bg-gray-50",
|
|
317
|
+
children: "Search"
|
|
318
|
+
}
|
|
319
|
+
),
|
|
320
|
+
params.q && /* @__PURE__ */ (0, import_jsx_runtime.jsx)(import_link.default, { href: basePath, className: "text-sm text-gray-500 hover:text-gray-900", children: "Clear" })
|
|
321
|
+
] }),
|
|
322
|
+
rows.length > 0 ? /* @__PURE__ */ (0, import_jsx_runtime.jsxs)(import_jsx_runtime.Fragment, { children: [
|
|
323
|
+
/* @__PURE__ */ (0, import_jsx_runtime.jsxs)("table", { className: "w-full text-sm", children: [
|
|
324
|
+
/* @__PURE__ */ (0, import_jsx_runtime.jsx)("thead", { children: /* @__PURE__ */ (0, import_jsx_runtime.jsx)("tr", { className: "border-b text-left", children: displayCols.map((col) => /* @__PURE__ */ (0, import_jsx_runtime.jsx)("th", { className: "px-3 py-2 font-medium text-gray-500", children: col.label }, col.id)) }) }),
|
|
325
|
+
/* @__PURE__ */ (0, import_jsx_runtime.jsx)("tbody", { children: rows.map((row) => /* @__PURE__ */ (0, import_jsx_runtime.jsx)("tr", { className: "border-b last:border-0 hover:bg-gray-50", children: displayCols.map((col) => /* @__PURE__ */ (0, import_jsx_runtime.jsx)("td", { className: "px-3 py-2", children: String(row[col.id] ?? "") }, col.id)) }, String(row.id))) })
|
|
326
|
+
] }),
|
|
327
|
+
/* @__PURE__ */ (0, import_jsx_runtime.jsxs)("div", { className: "flex items-center justify-between text-sm", children: [
|
|
328
|
+
/* @__PURE__ */ (0, import_jsx_runtime.jsxs)("span", { className: "text-gray-500", children: [
|
|
329
|
+
"Showing ",
|
|
330
|
+
skip + 1,
|
|
331
|
+
"\u2013",
|
|
332
|
+
skip + rows.length,
|
|
333
|
+
params.q && /* @__PURE__ */ (0, import_jsx_runtime.jsxs)(import_jsx_runtime.Fragment, { children: [
|
|
334
|
+
" matching ",
|
|
335
|
+
/* @__PURE__ */ (0, import_jsx_runtime.jsx)("strong", { children: params.q })
|
|
336
|
+
] })
|
|
337
|
+
] }),
|
|
338
|
+
/* @__PURE__ */ (0, import_jsx_runtime.jsxs)("div", { className: "flex gap-2", children: [
|
|
339
|
+
skip > 0 && /* @__PURE__ */ (0, import_jsx_runtime.jsx)(
|
|
340
|
+
import_link.default,
|
|
341
|
+
{
|
|
342
|
+
href: href(Math.max(0, skip - take)),
|
|
343
|
+
className: "rounded-md border px-3 py-1 hover:bg-gray-50",
|
|
344
|
+
children: "Previous"
|
|
345
|
+
}
|
|
346
|
+
),
|
|
347
|
+
rows.length === take && /* @__PURE__ */ (0, import_jsx_runtime.jsx)(
|
|
348
|
+
import_link.default,
|
|
349
|
+
{
|
|
350
|
+
href: href(skip + take),
|
|
351
|
+
className: "rounded-md border px-3 py-1 hover:bg-gray-50",
|
|
352
|
+
children: "Next"
|
|
353
|
+
}
|
|
354
|
+
)
|
|
355
|
+
] })
|
|
356
|
+
] })
|
|
357
|
+
] }) : /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("div", { className: "py-12 text-center text-gray-500", children: [
|
|
358
|
+
/* @__PURE__ */ (0, import_jsx_runtime.jsxs)("p", { className: "font-medium", children: [
|
|
359
|
+
"No ",
|
|
360
|
+
heading.toLowerCase(),
|
|
361
|
+
" found"
|
|
362
|
+
] }),
|
|
363
|
+
/* @__PURE__ */ (0, import_jsx_runtime.jsx)("p", { className: "mt-1 text-sm", children: params.q ? `No results for "${params.q}". Try a different search.` : "Run suiteportal introspect to pull your NetSuite data." }),
|
|
364
|
+
params.q && /* @__PURE__ */ (0, import_jsx_runtime.jsx)(import_link.default, { href: basePath, className: "mt-3 inline-block text-sm font-medium text-gray-900 underline", children: "Clear search" })
|
|
365
|
+
] })
|
|
366
|
+
] })
|
|
367
|
+
] }) });
|
|
368
|
+
}
|
|
369
|
+
// Annotate the CommonJS export names for ESM import in node:
|
|
370
|
+
0 && (module.exports = {
|
|
371
|
+
RecordTable,
|
|
372
|
+
createNS,
|
|
373
|
+
extractError,
|
|
374
|
+
getFormFields,
|
|
375
|
+
getRecordColumns,
|
|
376
|
+
ns,
|
|
377
|
+
processFormData,
|
|
378
|
+
recordAction
|
|
379
|
+
});
|
|
380
|
+
//# sourceMappingURL=next.cjs.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../src/next.tsx"],"sourcesContent":["import 'server-only';\nimport React from 'react';\nimport Link from 'next/link';\nimport { readFileSync, existsSync } from 'node:fs';\nimport { join } from 'node:path';\nimport { revalidatePath } from 'next/cache';\nimport { redirect } from 'next/navigation';\nimport { getClient, destroySingleton } from '@suiteportal/client-runtime';\nimport type { ClientOptions } from '@suiteportal/client-runtime';\nimport type { SuitePortalClient } from '@suiteportal/client-runtime';\nimport type { NetSuiteConfig } from '@suiteportal/connector';\nimport type {\n FindManyArgs,\n FindFirstArgs,\n CountArgs,\n CreateArgs,\n UpdateArgs,\n DeleteArgs,\n UpsertArgs,\n} from '@suiteportal/client-runtime';\n\n/**\n * Reads NetSuite config from standard NETSUITE_* environment variables.\n * Throws at query-time (not import-time) if vars are missing.\n */\nfunction configFromEnv(overrides?: Partial<NetSuiteConfig>): NetSuiteConfig {\n const accountId = overrides?.accountId ?? process.env.NETSUITE_ACCOUNT_ID;\n const consumerKey = overrides?.consumerKey ?? process.env.NETSUITE_CONSUMER_KEY;\n const consumerSecret = overrides?.consumerSecret ?? process.env.NETSUITE_CONSUMER_SECRET;\n const tokenId = overrides?.tokenId ?? process.env.NETSUITE_TOKEN_ID;\n const tokenSecret = overrides?.tokenSecret ?? process.env.NETSUITE_TOKEN_SECRET;\n\n if (!accountId || !consumerKey || !consumerSecret || !tokenId || !tokenSecret) {\n const missing = [\n !accountId && 'NETSUITE_ACCOUNT_ID',\n !consumerKey && 'NETSUITE_CONSUMER_KEY',\n !consumerSecret && 'NETSUITE_CONSUMER_SECRET',\n !tokenId && 'NETSUITE_TOKEN_ID',\n !tokenSecret && 'NETSUITE_TOKEN_SECRET',\n ].filter(Boolean);\n throw new Error(\n `Missing required NetSuite environment variable(s): ${missing.join(', ')}. ` +\n `Add them to your .env.local file.`,\n );\n }\n\n return {\n accountId,\n consumerKey,\n consumerSecret,\n tokenId,\n tokenSecret,\n ...overrides,\n };\n}\n\n/** Options for createNS. Extends NetSuiteConfig with schemaPath. */\nexport interface NSOptions extends Partial<NetSuiteConfig> {\n /** Path to schema.json. Default: '.suiteportal/schema.json' */\n schemaPath?: string;\n}\n\n/** A delegate for a single record type. */\nexport interface NSDelegate {\n findMany(args?: FindManyArgs): Promise<Record<string, unknown>[]>;\n findFirst(args?: FindFirstArgs): Promise<Record<string, unknown> | null>;\n count(args?: CountArgs): Promise<number>;\n create(args: CreateArgs): Promise<Record<string, unknown>>;\n update(args: UpdateArgs): Promise<Record<string, unknown>>;\n delete(args: DeleteArgs): Promise<void>;\n upsert(args: UpsertArgs): Promise<Record<string, unknown>>;\n}\n\n/**\n * Lazy NetSuite client. Access record types as properties: `ns.customer.findMany(...)`.\n *\n * The client initializes on first use — no need to `await` the client itself.\n * For full per-record type safety, use the generated client from `.suiteportal/client`.\n */\nexport type NSClient = {\n /** Execute a raw SuiteQL query. */\n $queryRaw<T = Record<string, unknown>>(sql: string): Promise<T[]>;\n /** Release resources and clear the singleton. */\n $disconnect(): void;\n /** Reload the schema from disk. */\n $loadSchema(): Promise<void>;\n} & {\n /** Access any record type by name. */\n readonly [recordType: string]: NSDelegate;\n};\n\n/**\n * Create a lazy NetSuite client for Next.js server use.\n *\n * - Reads credentials from `NETSUITE_*` env vars automatically\n * - Initializes the singleton on first query (not at import time)\n * - Includes `server-only` to prevent credentials leaking to client bundles\n *\n * ```ts\n * // lib/ns.ts\n * import { createNS } from 'suiteportal/next';\n * export const ns = createNS();\n *\n * // app/customers/page.tsx\n * import { ns } from '@/lib/ns';\n * export default async function Page() {\n * const customers = await ns.customer.findMany({ take: 50 });\n * return <pre>{JSON.stringify(customers, null, 2)}</pre>;\n * }\n * ```\n */\nexport function createNS(options?: NSOptions): NSClient {\n const { schemaPath, ...configOverrides } = options ?? {};\n let clientPromise: Promise<SuitePortalClient> | null = null;\n\n function ensure(): Promise<SuitePortalClient> {\n if (!clientPromise) {\n const config = configFromEnv(configOverrides);\n const clientOpts: ClientOptions | undefined = schemaPath ? { schemaPath } : undefined;\n clientPromise = getClient(config, clientOpts);\n }\n return clientPromise;\n }\n\n const handler: ProxyHandler<object> = {\n get(_, prop) {\n // Prevent Promise detection (await, Promise.resolve, etc.)\n if (prop === 'then' || typeof prop === 'symbol') return undefined;\n\n if (prop === '$queryRaw') {\n return async <T = Record<string, unknown>>(sql: string): Promise<T[]> => {\n const c = await ensure();\n return c.$queryRaw<T>(sql);\n };\n }\n\n if (prop === '$disconnect') {\n return () => {\n clientPromise = null;\n destroySingleton();\n };\n }\n\n if (prop === '$loadSchema') {\n return async () => {\n const c = await ensure();\n return c.$loadSchema();\n };\n }\n\n // Record delegate — proxy each method call through the real client\n const recordName = prop as string;\n return new Proxy({} as NSDelegate, {\n get(__, method) {\n if (typeof method === 'symbol' || method === 'then') return undefined;\n return async (...args: unknown[]) => {\n const c = await ensure();\n const delegate = (c as unknown as Record<string, unknown>)[recordName];\n if (!delegate || typeof (delegate as Record<string, unknown>)[method as string] !== 'function') {\n throw new Error(`ns.${recordName}.${String(method)} is not a function`);\n }\n return (delegate as Record<string, (...a: unknown[]) => unknown>)[method as string]!(...args);\n };\n },\n });\n },\n };\n\n return new Proxy({}, handler) as NSClient;\n}\n\n// ─── Form utilities ──────────────────────────────────────────────────────────\n\n/** A field option for dropdowns (select/reference fields). */\nexport interface FormFieldOption {\n value: string;\n label: string;\n}\n\n/** A field definition ready for form rendering. */\nexport interface FormField {\n /** Field ID (e.g. \"companyname\", \"subsidiary\"). */\n id: string;\n /** Human-readable label (e.g. \"Company Name\"). */\n label: string;\n /** Normalized type (e.g. \"string\", \"select\", \"email\"). */\n type: string;\n /** Whether the field is required for record creation. */\n required: boolean;\n /** Whether this is a reference/select field (value should be sent as `{ id: N }`). */\n isRef: boolean;\n /** Dropdown options for reference/select fields. Empty for plain inputs. */\n options: FormFieldOption[];\n}\n\ninterface SchemaField {\n id: string;\n label: string;\n type: string;\n required: boolean;\n readOnly: boolean;\n nativeType: string;\n enumValues?: Array<{ value: string; label: string }>;\n referenceTarget?: string;\n}\n\ninterface SchemaRelation {\n name: string;\n type: string;\n target: string;\n foreignKey: string;\n}\n\ninterface SchemaRecord {\n fields: Record<string, SchemaField>;\n relations: Record<string, SchemaRelation>;\n}\n\nconst SYSTEM_FIELDS = new Set(['id', 'internalid', 'externalid', 'links']);\n\nfunction isRefField(field: SchemaField): boolean {\n if (field.referenceTarget) return true;\n return field.nativeType === 'reference' || field.type === 'select' || field.type === 'multiselect';\n}\n\nfunction getRefTarget(record: SchemaRecord, field: SchemaField): string | undefined {\n if (field.referenceTarget) return field.referenceTarget;\n const rel = Object.values(record.relations).find((r) => r.foreignKey === field.id);\n return rel?.target;\n}\n\nfunction readSchema(schemaPath?: string): Record<string, SchemaRecord> {\n const path = schemaPath ?? join(process.cwd(), '.suiteportal', 'schema.json');\n if (!existsSync(path)) return {};\n const raw = JSON.parse(readFileSync(path, 'utf-8'));\n const out: Record<string, SchemaRecord> = {};\n for (const [key, rec] of Object.entries(raw.records ?? {})) {\n const r = rec as SchemaRecord;\n out[key] = { fields: r.fields ?? {}, relations: r.relations ?? {} };\n }\n return out;\n}\n\n/**\n * Load form field definitions for a record type.\n *\n * Reads the introspected schema to discover required and optional fields,\n * then queries NetSuite for dropdown options on reference/select fields.\n *\n * @param ns - The lazy NS client (from `createNS()`)\n * @param recordType - The record type ID (e.g. \"customer\", \"salesorder\")\n * @param options - Optional overrides\n * @returns Array of `FormField` objects ready for form rendering\n *\n * ```ts\n * import { createNS, getFormFields } from 'suiteportal/next';\n * const ns = createNS();\n * const fields = await getFormFields(ns, 'customer');\n * ```\n */\nexport async function getFormFields(\n ns: NSClient,\n recordType: string,\n options?: {\n /** Extra optional field IDs to include beyond required fields. */\n include?: string[];\n /** Schema.json path override. */\n schemaPath?: string;\n },\n): Promise<FormField[]> {\n const records = readSchema(options?.schemaPath);\n const rec = records[recordType];\n if (!rec) return [];\n\n const writable = Object.values(rec.fields).filter(\n (f) => !f.readOnly && !SYSTEM_FIELDS.has(f.id.toLowerCase()),\n );\n const mandatory = writable.filter((f) => f.required);\n const includeSet = new Set(options?.include ?? ['email', 'phone', 'url', 'comments']);\n const extras = writable.filter((f) => !f.required && includeSet.has(f.id));\n const allFields = [...mandatory, ...extras];\n\n // Load dropdown options for reference fields in parallel\n const fields: FormField[] = await Promise.all(\n allFields.map(async (f) => {\n const isRef = isRefField(f);\n let fieldOptions: FormFieldOption[] = [];\n\n if (isRef) {\n // Prefer inline enum values from the REST catalog\n if (f.enumValues && f.enumValues.length > 0) {\n fieldOptions = f.enumValues.map((e) => ({ value: e.value, label: e.label }));\n } else {\n const target = getRefTarget(rec, f);\n if (target) {\n try {\n const rows = await ns.$queryRaw<Record<string, unknown>>(\n `SELECT id, name FROM ${target} ORDER BY name FETCH FIRST 200 ROWS ONLY`,\n );\n fieldOptions = rows.map((r) => ({\n value: String(r.id),\n label: String(r.name ?? r.id),\n }));\n } catch {\n // Target not queryable — form will fall back to ID input\n }\n }\n }\n }\n\n return {\n id: f.id,\n label: f.label,\n type: f.type,\n required: mandatory.includes(f),\n isRef,\n options: fieldOptions,\n };\n }),\n );\n\n return fields;\n}\n\n/**\n * Convert a FormData submission into a REST API-ready payload.\n *\n * Handles:\n * - Reference/select fields → `{ id: N }` objects\n * - Address fields (prefixed with `_addr1`, `_city`, etc.) → `addressBook` sublist\n * - All internal fields (prefixed with `_` or `__`) are excluded from the body\n *\n * @param formData - The raw FormData from the form submission\n * @returns A plain object ready to pass to `ns.{record}.create({ data })`\n *\n * ```ts\n * const data = processFormData(formData);\n * await ns.customer.create({ data });\n * ```\n */\nexport function processFormData(formData: FormData): Record<string, unknown> {\n const refFieldIds = new Set(\n (formData.get('__refFields')?.toString() ?? '').split(',').filter(Boolean),\n );\n\n const data: Record<string, unknown> = {};\n\n for (const [key, value] of formData.entries()) {\n if (typeof value !== 'string' || !value.trim()) continue;\n if (key.startsWith('_')) continue; // skip internal fields\n\n const trimmed = value.trim();\n if (refFieldIds.has(key)) {\n data[key] = { id: Number(trimmed) };\n } else {\n data[key] = trimmed;\n }\n }\n\n // Build addressBook sublist from _-prefixed address fields\n const addr1 = formData.get('_addr1')?.toString().trim();\n if (addr1) {\n const address: Record<string, unknown> = { addr1 };\n for (const f of ['addr2', 'city', 'state', 'zip', 'addressee'] as const) {\n const v = formData.get(`_${f}`)?.toString().trim();\n if (v) address[f] = v;\n }\n const country = formData.get('_country')?.toString().trim() || 'US';\n address.country = { id: country };\n\n data.addressBook = {\n items: [{ defaultBilling: true, defaultShipping: true, addressBookAddress: address }],\n };\n }\n\n return data;\n}\n\n/** Result of a record creation attempt. */\nexport interface ActionState {\n error?: string;\n}\n\n/**\n * Extract a human-readable error message from a NetSuite REST API error.\n */\nexport function extractError(err: unknown): string {\n const message = err instanceof Error ? err.message : 'Operation failed';\n const details = (err as { details?: unknown })?.details;\n const detail = details\n ? typeof details === 'string'\n ? details\n : JSON.stringify(details)\n : '';\n return detail ? `${message} — ${detail}` : message;\n}\n\n// ─── Module-level singleton for components & actions ─────────────────────────\n\nlet _ns: NSClient | null = null;\n\n/** Get the module-level NS client singleton (auto-reads env vars). */\nexport function ns(): NSClient {\n if (!_ns) _ns = createNS();\n return _ns;\n}\n\n// ─── Record action factory ───────────────────────────────────────────────────\n\n/**\n * Create a server action for a record type.\n *\n * Returns an async function compatible with `useActionState` that:\n * - Parses FormData via `processFormData`\n * - Creates the record via the ORM client\n * - Redirects on success, returns `{ error }` on failure\n *\n * ```ts\n * // app/actions.ts\n * 'use server';\n * import { recordAction } from 'suiteportal/next';\n * export const createCustomer = recordAction('customer', '/customers');\n * export const createVendor = recordAction('vendor', '/vendors');\n * ```\n */\nexport function recordAction(\n record: string,\n redirectTo: string,\n): (_prev: ActionState, formData: FormData) => Promise<ActionState> {\n async function action(_prev: ActionState, formData: FormData): Promise<ActionState> {\n const client = ns();\n const data = processFormData(formData);\n try {\n await (client as unknown as Record<string, { create(a: { data: unknown }): Promise<unknown> }>)[record]!.create({ data });\n } catch (err) {\n return { error: extractError(err) };\n }\n revalidatePath(redirectTo);\n redirect(redirectTo); // redirect() throws internally — never actually returns\n }\n return action;\n}\n\n// ─── Schema helpers for components ───────────────────────────────────────────\n\nexport interface RecordFieldMeta {\n id: string;\n label: string;\n type: string;\n}\n\n/**\n * Get basic field metadata for a record type (for table column auto-detection).\n * Returns non-system, non-readOnly fields in schema order.\n */\nexport function getRecordColumns(recordType: string, schemaPath?: string): RecordFieldMeta[] {\n const records = readSchema(schemaPath);\n const rec = records[recordType];\n if (!rec) return [];\n\n return Object.values(rec.fields)\n .filter((f) => !SYSTEM_FIELDS.has(f.id.toLowerCase()))\n .map((f) => ({ id: f.id, label: f.label, type: f.type }));\n}\n\n// ─── RecordTable server component ────────────────────────────────────────────\n\nexport interface RecordTableProps {\n /** NetSuite record type (e.g. \"customer\", \"vendor\"). */\n record: string;\n /** Next.js page searchParams — pass directly from your page component. */\n searchParams: Promise<{ q?: string; take?: string; skip?: string }>;\n /** Table heading. Defaults to capitalized record + \"s\". */\n title?: string;\n /** Field IDs to show as columns. Auto-detected from schema if omitted. */\n columns?: string[];\n /** Rows per page. Default 25. */\n take?: number;\n /** Field ID to filter when searching. Default \"companyname\". */\n searchField?: string;\n /** \"New\" button href. Defaults to `/${record}s/new`. */\n createHref?: string;\n}\n\n/**\n * Drop-in server component that renders a searchable, paginated table\n * for any NetSuite record type.\n *\n * ```tsx\n * import { RecordTable } from 'suiteportal/next';\n *\n * export default async function Page({ searchParams }) {\n * return <RecordTable record=\"customer\" searchParams={searchParams} />;\n * }\n * ```\n */\nexport async function RecordTable({\n record,\n searchParams,\n title,\n columns,\n take: takeProp,\n searchField = 'companyname',\n createHref,\n}: RecordTableProps) {\n const client = ns();\n const params = await searchParams;\n const take = Math.min(Number(params.take) || takeProp || 25, 100);\n const skip = Number(params.skip) || 0;\n const basePath = `/${record}s`;\n const heading = title ?? record.charAt(0).toUpperCase() + record.slice(1) + 's';\n const singular = heading.replace(/s$/, '');\n\n // Auto-detect columns from schema if not provided\n const schemaCols = getRecordColumns(record);\n const displayCols = columns\n ? columns.map((id) => schemaCols.find((c) => c.id === id) ?? { id, label: id, type: 'string' })\n : schemaCols.slice(0, 4);\n\n // Query\n const where: Record<string, unknown> = {};\n if (params.q) where[searchField] = { contains: params.q };\n\n const delegate = (client as unknown as Record<string, { findMany(a: unknown): Promise<Record<string, unknown>[]> }>)[record]!;\n const rows = await delegate.findMany({ where, take, skip, orderBy: { id: 'asc' } });\n\n function href(newSkip: number) {\n const p = new URLSearchParams();\n p.set('take', String(take));\n p.set('skip', String(newSkip));\n if (params.q) p.set('q', params.q);\n return `${basePath}?${p.toString()}`;\n }\n\n return (\n <div className=\"p-6\">\n <div className=\"rounded-lg border bg-white shadow-sm\">\n {/* Header */}\n <div className=\"flex items-center justify-between border-b px-6 py-4\">\n <h2 className=\"text-lg font-semibold\">{heading}</h2>\n <Link\n href={createHref ?? `${basePath}/new`}\n className=\"rounded-md bg-gray-900 px-3 py-1.5 text-sm font-medium text-white hover:bg-gray-800\"\n >\n New {singular}\n </Link>\n </div>\n\n <div className=\"px-6 py-4 space-y-4\">\n {/* Search */}\n <form method=\"get\" className=\"flex items-center gap-2\">\n <input\n name=\"q\"\n defaultValue={params.q}\n placeholder=\"Search...\"\n className=\"max-w-sm rounded-md border px-3 py-1.5 text-sm outline-none focus:ring-2 focus:ring-gray-300\"\n />\n <button\n type=\"submit\"\n className=\"rounded-md border px-3 py-1.5 text-sm font-medium hover:bg-gray-50\"\n >\n Search\n </button>\n {params.q && (\n <Link href={basePath} className=\"text-sm text-gray-500 hover:text-gray-900\">\n Clear\n </Link>\n )}\n </form>\n\n {/* Table */}\n {rows.length > 0 ? (\n <>\n <table className=\"w-full text-sm\">\n <thead>\n <tr className=\"border-b text-left\">\n {displayCols.map((col) => (\n <th key={col.id} className=\"px-3 py-2 font-medium text-gray-500\">\n {col.label}\n </th>\n ))}\n </tr>\n </thead>\n <tbody>\n {rows.map((row) => (\n <tr key={String(row.id)} className=\"border-b last:border-0 hover:bg-gray-50\">\n {displayCols.map((col) => (\n <td key={col.id} className=\"px-3 py-2\">\n {String(row[col.id] ?? '')}\n </td>\n ))}\n </tr>\n ))}\n </tbody>\n </table>\n\n {/* Pagination */}\n <div className=\"flex items-center justify-between text-sm\">\n <span className=\"text-gray-500\">\n Showing {skip + 1}–{skip + rows.length}\n {params.q && <> matching <strong>{params.q}</strong></>}\n </span>\n <div className=\"flex gap-2\">\n {skip > 0 && (\n <Link\n href={href(Math.max(0, skip - take))}\n className=\"rounded-md border px-3 py-1 hover:bg-gray-50\"\n >\n Previous\n </Link>\n )}\n {rows.length === take && (\n <Link\n href={href(skip + take)}\n className=\"rounded-md border px-3 py-1 hover:bg-gray-50\"\n >\n Next\n </Link>\n )}\n </div>\n </div>\n </>\n ) : (\n <div className=\"py-12 text-center text-gray-500\">\n <p className=\"font-medium\">No {heading.toLowerCase()} found</p>\n <p className=\"mt-1 text-sm\">\n {params.q\n ? `No results for \"${params.q}\". Try a different search.`\n : 'Run suiteportal introspect to pull your NetSuite data.'}\n </p>\n {params.q && (\n <Link href={basePath} className=\"mt-3 inline-block text-sm font-medium text-gray-900 underline\">\n Clear search\n </Link>\n )}\n </div>\n )}\n </div>\n </div>\n </div>\n );\n}\n"],"mappings":"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","names":["ns","Link"]}
|