feat(schema): add auth, people, and financial tables (#26)
Add users, organizations, teams, groups, and project members tables. Extend customers/vendors with netsuite fields. Add netsuite schema for invoices, bills, payments, and credit memos. Include all migrations, seeds, new UI primitives, and config updates. Co-authored-by: Nicholai <nicholaivogelfilms@gmail.com>
This commit is contained in:
parent
d9b3e33b6e
commit
9c3a19279a
24
.env.example
Executable file
24
.env.example
Executable file
@ -0,0 +1,24 @@
|
||||
# WorkOS Authentication
|
||||
# Get these from your WorkOS dashboard: https://dashboard.workos.com
|
||||
WORKOS_API_KEY=your_workos_api_key_here
|
||||
WORKOS_CLIENT_ID=your_workos_client_id_here
|
||||
WORKOS_COOKIE_PASSWORD=your_random_32_character_string_here
|
||||
WORKOS_REDIRECT_URI=http://localhost:3000
|
||||
|
||||
# NetSuite Integration
|
||||
# OAuth 2.0 credentials from your NetSuite account
|
||||
NETSUITE_ACCOUNT_ID=your_account_id_here
|
||||
NETSUITE_CLIENT_ID=your_client_id_here
|
||||
NETSUITE_CLIENT_SECRET=your_client_secret_here
|
||||
NETSUITE_REDIRECT_URI=http://localhost:3000/api/netsuite/callback
|
||||
|
||||
# Token encryption key (generate with: openssl rand -hex 32)
|
||||
NETSUITE_TOKEN_ENCRYPTION_KEY=your_encryption_key_here
|
||||
|
||||
# Optional: Max concurrent requests to NetSuite API (default: 15)
|
||||
NETSUITE_CONCURRENCY_LIMIT=15
|
||||
|
||||
# Optional: For Automatic Github Deployments
|
||||
GITHUB_TOKEN=your_github_repo_token_here
|
||||
GITHUB_REPO=High-Performance-Structures/compass
|
||||
|
||||
6
.gitignore
vendored
6
.gitignore
vendored
@ -4,6 +4,7 @@ node_modules/
|
||||
# next.js
|
||||
.next/
|
||||
out/
|
||||
.open-next/
|
||||
|
||||
# cloudflare
|
||||
.wrangler/
|
||||
@ -20,3 +21,8 @@ dist/
|
||||
# misc
|
||||
.DS_Store
|
||||
*.tsbuildinfo
|
||||
|
||||
# dev tools
|
||||
.playwright-mcp
|
||||
mobile-ui-references/
|
||||
.fuse_*
|
||||
|
||||
92
bun.lock
92
bun.lock
@ -39,6 +39,8 @@
|
||||
"@radix-ui/react-tooltip": "^1.2.8",
|
||||
"@tabler/icons-react": "^3.36.1",
|
||||
"@tanstack/react-table": "^8.21.3",
|
||||
"@workos-inc/authkit-nextjs": "^2.13.0",
|
||||
"@workos-inc/node": "^8.1.0",
|
||||
"class-variance-authority": "^0.7.1",
|
||||
"clsx": "^2.1.1",
|
||||
"cmdk": "^1.1.1",
|
||||
@ -457,6 +459,12 @@
|
||||
|
||||
"@opennextjs/cloudflare": ["@opennextjs/cloudflare@1.15.0", "", { "dependencies": { "@ast-grep/napi": "0.40.0", "@dotenvx/dotenvx": "1.31.0", "@opennextjs/aws": "3.9.11", "cloudflare": "^4.4.1", "enquirer": "^2.4.1", "glob": "^12.0.0", "ts-tqdm": "^0.8.6", "yargs": "^18.0.0" }, "peerDependencies": { "next": "^14.2.35 || ~15.0.7 || ~15.1.11 || ~15.2.8 || ~15.3.8 || ~15.4.10 || ~15.5.9 || ^16.0.10", "wrangler": "^4.59.2" }, "bin": { "opennextjs-cloudflare": "dist/cli/index.js" } }, "sha512-AZPaqk25XUBxtdkfjUZQBbY3ovifVLC4GgSRHuejqsIWfv8KjTRNFVdaCaaPmbLkrgymqxNhkbfJS5sD28AK/g=="],
|
||||
|
||||
"@peculiar/asn1-schema": ["@peculiar/asn1-schema@2.6.0", "", { "dependencies": { "asn1js": "^3.0.6", "pvtsutils": "^1.3.6", "tslib": "^2.8.1" } }, "sha512-xNLYLBFTBKkCzEZIw842BxytQQATQv+lDTCEMZ8C196iJcJJMBUZxrhSTxLaohMyKK8QlzRNTRkUmanucnDSqg=="],
|
||||
|
||||
"@peculiar/json-schema": ["@peculiar/json-schema@1.1.12", "", { "dependencies": { "tslib": "^2.0.0" } }, "sha512-coUfuoMeIB7B8/NMekxaDzLhaYmp0HZNPEjYRm9goRou8UZIC3z21s0sL9AWoCw4EG876QyO3kYrc61WNF9B/w=="],
|
||||
|
||||
"@peculiar/webcrypto": ["@peculiar/webcrypto@1.5.0", "", { "dependencies": { "@peculiar/asn1-schema": "^2.3.8", "@peculiar/json-schema": "^1.1.12", "pvtsutils": "^1.3.5", "tslib": "^2.6.2", "webcrypto-core": "^1.8.0" } }, "sha512-BRs5XUAwiyCDQMsVA9IDvDa7UBR9gAvPHgugOeGng3YN6vJ9JYonyDc0lNczErgtCWtucjR5N7VtaonboD/ezg=="],
|
||||
|
||||
"@poppinss/colors": ["@poppinss/colors@4.1.6", "", { "dependencies": { "kleur": "^4.1.5" } }, "sha512-H9xkIdFswbS8n1d6vmRd8+c10t2Qe+rZITbbDHHkQixH5+2x1FDGmi/0K+WgWiqQFKPSlIYB7jlH6Kpfn6Fleg=="],
|
||||
|
||||
"@poppinss/dumper": ["@poppinss/dumper@0.6.5", "", { "dependencies": { "@poppinss/colors": "^4.1.5", "@sindresorhus/is": "^7.0.2", "supports-color": "^10.0.0" } }, "sha512-NBdYIb90J7LfOI32dOewKI1r7wnkiH6m920puQ3qHUeZkxNkQiFnXVWoE6YtFSv6QOiPPf7ys6i+HWWecDz7sw=="],
|
||||
@ -727,6 +735,18 @@
|
||||
|
||||
"@tybys/wasm-util": ["@tybys/wasm-util@0.10.1", "", { "dependencies": { "tslib": "^2.4.0" } }, "sha512-9tTaPJLSiejZKx+Bmog4uSubteqTvFrVrURwkmHixBo0G4seD0zUxp98E1DzUBJxLQ3NPwXrGKDiVjwx/DpPsg=="],
|
||||
|
||||
"@types/accepts": ["@types/accepts@1.3.7", "", { "dependencies": { "@types/node": "*" } }, "sha512-Pay9fq2lM2wXPWbteBsRAGiWH2hig4ZE2asK+mm7kUzlxRTfL961rj89I6zV/E3PcIkDqyuBEcMxFT7rccugeQ=="],
|
||||
|
||||
"@types/body-parser": ["@types/body-parser@1.19.6", "", { "dependencies": { "@types/connect": "*", "@types/node": "*" } }, "sha512-HLFeCYgz89uk22N5Qg3dvGvsv46B8GLvKKo1zKG4NybA8U2DiEO3w9lqGg29t/tfLRJpJ6iQxnVw4OnB7MoM9g=="],
|
||||
|
||||
"@types/connect": ["@types/connect@3.4.38", "", { "dependencies": { "@types/node": "*" } }, "sha512-K6uROf1LD88uDQqJCktA4yzL1YYAK6NgfsI0v/mTgyPKWsX1CnJ0XPSDhViejru1GcRkLWb8RlzFYJRqGUbaug=="],
|
||||
|
||||
"@types/content-disposition": ["@types/content-disposition@0.5.9", "", {}, "sha512-8uYXI3Gw35MhiVYhG3s295oihrxRyytcRHjSjqnqZVDDy/xcGBRny7+Xj1Wgfhv5QzRtN2hB2dVRBUX9XW3UcQ=="],
|
||||
|
||||
"@types/cookie": ["@types/cookie@0.5.4", "", {}, "sha512-7z/eR6O859gyWIAjuvBWFzNURmf2oPBmJlfVWkwehU5nzIyjwBsTh7WMmEEV4JFnHuQ3ex4oyTvfKzcyJVDBNA=="],
|
||||
|
||||
"@types/cookies": ["@types/cookies@0.9.2", "", { "dependencies": { "@types/connect": "*", "@types/express": "*", "@types/keygrip": "*", "@types/node": "*" } }, "sha512-1AvkDdZM2dbyFybL4fxpuNCaWyv//0AwsuUk2DWeXyM1/5ZKm6W3z6mQi24RZ4l2ucY+bkSHzbDVpySqPGuV8A=="],
|
||||
|
||||
"@types/d3-array": ["@types/d3-array@3.2.2", "", {}, "sha512-hOLWVbm7uRza0BYXpIIW5pxfrKe0W+D5lrFiAEYR+pb6w3N2SwSMaJbXdUfSEv+dT4MfHBLtn5js0LAWaO6otw=="],
|
||||
|
||||
"@types/d3-color": ["@types/d3-color@3.1.3", "", {}, "sha512-iO90scth9WAbmgv7ogoq57O9YpKmFBbmoEoCHDB2xMBY0+/KVrqAaCDyCE16dUspeOvIxFFRI+0sEtqDqy2b4A=="],
|
||||
@ -747,18 +767,42 @@
|
||||
|
||||
"@types/estree": ["@types/estree@1.0.8", "", {}, "sha512-dWHzHa2WqEXI/O1E9OjrocMTKJl2mSrEolh1Iomrv6U+JuNwaHXsXx9bLu5gG7BUWFIN0skIQJQ/L1rIex4X6w=="],
|
||||
|
||||
"@types/express": ["@types/express@4.17.25", "", { "dependencies": { "@types/body-parser": "*", "@types/express-serve-static-core": "^4.17.33", "@types/qs": "*", "@types/serve-static": "^1" } }, "sha512-dVd04UKsfpINUnK0yBoYHDF3xu7xVH4BuDotC/xGuycx4CgbP48X/KF/586bcObxT0HENHXEU8Nqtu6NR+eKhw=="],
|
||||
|
||||
"@types/express-serve-static-core": ["@types/express-serve-static-core@4.19.8", "", { "dependencies": { "@types/node": "*", "@types/qs": "*", "@types/range-parser": "*", "@types/send": "*" } }, "sha512-02S5fmqeoKzVZCHPZid4b8JH2eM5HzQLZWN2FohQEy/0eXTq8VXZfSN6Pcr3F6N9R/vNrj7cpgbhjie6m/1tCA=="],
|
||||
|
||||
"@types/http-assert": ["@types/http-assert@1.5.6", "", {}, "sha512-TTEwmtjgVbYAzZYWyeHPrrtWnfVkm8tQkP8P21uQifPgMRgjrow3XDEYqucuC8SKZJT7pUnhU/JymvjggxO9vw=="],
|
||||
|
||||
"@types/http-errors": ["@types/http-errors@2.0.5", "", {}, "sha512-r8Tayk8HJnX0FztbZN7oVqGccWgw98T/0neJphO91KkmOzug1KkofZURD4UaD5uH8AqcFLfdPErnBod0u71/qg=="],
|
||||
|
||||
"@types/json-schema": ["@types/json-schema@7.0.15", "", {}, "sha512-5+fP8P8MFNC+AyZCDxrB2pkZFPGzqQWUzpSeuuVLvm8VMcorNYavBqoFcxK8bQz4Qsbn4oUEEem4wDLfcysGHA=="],
|
||||
|
||||
"@types/json5": ["@types/json5@0.0.29", "", {}, "sha512-dRLjCWHYg4oaA77cxO64oO+7JwCwnIzkZPdrrC71jQmQtlhM556pwKo5bUzqvZndkVbeFLIIi+9TC40JNF5hNQ=="],
|
||||
|
||||
"@types/keygrip": ["@types/keygrip@1.0.6", "", {}, "sha512-lZuNAY9xeJt7Bx4t4dx0rYCDqGPW8RXhQZK1td7d4H6E9zYbLoOtjBvfwdTKpsyxQI/2jv+armjX/RW+ZNpXOQ=="],
|
||||
|
||||
"@types/koa": ["@types/koa@2.15.0", "", { "dependencies": { "@types/accepts": "*", "@types/content-disposition": "*", "@types/cookies": "*", "@types/http-assert": "*", "@types/http-errors": "*", "@types/keygrip": "*", "@types/koa-compose": "*", "@types/node": "*" } }, "sha512-7QFsywoE5URbuVnG3loe03QXuGajrnotr3gQkXcEBShORai23MePfFYdhz90FEtBBpkyIYQbVD+evKtloCgX3g=="],
|
||||
|
||||
"@types/koa-compose": ["@types/koa-compose@3.2.9", "", { "dependencies": { "@types/koa": "*" } }, "sha512-BroAZ9FTvPiCy0Pi8tjD1OfJ7bgU1gQf0eR6e1Vm+JJATy9eKOG3hQMFtMciMawiSOVnLMdmUOC46s7HBhSTsA=="],
|
||||
|
||||
"@types/mime": ["@types/mime@1.3.5", "", {}, "sha512-/pyBZWSLD2n0dcHE3hq8s8ZvcETHtEuF+3E7XVt0Ig2nvsVQXdghHVcEkIWjy9A0wKfTn97a/PSDYohKIlnP/w=="],
|
||||
|
||||
"@types/node": ["@types/node@25.0.10", "", { "dependencies": { "undici-types": "~7.16.0" } }, "sha512-zWW5KPngR/yvakJgGOmZ5vTBemDoSqF3AcV/LrO5u5wTWyEAVVh+IT39G4gtyAkh3CtTZs8aX/yRM82OfzHJRg=="],
|
||||
|
||||
"@types/node-fetch": ["@types/node-fetch@2.6.13", "", { "dependencies": { "@types/node": "*", "form-data": "^4.0.4" } }, "sha512-QGpRVpzSaUs30JBSGPjOg4Uveu384erbHBoT1zeONvyCfwQxIkUshLAOqN/k9EjGviPRmWTTe6aH2qySWKTVSw=="],
|
||||
|
||||
"@types/qs": ["@types/qs@6.14.0", "", {}, "sha512-eOunJqu0K1923aExK6y8p6fsihYEn/BYuQ4g0CxAAgFc4b/ZLN4CrsRZ55srTdqoiLzU2B2evC+apEIxprEzkQ=="],
|
||||
|
||||
"@types/range-parser": ["@types/range-parser@1.2.7", "", {}, "sha512-hKormJbkJqzQGhziax5PItDUTMAM9uE2XXQmM37dyd4hVM+5aVl7oVxMVUiVQn2oCQFN/LKCZdvSM0pFRqbSmQ=="],
|
||||
|
||||
"@types/react": ["@types/react@19.2.9", "", { "dependencies": { "csstype": "^3.2.2" } }, "sha512-Lpo8kgb/igvMIPeNV2rsYKTgaORYdO1XGVZ4Qz3akwOj0ySGYMPlQWa8BaLn0G63D1aSaAQ5ldR06wCpChQCjA=="],
|
||||
|
||||
"@types/react-dom": ["@types/react-dom@19.2.3", "", { "peerDependencies": { "@types/react": "^19.2.0" } }, "sha512-jp2L/eY6fn+KgVVQAOqYItbF0VY/YApe5Mz2F0aykSO8gx31bYCZyvSeYxCHKvzHG5eZjc+zyaS5BrBWya2+kQ=="],
|
||||
|
||||
"@types/send": ["@types/send@0.17.6", "", { "dependencies": { "@types/mime": "^1", "@types/node": "*" } }, "sha512-Uqt8rPBE8SY0RK8JB1EzVOIZ32uqy8HwdxCnoCOsYrvnswqmFZ/k+9Ikidlk/ImhsdvBsloHbAlewb2IEBV/Og=="],
|
||||
|
||||
"@types/serve-static": ["@types/serve-static@1.15.10", "", { "dependencies": { "@types/http-errors": "*", "@types/node": "*", "@types/send": "<1" } }, "sha512-tRs1dB+g8Itk72rlSI2ZrW6vZg0YrLI81iQSTkMmOqnqCaNr/8Ek4VwWcN5vZgCYWbg/JJSGBlUaYGAOP73qBw=="],
|
||||
|
||||
"@typescript-eslint/eslint-plugin": ["@typescript-eslint/eslint-plugin@8.53.1", "", { "dependencies": { "@eslint-community/regexpp": "^4.12.2", "@typescript-eslint/scope-manager": "8.53.1", "@typescript-eslint/type-utils": "8.53.1", "@typescript-eslint/utils": "8.53.1", "@typescript-eslint/visitor-keys": "8.53.1", "ignore": "^7.0.5", "natural-compare": "^1.4.0", "ts-api-utils": "^2.4.0" }, "peerDependencies": { "@typescript-eslint/parser": "^8.53.1", "eslint": "^8.57.0 || ^9.0.0", "typescript": ">=4.8.4 <6.0.0" } }, "sha512-cFYYFZ+oQFi6hUnBTbLRXfTJiaQtYE3t4O692agbBl+2Zy+eqSKWtPjhPXJu1G7j4RLjKgeJPDdq3EqOwmX5Ag=="],
|
||||
|
||||
"@typescript-eslint/parser": ["@typescript-eslint/parser@8.53.1", "", { "dependencies": { "@typescript-eslint/scope-manager": "8.53.1", "@typescript-eslint/types": "8.53.1", "@typescript-eslint/typescript-estree": "8.53.1", "@typescript-eslint/visitor-keys": "8.53.1", "debug": "^4.4.3" }, "peerDependencies": { "eslint": "^8.57.0 || ^9.0.0", "typescript": ">=4.8.4 <6.0.0" } }, "sha512-nm3cvFN9SqZGXjmw5bZ6cGmvJSyJPn0wU9gHAZZHDnZl2wF9PhHv78Xf06E0MaNk4zLVHL8hb2/c32XvyJOLQg=="],
|
||||
@ -817,6 +861,10 @@
|
||||
|
||||
"@unrs/resolver-binding-win32-x64-msvc": ["@unrs/resolver-binding-win32-x64-msvc@1.11.1", "", { "os": "win32", "cpu": "x64" }, "sha512-lrW200hZdbfRtztbygyaq/6jP6AKE8qQN2KvPcJ+x7wiD038YtnYtZ82IMNJ69GJibV7bwL3y9FgK+5w/pYt6g=="],
|
||||
|
||||
"@workos-inc/authkit-nextjs": ["@workos-inc/authkit-nextjs@2.13.0", "", { "dependencies": { "@workos-inc/node": "^7.72.0", "iron-session": "^8.0.1", "jose": "^5.2.3", "path-to-regexp": "^6.2.2" }, "peerDependencies": { "next": "^13.5.9 || ^14.2.26 || ^15.2.3 || ^16", "react": "^18.0 || ^19.0.0", "react-dom": "^18.0 || ^19.0.0" } }, "sha512-ppxzhfakPumHPPggYSROaAlgxfS7viFMPmWPG76Tp6Rh9G7YqkBSp7xtvMtM6gXOFFMvvEJRcKEta6YHeercTQ=="],
|
||||
|
||||
"@workos-inc/node": ["@workos-inc/node@8.1.0", "", { "dependencies": { "iron-webcrypto": "^2.0.0", "jose": "~6.1.0" } }, "sha512-Ep2QSP43y4ZdJIOuL4Hjaq5f0u8Z0qZe7QWzrrBV6cHc/kcicDBcB0AanMP6eB9x3x6FaHfevLbkbjPF4+TCYQ=="],
|
||||
|
||||
"abort-controller": ["abort-controller@3.0.0", "", { "dependencies": { "event-target-shim": "^5.0.0" } }, "sha512-h8lQ8tacZYnR3vNQTgibj+tODHI5/+l06Au2Pcriv/Gmet0eaj4TwWH41sO9wnHDiQsEj19q0drzdWdeAHtweg=="],
|
||||
|
||||
"accepts": ["accepts@2.0.0", "", { "dependencies": { "mime-types": "^3.0.0", "negotiator": "^1.0.0" } }, "sha512-5cvg6CtKwfgdmVqY1WIiXKc3Q1bkRqGLi+2W/6ao+6Y7gu/RCwRuAhGEzh5B4KlszSuTLgZYuqFqo5bImjNKng=="],
|
||||
@ -857,6 +905,8 @@
|
||||
|
||||
"arraybuffer.prototype.slice": ["arraybuffer.prototype.slice@1.0.4", "", { "dependencies": { "array-buffer-byte-length": "^1.0.1", "call-bind": "^1.0.8", "define-properties": "^1.2.1", "es-abstract": "^1.23.5", "es-errors": "^1.3.0", "get-intrinsic": "^1.2.6", "is-array-buffer": "^3.0.4" } }, "sha512-BNoCY6SXXPQ7gF2opIP4GBE+Xw7U+pHMYKuzjgCN3GwiaIR09UUeKfheyIry77QtrCBlC0KK0q5/TER/tYh3PQ=="],
|
||||
|
||||
"asn1js": ["asn1js@3.0.7", "", { "dependencies": { "pvtsutils": "^1.3.6", "pvutils": "^1.1.3", "tslib": "^2.8.1" } }, "sha512-uLvq6KJu04qoQM6gvBfKFjlh6Gl0vOKQuR5cJMDHQkmwfMOQeN3F3SHCv9SNYSL+CRoHvOGFfllDlVz03GQjvQ=="],
|
||||
|
||||
"ast-types-flow": ["ast-types-flow@0.0.8", "", {}, "sha512-OH/2E5Fg20h2aPrbe+QL8JZQFko0YZaF+j4mnQ7BGhfavO7OpSLa8a0y9sBwomHdSbkhTS8TQNayBfnW5DwbvQ=="],
|
||||
|
||||
"async-function": ["async-function@1.0.0", "", {}, "sha512-hsU18Ae8CDTR6Kgu9DYf0EbCr/a5iGL0rytQDobUcdpYOKokk8LEjVphnXkDkgpi0wYVsqrXuP0bZxJaTqdgoA=="],
|
||||
@ -873,6 +923,8 @@
|
||||
|
||||
"balanced-match": ["balanced-match@1.0.2", "", {}, "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw=="],
|
||||
|
||||
"base64-js": ["base64-js@1.5.1", "", {}, "sha512-AKpaYlHn8t4SVbOHCy+b5+KKgvR4vrsD8vbvrbiQJps7fKDTkjkDry6ji0rUJjC0kzbNePLwzxq8iypo41qeWA=="],
|
||||
|
||||
"blake3-wasm": ["blake3-wasm@2.1.5", "", {}, "sha512-F1+K8EbfOZE49dtoPtmxUQrpXaBIl3ICvasLh+nJta0xkz+9kF/7uet9fLnwKqhDrmj6g+6K3Tw9yQPUg2ka5g=="],
|
||||
|
||||
"body-parser": ["body-parser@2.2.2", "", { "dependencies": { "bytes": "^3.1.2", "content-type": "^1.0.5", "debug": "^4.4.3", "http-errors": "^2.0.0", "iconv-lite": "^0.7.0", "on-finished": "^2.4.1", "qs": "^6.14.1", "raw-body": "^3.0.1", "type-is": "^2.0.1" } }, "sha512-oP5VkATKlNwcgvxi0vM0p/D3n2C3EReYVX+DNYs5TjZFn/oQt2j+4sVJtSMr18pdRr8wjTcBl6LoV+FUwzPmNA=="],
|
||||
@ -883,6 +935,8 @@
|
||||
|
||||
"braces": ["braces@3.0.3", "", { "dependencies": { "fill-range": "^7.1.1" } }, "sha512-yQbXgO/OSZVD2IsiLlro+7Hf6Q18EJrKSEsdoMzKePKXct3gvD8oLcOQdIzGupr5Fj+EDe8gO/lxc1BzfMpxvA=="],
|
||||
|
||||
"buffer": ["buffer@6.0.3", "", { "dependencies": { "base64-js": "^1.3.1", "ieee754": "^1.2.1" } }, "sha512-FTiCpNxtwiZZHEZbcbTIcZjERVICn9yq/pDFkTl95/AxzD1naBctN7YO68riM/gLSDY7sdrMby8hofADYuuqOA=="],
|
||||
|
||||
"buffer-from": ["buffer-from@1.1.2", "", {}, "sha512-E+XQCRwSbaaiChtv6k6Dwgc+bx+Bs6vuKJHHl5kox/BaKbhiXzqQOwK4cO22yElGp2OCmjwVhT3HmxgyPGnJfQ=="],
|
||||
|
||||
"bytes": ["bytes@3.1.2", "", {}, "sha512-/Nf7TyzTx6S3yRJObOAV7956r8cr2+Oj8AC5dt8wSP3BQAoeX58NoHyCU8P8zGkNXStjTSi6fzO6F0pBdcYbEg=="],
|
||||
@ -1199,6 +1253,8 @@
|
||||
|
||||
"iconv-lite": ["iconv-lite@0.7.2", "", { "dependencies": { "safer-buffer": ">= 2.1.2 < 3.0.0" } }, "sha512-im9DjEDQ55s9fL4EYzOAv0yMqmMBSZp6G0VvFyTMPKWxiSBHUj9NW/qqLmXUwXrrM7AvqSlTCfvqRb0cM8yYqw=="],
|
||||
|
||||
"ieee754": ["ieee754@1.2.1", "", {}, "sha512-dcyqhDvX1C46lXZcVqCpK+FtMRQVdIMN6/Df5js2zouUsqG7I6sFxitIC+7KYK29KdXOLHdu9zL4sFnoVQnqaA=="],
|
||||
|
||||
"ignore": ["ignore@5.3.2", "", {}, "sha512-hsBTNUqQTDwkWtcdYI2i06Y/nUBEsNEDJKjWdigLvegy8kDuJAS8uRlpkkcQpyEXL0Z/pjDy5HBmMjRCJ2gq+g=="],
|
||||
|
||||
"import-fresh": ["import-fresh@3.3.1", "", { "dependencies": { "parent-module": "^1.0.0", "resolve-from": "^4.0.0" } }, "sha512-TR3KfrTZTYLPB6jUjfx6MF9WcWrHL9su5TObK4ZkYgBdWKPOFoSoQIdEuTuR82pmtxH2spWG9h6etwfr1pLBqQ=="],
|
||||
@ -1215,6 +1271,10 @@
|
||||
|
||||
"ipaddr.js": ["ipaddr.js@1.9.1", "", {}, "sha512-0KI/607xoxSToH7GjN1FfSbLoU0+btTicjsQSWQlh/hZykN8KpmMf7uYwPW3R+akZ6R/w18ZlXSHBYXiYUPO3g=="],
|
||||
|
||||
"iron-session": ["iron-session@8.0.4", "", { "dependencies": { "cookie": "^0.7.2", "iron-webcrypto": "^1.2.1", "uncrypto": "^0.1.3" } }, "sha512-9ivNnaKOd08osD0lJ3i6If23GFS2LsxyMU8Gf/uBUEgm8/8CC1hrrCHFDpMo3IFbpBgwoo/eairRsaD3c5itxA=="],
|
||||
|
||||
"iron-webcrypto": ["iron-webcrypto@2.0.0", "", { "dependencies": { "uint8array-extras": "^1.5.0" } }, "sha512-rtffZKDUHciZElM8mjFCufBC7nVhCxHYyWHESqs89OioEDz4parOofd8/uhrejh/INhQFfYQfByS22LlezR9sQ=="],
|
||||
|
||||
"is-array-buffer": ["is-array-buffer@3.0.5", "", { "dependencies": { "call-bind": "^1.0.8", "call-bound": "^1.0.3", "get-intrinsic": "^1.2.6" } }, "sha512-DDfANUiiG2wC1qawP66qlTugJeL5HyzMpfr8lLK+jMQirGzNod0B12cFB/9q838Ru27sBwfw78/rdoU7RERz6A=="],
|
||||
|
||||
"is-async-function": ["is-async-function@2.1.1", "", { "dependencies": { "async-function": "^1.0.0", "call-bound": "^1.0.3", "get-proto": "^1.0.1", "has-tostringtag": "^1.0.2", "safe-regex-test": "^1.1.0" } }, "sha512-9dgM/cZBnNvjzaMYHVoxxfPj2QXt22Ev7SuuPrs+xav0ukGB0S6d4ydZdEiM48kLx5kDV+QBPrpVnFyefL8kkQ=="],
|
||||
@ -1283,6 +1343,8 @@
|
||||
|
||||
"jiti": ["jiti@2.6.1", "", { "bin": { "jiti": "lib/jiti-cli.mjs" } }, "sha512-ekilCSN1jwRvIbgeg/57YFh8qQDNbwDb9xT/qu2DAHbFFZUicIl4ygVaAvzveMhMVr3LnpSKTNnwt8PoOfmKhQ=="],
|
||||
|
||||
"jose": ["jose@5.10.0", "", {}, "sha512-s+3Al/p9g32Iq+oqXxkW//7jk2Vig6FF1CFqzVXoTUXt2qz89YWbL+OwS17NFYEvxC35n0FKeGO2LGYSxeM2Gg=="],
|
||||
|
||||
"js-tokens": ["js-tokens@4.0.0", "", {}, "sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ=="],
|
||||
|
||||
"js-yaml": ["js-yaml@4.1.1", "", { "dependencies": { "argparse": "^2.0.1" }, "bin": { "js-yaml": "bin/js-yaml.js" } }, "sha512-qQKT4zQxXl8lLwBtHMWwaTcGfFOZviOJet3Oy/xmGk2gZH677CJM9EvtfdSkgWcATZhj/55JZ0rmy3myCT5lsA=="],
|
||||
@ -1305,6 +1367,8 @@
|
||||
|
||||
"language-tags": ["language-tags@1.0.9", "", { "dependencies": { "language-subtag-registry": "^0.3.20" } }, "sha512-MbjN408fEndfiQXbFQ1vnd+1NoLDsnQW41410oQBXiyXDMYH5z505juWa4KUE1LqxRC7DgOgZDbKLxHIwm27hA=="],
|
||||
|
||||
"leb": ["leb@1.0.0", "", {}, "sha512-Y3c3QZfvKWHX60BVOQPhLCvVGmDYWyJEiINE3drOog6KCyN2AOwvuQQzlS3uJg1J85kzpILXIUwRXULWavir+w=="],
|
||||
|
||||
"levn": ["levn@0.4.1", "", { "dependencies": { "prelude-ls": "^1.2.1", "type-check": "~0.4.0" } }, "sha512-+bT2uH4E5LGE7h/n3evcS/sQlJXCpIp6ym8OWJ5eV6+67Dsql/LaaT7qJBAt2rzfoa/5QBGBhxDix1dMt2kQKQ=="],
|
||||
|
||||
"lightningcss": ["lightningcss@1.30.2", "", { "dependencies": { "detect-libc": "^2.0.3" }, "optionalDependencies": { "lightningcss-android-arm64": "1.30.2", "lightningcss-darwin-arm64": "1.30.2", "lightningcss-darwin-x64": "1.30.2", "lightningcss-freebsd-x64": "1.30.2", "lightningcss-linux-arm-gnueabihf": "1.30.2", "lightningcss-linux-arm64-gnu": "1.30.2", "lightningcss-linux-arm64-musl": "1.30.2", "lightningcss-linux-x64-gnu": "1.30.2", "lightningcss-linux-x64-musl": "1.30.2", "lightningcss-win32-arm64-msvc": "1.30.2", "lightningcss-win32-x64-msvc": "1.30.2" } }, "sha512-utfs7Pr5uJyyvDETitgsaqSyjCb2qNRAtuqUeWIAKztsOYdcACf2KtARYXg2pSvhkt+9NfoaNY7fxjl6nuMjIQ=="],
|
||||
@ -1463,6 +1527,10 @@
|
||||
|
||||
"punycode": ["punycode@2.3.1", "", {}, "sha512-vYt7UD1U9Wg6138shLtLOvdAu+8DsC/ilFtEVHcH+wydcSpNE20AfSOduf6MkRFahL5FY7X1oU7nKVZFtfq8Fg=="],
|
||||
|
||||
"pvtsutils": ["pvtsutils@1.3.6", "", { "dependencies": { "tslib": "^2.8.1" } }, "sha512-PLgQXQ6H2FWCaeRak8vvk1GW462lMxB5s3Jm673N82zI4vqtVUPuZdffdZbPDFRoU8kAhItWFtPCWiPpp4/EDg=="],
|
||||
|
||||
"pvutils": ["pvutils@1.1.5", "", {}, "sha512-KTqnxsgGiQ6ZAzZCVlJH5eOjSnvlyEgx1m8bkRJfOhmGRqfo5KLvmAlACQkrjEtOQ4B7wF9TdSLIs9O90MX9xA=="],
|
||||
|
||||
"qs": ["qs@6.14.1", "", { "dependencies": { "side-channel": "^1.1.0" } }, "sha512-4EK3+xJl8Ts67nLYNwqw/dsFVnCf+qR7RgXSK9jEEm9unao3njwMDdmsdvoKBKHzxd7tCYz5e5M+SnMjdtXGQQ=="],
|
||||
|
||||
"queue-microtask": ["queue-microtask@1.2.3", "", {}, "sha512-NuaNSa6flKT5JaSYQzJok04JzTL1CA6aGhv5rfLW3PgqA+M2ChpZQnAC8h8i4ZFkBS8X5RqkDBHA7r4hej3K9A=="],
|
||||
@ -1643,8 +1711,12 @@
|
||||
|
||||
"typescript": ["typescript@5.9.3", "", { "bin": { "tsc": "bin/tsc", "tsserver": "bin/tsserver" } }, "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw=="],
|
||||
|
||||
"uint8array-extras": ["uint8array-extras@1.5.0", "", {}, "sha512-rvKSBiC5zqCCiDZ9kAOszZcDvdAHwwIKJG33Ykj43OKcWsnmcBRL09YTU4nOeHZ8Y2a7l1MgTd08SBe9A8Qj6A=="],
|
||||
|
||||
"unbox-primitive": ["unbox-primitive@1.1.0", "", { "dependencies": { "call-bound": "^1.0.3", "has-bigints": "^1.0.2", "has-symbols": "^1.1.0", "which-boxed-primitive": "^1.1.1" } }, "sha512-nWJ91DjeOkej/TA8pXQ3myruKpKEYgqvpw9lz4OPHj/NWFNluYrjbz9j01CJ8yKQd2g4jFoOkINCTW2I5LEEyw=="],
|
||||
|
||||
"uncrypto": ["uncrypto@0.1.3", "", {}, "sha512-Ql87qFHB3s/De2ClA9e0gsnS6zXG27SkTiSJwjCc9MebbfapQfuPzumMIUMi38ezPZVNFcHI9sUIepeQfw8J8Q=="],
|
||||
|
||||
"undici": ["undici@7.18.2", "", {}, "sha512-y+8YjDFzWdQlSE9N5nzKMT3g4a5UBX1HKowfdXh0uvAnTaqqwqB92Jt4UXBAeKekDs5IaDKyJFR4X1gYVCgXcw=="],
|
||||
|
||||
"undici-types": ["undici-types@7.16.0", "", {}, "sha512-Zz+aZWSj8LE6zoxD+xrjh4VfkIG8Ya6LvYkZqtUQGJPZjYl53ypCaUwWqo7eI0x66KBGeRo+mlBEkMSeSZ38Nw=="],
|
||||
@ -1675,6 +1747,8 @@
|
||||
|
||||
"web-streams-polyfill": ["web-streams-polyfill@4.0.0-beta.3", "", {}, "sha512-QW95TCTaHmsYfHDybGMwO5IJIM93I/6vTRk+daHTWFPhwh+C8Cg7j7XyKrwrj8Ib6vYXe0ocYNrmzY4xAAN6ug=="],
|
||||
|
||||
"webcrypto-core": ["webcrypto-core@1.8.1", "", { "dependencies": { "@peculiar/asn1-schema": "^2.3.13", "@peculiar/json-schema": "^1.1.12", "asn1js": "^3.0.5", "pvtsutils": "^1.3.5", "tslib": "^2.7.0" } }, "sha512-P+x1MvlNCXlKbLSOY4cYrdreqPG5hbzkmawbcXLKN/mf6DZW0SdNNkZ+sjwsqVkI4A4Ko2sPZmkZtCKY58w83A=="],
|
||||
|
||||
"webidl-conversions": ["webidl-conversions@3.0.1", "", {}, "sha512-2JAn3z8AR6rjK8Sm8orRC0h/bcl/DqL7tRPdGZ4I1CjdF+EaMLmYxBHyXuKL849eucPFhvBoxMsflfOb8kxaeQ=="],
|
||||
|
||||
"whatwg-url": ["whatwg-url@5.0.0", "", { "dependencies": { "tr46": "~0.0.3", "webidl-conversions": "^3.0.0" } }, "sha512-saE57nupxk6v3HY35+jzBwYa0rKSy0XR8JSxZPwgLr7ys0IBzhGviA1/TUGJLmSVqs8pb9AnvICXEuOHLprYTw=="],
|
||||
@ -2363,6 +2437,10 @@
|
||||
|
||||
"@typescript-eslint/typescript-estree/semver": ["semver@7.7.3", "", { "bin": { "semver": "bin/semver.js" } }, "sha512-SdsKMrI9TdgjdweUSR9MweHA4EJ8YxHn8DFaDisvhVlUOe4BF1tLD7GAj0lIqWVl+dPb/rExr0Btby5loQm20Q=="],
|
||||
|
||||
"@workos-inc/authkit-nextjs/@workos-inc/node": ["@workos-inc/node@7.82.0", "", { "dependencies": { "iron-session": "~6.3.1", "jose": "~5.6.3", "leb": "^1.0.0", "qs": "6.14.1" } }, "sha512-8h6XjIJf8nqNGYQMkWsjZ72WXMtzqrb4Azz39schXWoSRmwoK6tK+GpeAviJ9slddiJdOcp0Ht9/r+L6pGQMCg=="],
|
||||
|
||||
"@workos-inc/node/jose": ["jose@6.1.3", "", {}, "sha512-0TpaTfihd4QMNwrz/ob2Bp7X04yuxJkjRGi4aKmOqwhov54i6u79oCv7T+C7lo70MKH6BesI3vscD1yb/yzKXQ=="],
|
||||
|
||||
"cliui/strip-ansi": ["strip-ansi@7.1.2", "", { "dependencies": { "ansi-regex": "^6.0.1" } }, "sha512-gmBGslpoQJtgnMAvOVqGZpEz9dyoKTCzy2nfz/n8aIFhN/jCE/rCmcxabB6jOOHV+0WNnylOxaxBQPSvcWklhA=="],
|
||||
|
||||
"cloudflare/@types/node": ["@types/node@18.19.130", "", { "dependencies": { "undici-types": "~5.26.4" } }, "sha512-GRaXQx6jGfL8sKfaIDD6OupbIHBr9jv7Jnaml9tB7l4v068PAOXqfcujMMo5PhbIs6ggR1XODELqahT2R8v0fg=="],
|
||||
@ -2387,6 +2465,10 @@
|
||||
|
||||
"glob/minimatch": ["minimatch@10.1.1", "", { "dependencies": { "@isaacs/brace-expansion": "^5.0.0" } }, "sha512-enIvLvRAFZYXJzkCYG5RKmPfrFArdLv+R+lbQ53BmIMLIry74bjKzX6iHAm8WYamJkhSSEabrWN5D97XnKObjQ=="],
|
||||
|
||||
"iron-session/cookie": ["cookie@0.7.2", "", {}, "sha512-yki5XnKuf750l50uGTllt6kKILY4nQ1eNIQatoXEByZ5dWgnKqbnqmTrBE5B4N7lrMJKQ2ytWMiTO2o0v6Ew/w=="],
|
||||
|
||||
"iron-session/iron-webcrypto": ["iron-webcrypto@1.2.1", "", {}, "sha512-feOM6FaSr6rEABp/eDfVseKyTMDt+KGpeB35SkVn9Tyn0CqvVsY3EwI0v5i8nMHyJnzCIQf7nsy3p41TPkJZhg=="],
|
||||
|
||||
"is-bun-module/semver": ["semver@7.7.3", "", { "bin": { "semver": "bin/semver.js" } }, "sha512-SdsKMrI9TdgjdweUSR9MweHA4EJ8YxHn8DFaDisvhVlUOe4BF1tLD7GAj0lIqWVl+dPb/rExr0Btby5loQm20Q=="],
|
||||
|
||||
"micromatch/picomatch": ["picomatch@2.3.1", "", {}, "sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA=="],
|
||||
@ -2913,6 +2995,10 @@
|
||||
|
||||
"@typescript-eslint/typescript-estree/minimatch/brace-expansion": ["brace-expansion@2.0.2", "", { "dependencies": { "balanced-match": "^1.0.0" } }, "sha512-Jt0vHyM+jmUBqojB7E1NIYadt0vI0Qxjxd2TErW94wDz+E2LAm5vKMXXwg6ZZBTHPuUlDgQHKXvjGBdfcF1ZDQ=="],
|
||||
|
||||
"@workos-inc/authkit-nextjs/@workos-inc/node/iron-session": ["iron-session@6.3.1", "", { "dependencies": { "@peculiar/webcrypto": "^1.4.0", "@types/cookie": "^0.5.1", "@types/express": "^4.17.13", "@types/koa": "^2.13.5", "@types/node": "^17.0.41", "cookie": "^0.5.0", "iron-webcrypto": "^0.2.5" }, "peerDependencies": { "express": ">=4", "koa": ">=2", "next": ">=10" }, "optionalPeers": ["express", "koa", "next"] }, "sha512-3UJ7y2vk/WomAtEySmPgM6qtYF1cZ3tXuWX5GsVX4PJXAcs5y/sV9HuSfpjKS6HkTL/OhZcTDWJNLZ7w+Erx3A=="],
|
||||
|
||||
"@workos-inc/authkit-nextjs/@workos-inc/node/jose": ["jose@5.6.3", "", {}, "sha512-1Jh//hEEwMhNYPDDLwXHa2ePWgWiFNNUadVmguAAw2IJ6sj9mNxV5tGXJNqlMkJAybF6Lgw1mISDxTePP/187g=="],
|
||||
|
||||
"cliui/strip-ansi/ansi-regex": ["ansi-regex@6.2.2", "", {}, "sha512-Bq3SmSpyFHaWjPk8If9yc6svM8c56dB5BAtW4Qbw5jHTwwXXcTLoRMkpDJp6VL0XzlWaCHTXrkFURMYmD0sLqg=="],
|
||||
|
||||
"cloudflare/@types/node/undici-types": ["undici-types@5.26.5", "", {}, "sha512-JlCMO+ehdEIKqlFxk6IfVoAUVmgz7cU7zD/h9XZ0qzeosSHmUJVOzSQvvYSYWXkFXC+IfLKSIffhv0sVZup6pA=="],
|
||||
@ -3131,6 +3217,12 @@
|
||||
|
||||
"@smithy/core/@smithy/util-stream/@smithy/node-http-handler/@smithy/querystring-builder": ["@smithy/querystring-builder@4.2.8", "", { "dependencies": { "@smithy/types": "^4.12.0", "@smithy/util-uri-escape": "^4.2.0", "tslib": "^2.6.2" } }, "sha512-Xr83r31+DrE8CP3MqPgMJl+pQlLLmOfiEUnoyAlGzzJIrEsbKsPy1hqH0qySaQm4oWrCBlUqRt+idEgunKB+iw=="],
|
||||
|
||||
"@workos-inc/authkit-nextjs/@workos-inc/node/iron-session/@types/node": ["@types/node@17.0.45", "", {}, "sha512-w+tIMs3rq2afQdsPJlODhoUEKzFP1ayaoyl1CcnwtIlsVe7K7bA1NGm4s3PraqTLlXnbIN84zuBlxBWo1u9BLw=="],
|
||||
|
||||
"@workos-inc/authkit-nextjs/@workos-inc/node/iron-session/cookie": ["cookie@0.5.0", "", {}, "sha512-YZ3GUyn/o8gfKJlnlX7g7xq4gyO6OSuhGPKaaGssGB2qgDUS0gPgtTvoyZLTt9Ab6dC4hfc9dV5arkvc/OCmrw=="],
|
||||
|
||||
"@workos-inc/authkit-nextjs/@workos-inc/node/iron-session/iron-webcrypto": ["iron-webcrypto@0.2.8", "", { "dependencies": { "buffer": "^6" } }, "sha512-YPdCvjFMOBjXaYuDj5tiHst5CEk6Xw84Jo8Y2+jzhMceclAnb3+vNPP/CTtb5fO2ZEuXEaO4N+w62Vfko757KA=="],
|
||||
|
||||
"@aws-sdk/core/@smithy/smithy-client/@smithy/middleware-endpoint/@smithy/url-parser/@smithy/querystring-parser": ["@smithy/querystring-parser@4.2.8", "", { "dependencies": { "@smithy/types": "^4.12.0", "tslib": "^2.6.2" } }, "sha512-vUurovluVy50CUlazOiXkPq40KGvGWSdmusa3130MwrR1UNnNgKAlj58wlOe61XSHRpUfIIh6cE0zZ8mzKaDPA=="],
|
||||
|
||||
"@aws-sdk/core/@smithy/smithy-client/@smithy/util-stream/@smithy/fetch-http-handler/@smithy/querystring-builder": ["@smithy/querystring-builder@4.2.8", "", { "dependencies": { "@smithy/types": "^4.12.0", "@smithy/util-uri-escape": "^4.2.0", "tslib": "^2.6.2" } }, "sha512-Xr83r31+DrE8CP3MqPgMJl+pQlLLmOfiEUnoyAlGzzJIrEsbKsPy1hqH0qySaQm4oWrCBlUqRt+idEgunKB+iw=="],
|
||||
|
||||
@ -1,7 +1,7 @@
|
||||
import { defineConfig } from "drizzle-kit"
|
||||
|
||||
export default defineConfig({
|
||||
schema: "./src/db/schema.ts",
|
||||
schema: ["./src/db/schema.ts", "./src/db/schema-netsuite.ts"],
|
||||
out: "./drizzle",
|
||||
dialect: "sqlite",
|
||||
})
|
||||
|
||||
130
drizzle/0005_concerned_midnight.sql
Executable file
130
drizzle/0005_concerned_midnight.sql
Executable file
@ -0,0 +1,130 @@
|
||||
CREATE TABLE `credit_memos` (
|
||||
`id` text PRIMARY KEY NOT NULL,
|
||||
`netsuite_id` text,
|
||||
`customer_id` text NOT NULL,
|
||||
`project_id` text,
|
||||
`memo_number` text,
|
||||
`status` text DEFAULT 'draft' NOT NULL,
|
||||
`issue_date` text NOT NULL,
|
||||
`total` real DEFAULT 0 NOT NULL,
|
||||
`amount_applied` real DEFAULT 0 NOT NULL,
|
||||
`amount_remaining` real DEFAULT 0 NOT NULL,
|
||||
`memo` text,
|
||||
`line_items` text,
|
||||
`created_at` text NOT NULL,
|
||||
`updated_at` text NOT NULL,
|
||||
FOREIGN KEY (`customer_id`) REFERENCES `customers`(`id`) ON UPDATE no action ON DELETE no action,
|
||||
FOREIGN KEY (`project_id`) REFERENCES `projects`(`id`) ON UPDATE no action ON DELETE no action
|
||||
);
|
||||
--> statement-breakpoint
|
||||
CREATE TABLE `invoices` (
|
||||
`id` text PRIMARY KEY NOT NULL,
|
||||
`netsuite_id` text,
|
||||
`customer_id` text NOT NULL,
|
||||
`project_id` text,
|
||||
`invoice_number` text,
|
||||
`status` text DEFAULT 'draft' NOT NULL,
|
||||
`issue_date` text NOT NULL,
|
||||
`due_date` text,
|
||||
`subtotal` real DEFAULT 0 NOT NULL,
|
||||
`tax` real DEFAULT 0 NOT NULL,
|
||||
`total` real DEFAULT 0 NOT NULL,
|
||||
`amount_paid` real DEFAULT 0 NOT NULL,
|
||||
`amount_due` real DEFAULT 0 NOT NULL,
|
||||
`memo` text,
|
||||
`line_items` text,
|
||||
`created_at` text NOT NULL,
|
||||
`updated_at` text NOT NULL,
|
||||
FOREIGN KEY (`customer_id`) REFERENCES `customers`(`id`) ON UPDATE no action ON DELETE no action,
|
||||
FOREIGN KEY (`project_id`) REFERENCES `projects`(`id`) ON UPDATE no action ON DELETE no action
|
||||
);
|
||||
--> statement-breakpoint
|
||||
CREATE TABLE `netsuite_auth` (
|
||||
`id` text PRIMARY KEY NOT NULL,
|
||||
`account_id` text NOT NULL,
|
||||
`access_token_encrypted` text NOT NULL,
|
||||
`refresh_token_encrypted` text NOT NULL,
|
||||
`expires_in` integer NOT NULL,
|
||||
`token_type` text NOT NULL,
|
||||
`issued_at` integer NOT NULL,
|
||||
`created_at` text NOT NULL,
|
||||
`updated_at` text NOT NULL
|
||||
);
|
||||
--> statement-breakpoint
|
||||
CREATE TABLE `netsuite_sync_log` (
|
||||
`id` text PRIMARY KEY NOT NULL,
|
||||
`sync_type` text NOT NULL,
|
||||
`record_type` text NOT NULL,
|
||||
`direction` text NOT NULL,
|
||||
`status` text NOT NULL,
|
||||
`records_processed` integer DEFAULT 0 NOT NULL,
|
||||
`records_failed` integer DEFAULT 0 NOT NULL,
|
||||
`error_summary` text,
|
||||
`started_at` text NOT NULL,
|
||||
`completed_at` text,
|
||||
`created_at` text NOT NULL
|
||||
);
|
||||
--> statement-breakpoint
|
||||
CREATE TABLE `netsuite_sync_metadata` (
|
||||
`id` text PRIMARY KEY NOT NULL,
|
||||
`local_table` text NOT NULL,
|
||||
`local_record_id` text NOT NULL,
|
||||
`netsuite_record_type` text NOT NULL,
|
||||
`netsuite_internal_id` text,
|
||||
`last_synced_at` text,
|
||||
`last_modified_local` text,
|
||||
`last_modified_remote` text,
|
||||
`sync_status` text DEFAULT 'synced' NOT NULL,
|
||||
`conflict_data` text,
|
||||
`error_message` text,
|
||||
`retry_count` integer DEFAULT 0 NOT NULL,
|
||||
`created_at` text NOT NULL,
|
||||
`updated_at` text NOT NULL
|
||||
);
|
||||
--> statement-breakpoint
|
||||
CREATE TABLE `payments` (
|
||||
`id` text PRIMARY KEY NOT NULL,
|
||||
`netsuite_id` text,
|
||||
`customer_id` text,
|
||||
`vendor_id` text,
|
||||
`project_id` text,
|
||||
`payment_type` text NOT NULL,
|
||||
`amount` real NOT NULL,
|
||||
`payment_date` text NOT NULL,
|
||||
`payment_method` text,
|
||||
`reference_number` text,
|
||||
`memo` text,
|
||||
`created_at` text NOT NULL,
|
||||
`updated_at` text NOT NULL,
|
||||
FOREIGN KEY (`customer_id`) REFERENCES `customers`(`id`) ON UPDATE no action ON DELETE no action,
|
||||
FOREIGN KEY (`vendor_id`) REFERENCES `vendors`(`id`) ON UPDATE no action ON DELETE no action,
|
||||
FOREIGN KEY (`project_id`) REFERENCES `projects`(`id`) ON UPDATE no action ON DELETE no action
|
||||
);
|
||||
--> statement-breakpoint
|
||||
CREATE TABLE `vendor_bills` (
|
||||
`id` text PRIMARY KEY NOT NULL,
|
||||
`netsuite_id` text,
|
||||
`vendor_id` text NOT NULL,
|
||||
`project_id` text,
|
||||
`bill_number` text,
|
||||
`status` text DEFAULT 'pending' NOT NULL,
|
||||
`bill_date` text NOT NULL,
|
||||
`due_date` text,
|
||||
`subtotal` real DEFAULT 0 NOT NULL,
|
||||
`tax` real DEFAULT 0 NOT NULL,
|
||||
`total` real DEFAULT 0 NOT NULL,
|
||||
`amount_paid` real DEFAULT 0 NOT NULL,
|
||||
`amount_due` real DEFAULT 0 NOT NULL,
|
||||
`memo` text,
|
||||
`line_items` text,
|
||||
`created_at` text NOT NULL,
|
||||
`updated_at` text NOT NULL,
|
||||
FOREIGN KEY (`vendor_id`) REFERENCES `vendors`(`id`) ON UPDATE no action ON DELETE no action,
|
||||
FOREIGN KEY (`project_id`) REFERENCES `projects`(`id`) ON UPDATE no action ON DELETE no action
|
||||
);
|
||||
--> statement-breakpoint
|
||||
ALTER TABLE `customers` ADD `netsuite_id` text;--> statement-breakpoint
|
||||
ALTER TABLE `customers` ADD `updated_at` text;--> statement-breakpoint
|
||||
ALTER TABLE `projects` ADD `netsuite_job_id` text;--> statement-breakpoint
|
||||
ALTER TABLE `vendors` ADD `netsuite_id` text;--> statement-breakpoint
|
||||
ALTER TABLE `vendors` ADD `updated_at` text;
|
||||
85
drizzle/0006_brainy_vulcan.sql
Executable file
85
drizzle/0006_brainy_vulcan.sql
Executable file
@ -0,0 +1,85 @@
|
||||
CREATE TABLE `group_members` (
|
||||
`id` text PRIMARY KEY NOT NULL,
|
||||
`group_id` text NOT NULL,
|
||||
`user_id` text NOT NULL,
|
||||
`joined_at` text NOT NULL,
|
||||
FOREIGN KEY (`group_id`) REFERENCES `groups`(`id`) ON UPDATE no action ON DELETE cascade,
|
||||
FOREIGN KEY (`user_id`) REFERENCES `users`(`id`) ON UPDATE no action ON DELETE cascade
|
||||
);
|
||||
--> statement-breakpoint
|
||||
CREATE TABLE `groups` (
|
||||
`id` text PRIMARY KEY NOT NULL,
|
||||
`organization_id` text NOT NULL,
|
||||
`name` text NOT NULL,
|
||||
`description` text,
|
||||
`color` text,
|
||||
`created_at` text NOT NULL,
|
||||
FOREIGN KEY (`organization_id`) REFERENCES `organizations`(`id`) ON UPDATE no action ON DELETE cascade
|
||||
);
|
||||
--> statement-breakpoint
|
||||
CREATE TABLE `organization_members` (
|
||||
`id` text PRIMARY KEY NOT NULL,
|
||||
`organization_id` text NOT NULL,
|
||||
`user_id` text NOT NULL,
|
||||
`role` text NOT NULL,
|
||||
`joined_at` text NOT NULL,
|
||||
FOREIGN KEY (`organization_id`) REFERENCES `organizations`(`id`) ON UPDATE no action ON DELETE cascade,
|
||||
FOREIGN KEY (`user_id`) REFERENCES `users`(`id`) ON UPDATE no action ON DELETE cascade
|
||||
);
|
||||
--> statement-breakpoint
|
||||
CREATE TABLE `organizations` (
|
||||
`id` text PRIMARY KEY NOT NULL,
|
||||
`name` text NOT NULL,
|
||||
`slug` text NOT NULL,
|
||||
`type` text NOT NULL,
|
||||
`logo_url` text,
|
||||
`is_active` integer DEFAULT true NOT NULL,
|
||||
`created_at` text NOT NULL,
|
||||
`updated_at` text NOT NULL
|
||||
);
|
||||
--> statement-breakpoint
|
||||
CREATE UNIQUE INDEX `organizations_slug_unique` ON `organizations` (`slug`);--> statement-breakpoint
|
||||
CREATE TABLE `project_members` (
|
||||
`id` text PRIMARY KEY NOT NULL,
|
||||
`project_id` text NOT NULL,
|
||||
`user_id` text NOT NULL,
|
||||
`role` text NOT NULL,
|
||||
`assigned_at` text NOT NULL,
|
||||
FOREIGN KEY (`project_id`) REFERENCES `projects`(`id`) ON UPDATE no action ON DELETE cascade,
|
||||
FOREIGN KEY (`user_id`) REFERENCES `users`(`id`) ON UPDATE no action ON DELETE cascade
|
||||
);
|
||||
--> statement-breakpoint
|
||||
CREATE TABLE `team_members` (
|
||||
`id` text PRIMARY KEY NOT NULL,
|
||||
`team_id` text NOT NULL,
|
||||
`user_id` text NOT NULL,
|
||||
`joined_at` text NOT NULL,
|
||||
FOREIGN KEY (`team_id`) REFERENCES `teams`(`id`) ON UPDATE no action ON DELETE cascade,
|
||||
FOREIGN KEY (`user_id`) REFERENCES `users`(`id`) ON UPDATE no action ON DELETE cascade
|
||||
);
|
||||
--> statement-breakpoint
|
||||
CREATE TABLE `teams` (
|
||||
`id` text PRIMARY KEY NOT NULL,
|
||||
`organization_id` text NOT NULL,
|
||||
`name` text NOT NULL,
|
||||
`description` text,
|
||||
`created_at` text NOT NULL,
|
||||
FOREIGN KEY (`organization_id`) REFERENCES `organizations`(`id`) ON UPDATE no action ON DELETE cascade
|
||||
);
|
||||
--> statement-breakpoint
|
||||
CREATE TABLE `users` (
|
||||
`id` text PRIMARY KEY NOT NULL,
|
||||
`email` text NOT NULL,
|
||||
`first_name` text,
|
||||
`last_name` text,
|
||||
`display_name` text,
|
||||
`avatar_url` text,
|
||||
`role` text DEFAULT 'office' NOT NULL,
|
||||
`is_active` integer DEFAULT true NOT NULL,
|
||||
`last_login_at` text,
|
||||
`created_at` text NOT NULL,
|
||||
`updated_at` text NOT NULL
|
||||
);
|
||||
--> statement-breakpoint
|
||||
CREATE UNIQUE INDEX `users_email_unique` ON `users` (`email`);--> statement-breakpoint
|
||||
ALTER TABLE `projects` ADD `organization_id` text REFERENCES organizations(id);
|
||||
5
drizzle/0007_add_customer_fields.sql
Executable file
5
drizzle/0007_add_customer_fields.sql
Executable file
@ -0,0 +1,5 @@
|
||||
ALTER TABLE customers ADD COLUMN company text;
|
||||
--> statement-breakpoint
|
||||
ALTER TABLE customers ADD COLUMN address text;
|
||||
--> statement-breakpoint
|
||||
ALTER TABLE customers ADD COLUMN notes text;
|
||||
1559
drizzle/meta/0005_snapshot.json
Executable file
1559
drizzle/meta/0005_snapshot.json
Executable file
File diff suppressed because it is too large
Load Diff
2151
drizzle/meta/0006_snapshot.json
Executable file
2151
drizzle/meta/0006_snapshot.json
Executable file
File diff suppressed because it is too large
Load Diff
@ -36,6 +36,27 @@
|
||||
"when": 1769287376759,
|
||||
"tag": "0004_quick_firebrand",
|
||||
"breakpoints": true
|
||||
},
|
||||
{
|
||||
"idx": 5,
|
||||
"version": "6",
|
||||
"when": 1770235828892,
|
||||
"tag": "0005_concerned_midnight",
|
||||
"breakpoints": true
|
||||
},
|
||||
{
|
||||
"idx": 6,
|
||||
"version": "6",
|
||||
"when": 1770235964505,
|
||||
"tag": "0006_brainy_vulcan",
|
||||
"breakpoints": true
|
||||
},
|
||||
{
|
||||
"idx": 7,
|
||||
"version": "6",
|
||||
"when": 1770321600000,
|
||||
"tag": "0007_add_customer_fields",
|
||||
"breakpoints": true
|
||||
}
|
||||
]
|
||||
}
|
||||
57
drizzle/seed-users.sql
Executable file
57
drizzle/seed-users.sql
Executable file
@ -0,0 +1,57 @@
|
||||
-- seed users for development
|
||||
INSERT INTO users (id, email, first_name, last_name, display_name, avatar_url, role, is_active, last_login_at, created_at, updated_at)
|
||||
VALUES
|
||||
('user-1', 'admin@compass.io', 'Admin', 'User', 'Admin User', NULL, 'admin', 1, '2026-02-04T12:00:00Z', '2026-01-01T00:00:00Z', '2026-02-04T12:00:00Z'),
|
||||
('user-2', 'john@compass.io', 'John', 'Smith', 'John Smith', NULL, 'office', 1, '2026-02-03T10:30:00Z', '2026-01-15T00:00:00Z', '2026-02-03T10:30:00Z'),
|
||||
('user-3', 'sarah@compass.io', 'Sarah', 'Johnson', 'Sarah Johnson', NULL, 'office', 1, '2026-02-04T08:15:00Z', '2026-01-20T00:00:00Z', '2026-02-04T08:15:00Z'),
|
||||
('user-4', 'mike@compass.io', 'Mike', 'Wilson', 'Mike Wilson', NULL, 'field', 1, '2026-02-02T14:20:00Z', '2026-01-25T00:00:00Z', '2026-02-02T14:20:00Z'),
|
||||
('user-5', 'client@example.com', 'Jane', 'Client', 'Jane Client', NULL, 'client', 1, '2026-02-01T09:00:00Z', '2026-02-01T00:00:00Z', '2026-02-01T09:00:00Z');
|
||||
|
||||
-- seed organizations
|
||||
INSERT INTO organizations (id, name, slug, type, logo_url, is_active, created_at, updated_at)
|
||||
VALUES
|
||||
('org-1', 'Open Range Construction', 'open-range', 'internal', NULL, 1, '2026-01-01T00:00:00Z', '2026-01-01T00:00:00Z'),
|
||||
('org-2', 'Example Corp', 'example-corp', 'client', NULL, 1, '2026-02-01T00:00:00Z', '2026-02-01T00:00:00Z');
|
||||
|
||||
-- seed organization members
|
||||
INSERT INTO organization_members (id, organization_id, user_id, role, joined_at)
|
||||
VALUES
|
||||
('om-1', 'org-1', 'user-1', 'admin', '2026-01-01T00:00:00Z'),
|
||||
('om-2', 'org-1', 'user-2', 'office', '2026-01-15T00:00:00Z'),
|
||||
('om-3', 'org-1', 'user-3', 'office', '2026-01-20T00:00:00Z'),
|
||||
('om-4', 'org-1', 'user-4', 'field', '2026-01-25T00:00:00Z'),
|
||||
('om-5', 'org-2', 'user-5', 'client', '2026-02-01T00:00:00Z');
|
||||
|
||||
-- seed teams
|
||||
INSERT INTO teams (id, organization_id, name, description, created_at)
|
||||
VALUES
|
||||
('team-1', 'org-1', 'Engineering Team', 'Main engineering team', '2026-01-01T00:00:00Z'),
|
||||
('team-2', 'org-1', 'Field Crew Alpha', 'Field crew for site work', '2026-01-01T00:00:00Z');
|
||||
|
||||
-- seed team members
|
||||
INSERT INTO team_members (id, team_id, user_id, joined_at)
|
||||
VALUES
|
||||
('tm-1', 'team-1', 'user-2', '2026-01-15T00:00:00Z'),
|
||||
('tm-2', 'team-1', 'user-3', '2026-01-20T00:00:00Z'),
|
||||
('tm-3', 'team-2', 'user-4', '2026-01-25T00:00:00Z');
|
||||
|
||||
-- seed groups
|
||||
INSERT INTO groups (id, organization_id, name, description, color, created_at)
|
||||
VALUES
|
||||
('group-1', 'org-1', 'Project Managers', 'Project management group', '#3b82f6', '2026-01-01T00:00:00Z'),
|
||||
('group-2', 'org-1', 'Field Supervisors', 'Field supervision group', '#10b981', '2026-01-01T00:00:00Z');
|
||||
|
||||
-- seed group members
|
||||
INSERT INTO group_members (id, group_id, user_id, joined_at)
|
||||
VALUES
|
||||
('gm-1', 'group-1', 'user-2', '2026-01-15T00:00:00Z'),
|
||||
('gm-2', 'group-2', 'user-4', '2026-01-25T00:00:00Z');
|
||||
|
||||
-- seed project members (using existing project)
|
||||
INSERT INTO project_members (id, project_id, user_id, role, assigned_at)
|
||||
VALUES
|
||||
('pm-1', 'proj-o-001', 'user-1', 'admin', '2026-01-01T00:00:00Z'),
|
||||
('pm-2', 'proj-o-001', 'user-2', 'manager', '2026-01-15T00:00:00Z'),
|
||||
('pm-3', 'proj-o-001', 'user-4', 'crew', '2026-01-25T00:00:00Z'),
|
||||
('pm-4', 'proj-o-002', 'user-2', 'manager', '2026-01-16T00:00:00Z'),
|
||||
('pm-5', 'proj-o-003', 'user-3', 'manager', '2026-01-21T00:00:00Z');
|
||||
@ -51,6 +51,8 @@
|
||||
"@radix-ui/react-tooltip": "^1.2.8",
|
||||
"@tabler/icons-react": "^3.36.1",
|
||||
"@tanstack/react-table": "^8.21.3",
|
||||
"@workos-inc/authkit-nextjs": "^2.13.0",
|
||||
"@workos-inc/node": "^8.1.0",
|
||||
"class-variance-authority": "^0.7.1",
|
||||
"clsx": "^2.1.1",
|
||||
"cmdk": "^1.1.1",
|
||||
|
||||
@ -203,3 +203,13 @@
|
||||
[data-mobile="true"] [data-sidebar="content"]::-webkit-scrollbar {
|
||||
display: none;
|
||||
}
|
||||
|
||||
/* hide scrollbars globally while maintaining scroll functionality */
|
||||
* {
|
||||
scrollbar-width: none; /* firefox */
|
||||
-ms-overflow-style: none; /* IE and edge */
|
||||
}
|
||||
|
||||
*::-webkit-scrollbar {
|
||||
display: none; /* chrome, safari, opera */
|
||||
}
|
||||
32
src/components/ui/badge-indicator.tsx
Executable file
32
src/components/ui/badge-indicator.tsx
Executable file
@ -0,0 +1,32 @@
|
||||
import { cn } from "@/lib/utils"
|
||||
|
||||
interface BadgeIndicatorProps {
|
||||
children: React.ReactNode
|
||||
count?: number
|
||||
dot?: boolean
|
||||
className?: string
|
||||
}
|
||||
|
||||
export function BadgeIndicator({
|
||||
children,
|
||||
count,
|
||||
dot,
|
||||
className,
|
||||
}: BadgeIndicatorProps) {
|
||||
const showCount = count !== undefined && count > 0
|
||||
const showDot = dot && !showCount
|
||||
|
||||
return (
|
||||
<span className={cn("relative inline-flex", className)}>
|
||||
{children}
|
||||
{showCount && (
|
||||
<span className="absolute -right-1 -top-1 flex size-5 items-center justify-center rounded-full bg-destructive text-[10px] font-semibold text-destructive-foreground">
|
||||
{count > 99 ? "99+" : count}
|
||||
</span>
|
||||
)}
|
||||
{showDot && (
|
||||
<span className="absolute right-0 top-0 size-2 rounded-full bg-destructive ring-2 ring-background" />
|
||||
)}
|
||||
</span>
|
||||
)
|
||||
}
|
||||
81
src/components/ui/carousel-pages.tsx
Executable file
81
src/components/ui/carousel-pages.tsx
Executable file
@ -0,0 +1,81 @@
|
||||
"use client"
|
||||
|
||||
import * as React from "react"
|
||||
import useEmblaCarousel from "embla-carousel-react"
|
||||
import { cn } from "@/lib/utils"
|
||||
|
||||
interface CarouselPagesProps {
|
||||
children: React.ReactNode[]
|
||||
className?: string
|
||||
}
|
||||
|
||||
export function CarouselPages({ children, className }: CarouselPagesProps) {
|
||||
const [emblaRef, emblaApi] = useEmblaCarousel({
|
||||
loop: false,
|
||||
skipSnaps: false,
|
||||
watchDrag: (emblaApi, evt) => {
|
||||
// Don't capture drag events on interactive elements
|
||||
const target = evt.target as HTMLElement
|
||||
const interactiveElements = ['INPUT', 'SELECT', 'BUTTON', 'A', 'TEXTAREA']
|
||||
const isInteractive = interactiveElements.includes(target.tagName)
|
||||
const isSlider = target.closest('[role="slider"]')
|
||||
const isSwitch = target.closest('[role="switch"]')
|
||||
const isSelect = target.closest('[role="combobox"]') || target.closest('[role="listbox"]')
|
||||
|
||||
return !isInteractive && !isSlider && !isSwitch && !isSelect
|
||||
}
|
||||
})
|
||||
const [selectedIndex, setSelectedIndex] = React.useState(0)
|
||||
|
||||
const onSelect = React.useCallback(() => {
|
||||
if (!emblaApi) return
|
||||
setSelectedIndex(emblaApi.selectedScrollSnap())
|
||||
}, [emblaApi])
|
||||
|
||||
React.useEffect(() => {
|
||||
if (!emblaApi) return
|
||||
onSelect()
|
||||
emblaApi.on("select", onSelect)
|
||||
emblaApi.on("reInit", onSelect)
|
||||
return () => {
|
||||
emblaApi.off("select", onSelect)
|
||||
emblaApi.off("reInit", onSelect)
|
||||
}
|
||||
}, [emblaApi, onSelect])
|
||||
|
||||
return (
|
||||
<div className="flex flex-col gap-3">
|
||||
<div className="overflow-hidden" ref={emblaRef}>
|
||||
<div className="flex touch-pan-y">
|
||||
{children.map((child, index) => (
|
||||
<div
|
||||
key={index}
|
||||
className="min-w-0 shrink-0 grow-0 basis-full"
|
||||
>
|
||||
{child}
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{children.length > 1 && (
|
||||
<div className="flex justify-center gap-1.5 pb-1">
|
||||
{children.map((_, index) => (
|
||||
<button
|
||||
key={index}
|
||||
type="button"
|
||||
className={cn(
|
||||
"h-1.5 rounded-full transition-all",
|
||||
index === selectedIndex
|
||||
? "bg-primary w-6"
|
||||
: "bg-muted-foreground/30 w-1.5"
|
||||
)}
|
||||
onClick={() => emblaApi?.scrollTo(index)}
|
||||
aria-label={`Go to page ${index + 1}`}
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
@ -49,10 +49,13 @@ function CommandDialog({
|
||||
<DialogDescription>{description}</DialogDescription>
|
||||
</DialogHeader>
|
||||
<DialogContent
|
||||
className={cn("overflow-hidden p-0", className)}
|
||||
className={cn(
|
||||
"w-[calc(100%-2rem)] max-w-xl overflow-hidden p-0",
|
||||
className
|
||||
)}
|
||||
showCloseButton={showCloseButton}
|
||||
>
|
||||
<Command className="[&_[cmdk-group-heading]]:text-muted-foreground **:data-[slot=command-input-wrapper]:h-12 [&_[cmdk-group-heading]]:px-2 [&_[cmdk-group-heading]]:font-medium [&_[cmdk-group]]:px-2 [&_[cmdk-group]:not([hidden])_~[cmdk-group]]:pt-0 [&_[cmdk-input-wrapper]_svg]:h-5 [&_[cmdk-input-wrapper]_svg]:w-5 [&_[cmdk-input]]:h-12 [&_[cmdk-item]]:px-2 [&_[cmdk-item]]:py-3 [&_[cmdk-item]_svg]:h-5 [&_[cmdk-item]_svg]:w-5">
|
||||
<Command className="[&_[cmdk-group-heading]]:text-muted-foreground [&_[cmdk-group-heading]]:px-2 [&_[cmdk-group-heading]]:font-medium [&_[cmdk-group]]:px-2 [&_[cmdk-group]:not([hidden])_~[cmdk-group]]:pt-0 [&_[cmdk-input-wrapper]_svg]:h-4 [&_[cmdk-input-wrapper]_svg]:w-4 sm:[&_[cmdk-input-wrapper]_svg]:h-5 sm:[&_[cmdk-input-wrapper]_svg]:w-5 [&_[cmdk-input]]:h-10 sm:[&_[cmdk-input]]:h-12 [&_[cmdk-item]]:px-2 [&_[cmdk-item]]:py-2.5 sm:[&_[cmdk-item]]:py-3 [&_[cmdk-item]_svg]:h-4 [&_[cmdk-item]_svg]:w-4 sm:[&_[cmdk-item]_svg]:h-5 sm:[&_[cmdk-item]_svg]:w-5">
|
||||
{children}
|
||||
</Command>
|
||||
</DialogContent>
|
||||
@ -67,7 +70,7 @@ function CommandInput({
|
||||
return (
|
||||
<div
|
||||
data-slot="command-input-wrapper"
|
||||
className="flex h-9 items-center gap-2 border-b px-3"
|
||||
className="flex h-10 sm:h-12 items-center gap-2 border-b px-3"
|
||||
>
|
||||
<SearchIcon className="size-4 shrink-0 opacity-50" />
|
||||
<CommandPrimitive.Input
|
||||
@ -90,7 +93,7 @@ function CommandList({
|
||||
<CommandPrimitive.List
|
||||
data-slot="command-list"
|
||||
className={cn(
|
||||
"max-h-[300px] scroll-py-1 overflow-x-hidden overflow-y-auto",
|
||||
"max-h-[50vh] scroll-py-1 overflow-x-hidden overflow-y-auto",
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
|
||||
62
src/components/ui/date-picker.tsx
Executable file
62
src/components/ui/date-picker.tsx
Executable file
@ -0,0 +1,62 @@
|
||||
"use client"
|
||||
|
||||
import * as React from "react"
|
||||
import { format, parse } from "date-fns"
|
||||
import { CalendarIcon } from "lucide-react"
|
||||
|
||||
import { cn } from "@/lib/utils"
|
||||
import { Button } from "@/components/ui/button"
|
||||
import { Calendar } from "@/components/ui/calendar"
|
||||
import {
|
||||
Popover,
|
||||
PopoverContent,
|
||||
PopoverTrigger,
|
||||
} from "@/components/ui/popover"
|
||||
|
||||
interface DatePickerProps {
|
||||
value: string
|
||||
onChange: (value: string) => void
|
||||
placeholder?: string
|
||||
className?: string
|
||||
}
|
||||
|
||||
export function DatePicker({
|
||||
value,
|
||||
onChange,
|
||||
placeholder = "Pick a date",
|
||||
className,
|
||||
}: DatePickerProps) {
|
||||
const [open, setOpen] = React.useState(false)
|
||||
|
||||
const selected = value
|
||||
? parse(value, "yyyy-MM-dd", new Date())
|
||||
: undefined
|
||||
|
||||
return (
|
||||
<Popover open={open} onOpenChange={setOpen}>
|
||||
<PopoverTrigger asChild>
|
||||
<Button
|
||||
variant="outline"
|
||||
className={cn(
|
||||
"h-9 w-full justify-start text-left font-normal",
|
||||
!value && "text-muted-foreground",
|
||||
className
|
||||
)}
|
||||
>
|
||||
<CalendarIcon className="mr-2 size-3.5" />
|
||||
{selected ? format(selected, "PPP") : placeholder}
|
||||
</Button>
|
||||
</PopoverTrigger>
|
||||
<PopoverContent className="w-auto p-0" align="start">
|
||||
<Calendar
|
||||
mode="single"
|
||||
selected={selected}
|
||||
onSelect={(date) => {
|
||||
onChange(date ? format(date, "yyyy-MM-dd") : "")
|
||||
setOpen(false)
|
||||
}}
|
||||
/>
|
||||
</PopoverContent>
|
||||
</Popover>
|
||||
)
|
||||
}
|
||||
193
src/components/ui/responsive-dialog.tsx
Executable file
193
src/components/ui/responsive-dialog.tsx
Executable file
@ -0,0 +1,193 @@
|
||||
"use client"
|
||||
|
||||
import * as React from "react"
|
||||
import { useIsMobile } from "@/hooks/use-mobile"
|
||||
import { CarouselPages } from "@/components/ui/carousel-pages"
|
||||
import {
|
||||
Dialog,
|
||||
DialogContent,
|
||||
DialogDescription,
|
||||
DialogHeader,
|
||||
DialogTitle,
|
||||
} from "@/components/ui/dialog"
|
||||
import {
|
||||
Sheet,
|
||||
SheetContent,
|
||||
SheetDescription,
|
||||
SheetHeader,
|
||||
SheetTitle,
|
||||
} from "@/components/ui/sheet"
|
||||
import { cn } from "@/lib/utils"
|
||||
|
||||
interface ResponsiveDialogProps {
|
||||
open: boolean
|
||||
onOpenChange: (open: boolean) => void
|
||||
children: React.ReactNode
|
||||
title?: string
|
||||
description?: string
|
||||
className?: string
|
||||
/**
|
||||
* Use Sheet on mobile instead of Dialog
|
||||
* @default false - uses Dialog with mobile optimizations
|
||||
*/
|
||||
useSheetOnMobile?: boolean
|
||||
}
|
||||
|
||||
/**
|
||||
* A responsive dialog component based on the account modal pattern.
|
||||
* Provides consistent mobile and desktop experience with proper scrolling.
|
||||
*
|
||||
* Key features:
|
||||
* - Max height 90vh with scrollable content
|
||||
* - Optimized padding: p-4 on mobile, p-6 on desktop
|
||||
* - Consistent text sizing: title is text-base, description is text-xs
|
||||
* - Content spacing: space-y-3 with py-1
|
||||
*
|
||||
* @example
|
||||
* ```tsx
|
||||
* <ResponsiveDialog
|
||||
* open={open}
|
||||
* onOpenChange={setOpen}
|
||||
* title="Account Settings"
|
||||
* description="Manage your profile"
|
||||
* >
|
||||
* <ResponsiveDialogBody>
|
||||
* <form>...</form>
|
||||
* </ResponsiveDialogBody>
|
||||
* <ResponsiveDialogFooter>
|
||||
* <Button>Save</Button>
|
||||
* </ResponsiveDialogFooter>
|
||||
* </ResponsiveDialog>
|
||||
* ```
|
||||
*/
|
||||
export function ResponsiveDialog({
|
||||
open,
|
||||
onOpenChange,
|
||||
children,
|
||||
title,
|
||||
description,
|
||||
className,
|
||||
useSheetOnMobile = false,
|
||||
}: ResponsiveDialogProps) {
|
||||
const isMobile = useIsMobile()
|
||||
|
||||
if (isMobile && useSheetOnMobile) {
|
||||
return (
|
||||
<Sheet open={open} onOpenChange={onOpenChange}>
|
||||
<SheetContent
|
||||
side="bottom"
|
||||
className={cn(
|
||||
"max-h-[90vh] flex flex-col p-4 sm:p-6",
|
||||
className
|
||||
)}
|
||||
>
|
||||
{(title || description) && (
|
||||
<SheetHeader className="space-y-1 text-left shrink-0">
|
||||
{title && (
|
||||
<SheetTitle className="text-base">
|
||||
{title}
|
||||
</SheetTitle>
|
||||
)}
|
||||
{description && (
|
||||
<SheetDescription className="text-xs">
|
||||
{description}
|
||||
</SheetDescription>
|
||||
)}
|
||||
</SheetHeader>
|
||||
)}
|
||||
{children}
|
||||
</SheetContent>
|
||||
</Sheet>
|
||||
)
|
||||
}
|
||||
|
||||
return (
|
||||
<Dialog open={open} onOpenChange={onOpenChange}>
|
||||
<DialogContent
|
||||
className={cn(
|
||||
"w-[calc(100%-2rem)] max-w-md max-h-[90vh] flex flex-col p-4 sm:p-6",
|
||||
className
|
||||
)}
|
||||
>
|
||||
{(title || description) && (
|
||||
<DialogHeader className="space-y-1 shrink-0">
|
||||
{title && (
|
||||
<DialogTitle className="text-base">
|
||||
{title}
|
||||
</DialogTitle>
|
||||
)}
|
||||
{description && (
|
||||
<DialogDescription className="text-xs">
|
||||
{description}
|
||||
</DialogDescription>
|
||||
)}
|
||||
</DialogHeader>
|
||||
)}
|
||||
{children}
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
)
|
||||
}
|
||||
|
||||
/**
|
||||
* Body container for ResponsiveDialog content.
|
||||
* Scrollable when content exceeds available space.
|
||||
* On mobile, if pages prop is provided, renders as swipeable carousel.
|
||||
*/
|
||||
export function ResponsiveDialogBody({
|
||||
children,
|
||||
className,
|
||||
pages,
|
||||
}: {
|
||||
children?: React.ReactNode
|
||||
className?: string
|
||||
pages?: React.ReactNode[]
|
||||
}) {
|
||||
const isMobile = useIsMobile()
|
||||
|
||||
// If pages are provided and we're on mobile, use carousel
|
||||
if (pages && pages.length > 0 && isMobile) {
|
||||
return (
|
||||
<div className={cn("overflow-hidden flex-1 min-h-0 flex flex-col", className)}>
|
||||
<CarouselPages>
|
||||
{pages.map((page, index) => (
|
||||
<div key={index} className="space-y-4 py-2 px-1 overflow-y-auto">
|
||||
{page}
|
||||
</div>
|
||||
))}
|
||||
</CarouselPages>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
// Default: single scrollable container
|
||||
return (
|
||||
<div className={cn("space-y-2.5 py-1 overflow-y-auto flex-1 min-h-0", className)}>
|
||||
{children}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
/**
|
||||
* Footer with action buttons for ResponsiveDialog.
|
||||
* Buttons stack vertically on mobile (reverse order), horizontally on desktop.
|
||||
* Uses h-9 for buttons to match account modal.
|
||||
*/
|
||||
export function ResponsiveDialogFooter({
|
||||
children,
|
||||
className,
|
||||
}: {
|
||||
children: React.ReactNode
|
||||
className?: string
|
||||
}) {
|
||||
return (
|
||||
<div
|
||||
className={cn(
|
||||
"flex flex-col-reverse sm:flex-row sm:justify-end gap-2 pt-3 shrink-0",
|
||||
className
|
||||
)}
|
||||
>
|
||||
{children}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
@ -48,9 +48,11 @@ function SheetContent({
|
||||
className,
|
||||
children,
|
||||
side = "right",
|
||||
showClose = true,
|
||||
...props
|
||||
}: React.ComponentProps<typeof SheetPrimitive.Content> & {
|
||||
side?: "top" | "right" | "bottom" | "left"
|
||||
showClose?: boolean
|
||||
}) {
|
||||
return (
|
||||
<SheetPortal>
|
||||
@ -62,7 +64,7 @@ function SheetContent({
|
||||
side === "right" &&
|
||||
"data-[state=closed]:slide-out-to-right data-[state=open]:slide-in-from-right inset-y-0 right-0 h-full w-3/4 border-l sm:max-w-sm",
|
||||
side === "left" &&
|
||||
"data-[state=closed]:slide-out-to-left data-[state=open]:slide-in-from-left inset-y-0 left-0 h-full w-3/4 border-r sm:max-w-sm",
|
||||
"data-[state=closed]:slide-out-to-left data-[state=open]:slide-in-from-left inset-y-0 left-0 h-full w-3/4 sm:max-w-sm",
|
||||
side === "top" &&
|
||||
"data-[state=closed]:slide-out-to-top data-[state=open]:slide-in-from-top inset-x-0 top-0 h-auto border-b",
|
||||
side === "bottom" &&
|
||||
@ -72,10 +74,12 @@ function SheetContent({
|
||||
{...props}
|
||||
>
|
||||
{children}
|
||||
{showClose && (
|
||||
<SheetPrimitive.Close className="ring-offset-background focus:ring-ring data-[state=open]:bg-secondary absolute top-4 right-4 rounded-xs opacity-70 transition-opacity hover:opacity-100 focus:ring-2 focus:ring-offset-2 focus:outline-hidden disabled:pointer-events-none">
|
||||
<XIcon className="size-4" />
|
||||
<span className="sr-only">Close</span>
|
||||
</SheetPrimitive.Close>
|
||||
)}
|
||||
</SheetPrimitive.Content>
|
||||
</SheetPortal>
|
||||
)
|
||||
|
||||
@ -27,7 +27,7 @@ import {
|
||||
|
||||
const SIDEBAR_COOKIE_NAME = "sidebar_state"
|
||||
const SIDEBAR_COOKIE_MAX_AGE = 60 * 60 * 24 * 7
|
||||
const SIDEBAR_WIDTH = "16rem"
|
||||
const SIDEBAR_WIDTH = "11rem"
|
||||
const SIDEBAR_WIDTH_MOBILE = "18rem"
|
||||
const SIDEBAR_WIDTH_ICON = "3rem"
|
||||
const SIDEBAR_KEYBOARD_SHORTCUT = "b"
|
||||
@ -187,13 +187,9 @@ function Sidebar({
|
||||
data-sidebar="sidebar"
|
||||
data-slot="sidebar"
|
||||
data-mobile="true"
|
||||
className="bg-sidebar text-sidebar-foreground w-(--sidebar-width) p-0 [&>button]:text-sidebar-foreground [&>button]:opacity-80 [&>button]:hover:opacity-100"
|
||||
style={
|
||||
{
|
||||
"--sidebar-width": SIDEBAR_WIDTH_MOBILE,
|
||||
} as React.CSSProperties
|
||||
}
|
||||
className="bg-sidebar text-sidebar-foreground w-[85vw] max-w-none p-0 border-0"
|
||||
side={side}
|
||||
showClose={false}
|
||||
>
|
||||
<SheetHeader className="sr-only">
|
||||
<SheetTitle>Sidebar</SheetTitle>
|
||||
|
||||
@ -1,6 +1,9 @@
|
||||
import { drizzle } from "drizzle-orm/d1"
|
||||
import * as schema from "./schema"
|
||||
import * as netsuiteSchema from "./schema-netsuite"
|
||||
|
||||
const allSchemas = { ...schema, ...netsuiteSchema }
|
||||
|
||||
export function getDb(d1: D1Database) {
|
||||
return drizzle(d1, { schema })
|
||||
return drizzle(d1, { schema: allSchemas })
|
||||
}
|
||||
|
||||
153
src/db/schema-netsuite.ts
Executable file
153
src/db/schema-netsuite.ts
Executable file
@ -0,0 +1,153 @@
|
||||
import {
|
||||
sqliteTable,
|
||||
text,
|
||||
integer,
|
||||
real,
|
||||
} from "drizzle-orm/sqlite-core"
|
||||
import { projects, customers, vendors } from "./schema"
|
||||
|
||||
// oauth token storage (encrypted at rest)
|
||||
export const netsuiteAuth = sqliteTable("netsuite_auth", {
|
||||
id: text("id").primaryKey(),
|
||||
accountId: text("account_id").notNull(),
|
||||
accessTokenEncrypted: text("access_token_encrypted").notNull(),
|
||||
refreshTokenEncrypted: text("refresh_token_encrypted").notNull(),
|
||||
expiresIn: integer("expires_in").notNull(),
|
||||
tokenType: text("token_type").notNull(),
|
||||
issuedAt: integer("issued_at").notNull(),
|
||||
createdAt: text("created_at").notNull(),
|
||||
updatedAt: text("updated_at").notNull(),
|
||||
})
|
||||
|
||||
// per-record sync tracking
|
||||
export const netsuiteSyncMetadata = sqliteTable("netsuite_sync_metadata", {
|
||||
id: text("id").primaryKey(),
|
||||
localTable: text("local_table").notNull(),
|
||||
localRecordId: text("local_record_id").notNull(),
|
||||
netsuiteRecordType: text("netsuite_record_type").notNull(),
|
||||
netsuiteInternalId: text("netsuite_internal_id"),
|
||||
lastSyncedAt: text("last_synced_at"),
|
||||
lastModifiedLocal: text("last_modified_local"),
|
||||
lastModifiedRemote: text("last_modified_remote"),
|
||||
syncStatus: text("sync_status").notNull().default("synced"),
|
||||
conflictData: text("conflict_data"),
|
||||
errorMessage: text("error_message"),
|
||||
retryCount: integer("retry_count").notNull().default(0),
|
||||
createdAt: text("created_at").notNull(),
|
||||
updatedAt: text("updated_at").notNull(),
|
||||
})
|
||||
|
||||
// sync run history
|
||||
export const netsuiteSyncLog = sqliteTable("netsuite_sync_log", {
|
||||
id: text("id").primaryKey(),
|
||||
syncType: text("sync_type").notNull(),
|
||||
recordType: text("record_type").notNull(),
|
||||
direction: text("direction").notNull(),
|
||||
status: text("status").notNull(),
|
||||
recordsProcessed: integer("records_processed").notNull().default(0),
|
||||
recordsFailed: integer("records_failed").notNull().default(0),
|
||||
errorSummary: text("error_summary"),
|
||||
startedAt: text("started_at").notNull(),
|
||||
completedAt: text("completed_at"),
|
||||
createdAt: text("created_at").notNull(),
|
||||
})
|
||||
|
||||
// financial tables
|
||||
|
||||
export const invoices = sqliteTable("invoices", {
|
||||
id: text("id").primaryKey(),
|
||||
netsuiteId: text("netsuite_id"),
|
||||
customerId: text("customer_id")
|
||||
.notNull()
|
||||
.references(() => customers.id),
|
||||
projectId: text("project_id")
|
||||
.references(() => projects.id),
|
||||
invoiceNumber: text("invoice_number"),
|
||||
status: text("status").notNull().default("draft"),
|
||||
issueDate: text("issue_date").notNull(),
|
||||
dueDate: text("due_date"),
|
||||
subtotal: real("subtotal").notNull().default(0),
|
||||
tax: real("tax").notNull().default(0),
|
||||
total: real("total").notNull().default(0),
|
||||
amountPaid: real("amount_paid").notNull().default(0),
|
||||
amountDue: real("amount_due").notNull().default(0),
|
||||
memo: text("memo"),
|
||||
lineItems: text("line_items"),
|
||||
createdAt: text("created_at").notNull(),
|
||||
updatedAt: text("updated_at").notNull(),
|
||||
})
|
||||
|
||||
export const vendorBills = sqliteTable("vendor_bills", {
|
||||
id: text("id").primaryKey(),
|
||||
netsuiteId: text("netsuite_id"),
|
||||
vendorId: text("vendor_id")
|
||||
.notNull()
|
||||
.references(() => vendors.id),
|
||||
projectId: text("project_id")
|
||||
.references(() => projects.id),
|
||||
billNumber: text("bill_number"),
|
||||
status: text("status").notNull().default("pending"),
|
||||
billDate: text("bill_date").notNull(),
|
||||
dueDate: text("due_date"),
|
||||
subtotal: real("subtotal").notNull().default(0),
|
||||
tax: real("tax").notNull().default(0),
|
||||
total: real("total").notNull().default(0),
|
||||
amountPaid: real("amount_paid").notNull().default(0),
|
||||
amountDue: real("amount_due").notNull().default(0),
|
||||
memo: text("memo"),
|
||||
lineItems: text("line_items"),
|
||||
createdAt: text("created_at").notNull(),
|
||||
updatedAt: text("updated_at").notNull(),
|
||||
})
|
||||
|
||||
export const payments = sqliteTable("payments", {
|
||||
id: text("id").primaryKey(),
|
||||
netsuiteId: text("netsuite_id"),
|
||||
customerId: text("customer_id")
|
||||
.references(() => customers.id),
|
||||
vendorId: text("vendor_id")
|
||||
.references(() => vendors.id),
|
||||
projectId: text("project_id")
|
||||
.references(() => projects.id),
|
||||
paymentType: text("payment_type").notNull(),
|
||||
amount: real("amount").notNull(),
|
||||
paymentDate: text("payment_date").notNull(),
|
||||
paymentMethod: text("payment_method"),
|
||||
referenceNumber: text("reference_number"),
|
||||
memo: text("memo"),
|
||||
createdAt: text("created_at").notNull(),
|
||||
updatedAt: text("updated_at").notNull(),
|
||||
})
|
||||
|
||||
export const creditMemos = sqliteTable("credit_memos", {
|
||||
id: text("id").primaryKey(),
|
||||
netsuiteId: text("netsuite_id"),
|
||||
customerId: text("customer_id")
|
||||
.notNull()
|
||||
.references(() => customers.id),
|
||||
projectId: text("project_id")
|
||||
.references(() => projects.id),
|
||||
memoNumber: text("memo_number"),
|
||||
status: text("status").notNull().default("draft"),
|
||||
issueDate: text("issue_date").notNull(),
|
||||
total: real("total").notNull().default(0),
|
||||
amountApplied: real("amount_applied").notNull().default(0),
|
||||
amountRemaining: real("amount_remaining").notNull().default(0),
|
||||
memo: text("memo"),
|
||||
lineItems: text("line_items"),
|
||||
createdAt: text("created_at").notNull(),
|
||||
updatedAt: text("updated_at").notNull(),
|
||||
})
|
||||
|
||||
// type exports
|
||||
export type NetSuiteAuth = typeof netsuiteAuth.$inferSelect
|
||||
export type NetSuiteSyncMetadata = typeof netsuiteSyncMetadata.$inferSelect
|
||||
export type NetSuiteSyncLog = typeof netsuiteSyncLog.$inferSelect
|
||||
export type Invoice = typeof invoices.$inferSelect
|
||||
export type NewInvoice = typeof invoices.$inferInsert
|
||||
export type VendorBill = typeof vendorBills.$inferSelect
|
||||
export type NewVendorBill = typeof vendorBills.$inferInsert
|
||||
export type Payment = typeof payments.$inferSelect
|
||||
export type NewPayment = typeof payments.$inferInsert
|
||||
export type CreditMemo = typeof creditMemos.$inferSelect
|
||||
export type NewCreditMemo = typeof creditMemos.$inferInsert
|
||||
120
src/db/schema.ts
120
src/db/schema.ts
@ -4,6 +4,87 @@ import {
|
||||
integer,
|
||||
} from "drizzle-orm/sqlite-core"
|
||||
|
||||
// Auth and user management tables
|
||||
export const users = sqliteTable("users", {
|
||||
id: text("id").primaryKey(), // workos user id
|
||||
email: text("email").notNull().unique(),
|
||||
firstName: text("first_name"),
|
||||
lastName: text("last_name"),
|
||||
displayName: text("display_name"),
|
||||
avatarUrl: text("avatar_url"),
|
||||
role: text("role").notNull().default("office"), // admin, office, field, client
|
||||
isActive: integer("is_active", { mode: "boolean" }).notNull().default(true),
|
||||
lastLoginAt: text("last_login_at"),
|
||||
createdAt: text("created_at").notNull(),
|
||||
updatedAt: text("updated_at").notNull(),
|
||||
})
|
||||
|
||||
export const organizations = sqliteTable("organizations", {
|
||||
id: text("id").primaryKey(), // workos org id
|
||||
name: text("name").notNull(),
|
||||
slug: text("slug").notNull().unique(),
|
||||
type: text("type").notNull(), // "internal" or "client"
|
||||
logoUrl: text("logo_url"),
|
||||
isActive: integer("is_active", { mode: "boolean" }).notNull().default(true),
|
||||
createdAt: text("created_at").notNull(),
|
||||
updatedAt: text("updated_at").notNull(),
|
||||
})
|
||||
|
||||
export const organizationMembers = sqliteTable("organization_members", {
|
||||
id: text("id").primaryKey(),
|
||||
organizationId: text("organization_id")
|
||||
.notNull()
|
||||
.references(() => organizations.id, { onDelete: "cascade" }),
|
||||
userId: text("user_id")
|
||||
.notNull()
|
||||
.references(() => users.id, { onDelete: "cascade" }),
|
||||
role: text("role").notNull(),
|
||||
joinedAt: text("joined_at").notNull(),
|
||||
})
|
||||
|
||||
export const teams = sqliteTable("teams", {
|
||||
id: text("id").primaryKey(),
|
||||
organizationId: text("organization_id")
|
||||
.notNull()
|
||||
.references(() => organizations.id, { onDelete: "cascade" }),
|
||||
name: text("name").notNull(),
|
||||
description: text("description"),
|
||||
createdAt: text("created_at").notNull(),
|
||||
})
|
||||
|
||||
export const teamMembers = sqliteTable("team_members", {
|
||||
id: text("id").primaryKey(),
|
||||
teamId: text("team_id")
|
||||
.notNull()
|
||||
.references(() => teams.id, { onDelete: "cascade" }),
|
||||
userId: text("user_id")
|
||||
.notNull()
|
||||
.references(() => users.id, { onDelete: "cascade" }),
|
||||
joinedAt: text("joined_at").notNull(),
|
||||
})
|
||||
|
||||
export const groups = sqliteTable("groups", {
|
||||
id: text("id").primaryKey(),
|
||||
organizationId: text("organization_id")
|
||||
.notNull()
|
||||
.references(() => organizations.id, { onDelete: "cascade" }),
|
||||
name: text("name").notNull(),
|
||||
description: text("description"),
|
||||
color: text("color"), // hex color for badges
|
||||
createdAt: text("created_at").notNull(),
|
||||
})
|
||||
|
||||
export const groupMembers = sqliteTable("group_members", {
|
||||
id: text("id").primaryKey(),
|
||||
groupId: text("group_id")
|
||||
.notNull()
|
||||
.references(() => groups.id, { onDelete: "cascade" }),
|
||||
userId: text("user_id")
|
||||
.notNull()
|
||||
.references(() => users.id, { onDelete: "cascade" }),
|
||||
joinedAt: text("joined_at").notNull(),
|
||||
})
|
||||
|
||||
export const projects = sqliteTable("projects", {
|
||||
id: text("id").primaryKey(),
|
||||
name: text("name").notNull(),
|
||||
@ -11,9 +92,23 @@ export const projects = sqliteTable("projects", {
|
||||
address: text("address"),
|
||||
clientName: text("client_name"),
|
||||
projectManager: text("project_manager"),
|
||||
organizationId: text("organization_id").references(() => organizations.id),
|
||||
netsuiteJobId: text("netsuite_job_id"),
|
||||
createdAt: text("created_at").notNull(),
|
||||
})
|
||||
|
||||
export const projectMembers = sqliteTable("project_members", {
|
||||
id: text("id").primaryKey(),
|
||||
projectId: text("project_id")
|
||||
.notNull()
|
||||
.references(() => projects.id, { onDelete: "cascade" }),
|
||||
userId: text("user_id")
|
||||
.notNull()
|
||||
.references(() => users.id, { onDelete: "cascade" }),
|
||||
role: text("role").notNull(),
|
||||
assignedAt: text("assigned_at").notNull(),
|
||||
})
|
||||
|
||||
export const scheduleTasks = sqliteTable("schedule_tasks", {
|
||||
id: text("id").primaryKey(),
|
||||
projectId: text("project_id")
|
||||
@ -79,9 +174,14 @@ export const scheduleBaselines = sqliteTable("schedule_baselines", {
|
||||
export const customers = sqliteTable("customers", {
|
||||
id: text("id").primaryKey(),
|
||||
name: text("name").notNull(),
|
||||
company: text("company"),
|
||||
email: text("email"),
|
||||
phone: text("phone"),
|
||||
address: text("address"),
|
||||
notes: text("notes"),
|
||||
netsuiteId: text("netsuite_id"),
|
||||
createdAt: text("created_at").notNull(),
|
||||
updatedAt: text("updated_at"),
|
||||
})
|
||||
|
||||
export const vendors = sqliteTable("vendors", {
|
||||
@ -91,7 +191,9 @@ export const vendors = sqliteTable("vendors", {
|
||||
email: text("email"),
|
||||
phone: text("phone"),
|
||||
address: text("address"),
|
||||
netsuiteId: text("netsuite_id"),
|
||||
createdAt: text("created_at").notNull(),
|
||||
updatedAt: text("updated_at"),
|
||||
})
|
||||
|
||||
export type Project = typeof projects.$inferSelect
|
||||
@ -124,3 +226,21 @@ export type Vendor = typeof vendors.$inferSelect
|
||||
export type NewVendor = typeof vendors.$inferInsert
|
||||
export type Feedback = typeof feedback.$inferSelect
|
||||
export type NewFeedback = typeof feedback.$inferInsert
|
||||
|
||||
// Auth and user management types
|
||||
export type User = typeof users.$inferSelect
|
||||
export type NewUser = typeof users.$inferInsert
|
||||
export type Organization = typeof organizations.$inferSelect
|
||||
export type NewOrganization = typeof organizations.$inferInsert
|
||||
export type OrganizationMember = typeof organizationMembers.$inferSelect
|
||||
export type NewOrganizationMember = typeof organizationMembers.$inferInsert
|
||||
export type Team = typeof teams.$inferSelect
|
||||
export type NewTeam = typeof teams.$inferInsert
|
||||
export type TeamMember = typeof teamMembers.$inferSelect
|
||||
export type NewTeamMember = typeof teamMembers.$inferInsert
|
||||
export type Group = typeof groups.$inferSelect
|
||||
export type NewGroup = typeof groups.$inferInsert
|
||||
export type GroupMember = typeof groupMembers.$inferSelect
|
||||
export type NewGroupMember = typeof groupMembers.$inferInsert
|
||||
export type ProjectMember = typeof projectMembers.$inferSelect
|
||||
export type NewProjectMember = typeof projectMembers.$inferInsert
|
||||
|
||||
@ -4,3 +4,10 @@ import { twMerge } from "tailwind-merge"
|
||||
export function cn(...inputs: ClassValue[]) {
|
||||
return twMerge(clsx(inputs))
|
||||
}
|
||||
|
||||
export function formatCurrency(amount: number): string {
|
||||
return new Intl.NumberFormat("en-US", {
|
||||
style: "currency",
|
||||
currency: "USD",
|
||||
}).format(amount)
|
||||
}
|
||||
|
||||
@ -41,5 +41,8 @@
|
||||
"database_id": "cd6983ff-d286-4042-a823-6b2433c9fba7",
|
||||
"migrations_dir": "drizzle"
|
||||
}
|
||||
]
|
||||
],
|
||||
"vars": {
|
||||
"WORKOS_REDIRECT_URI": "https://compass.openrangeconstruction.ltd/api/auth/callback"
|
||||
}
|
||||
}
|
||||
Loading…
x
Reference in New Issue
Block a user