From 6dd4f9e9c35363d6a3437505bf22c376767b8996 Mon Sep 17 00:00:00 2001 From: kovakmedya Date: Thu, 30 Apr 2026 02:43:25 +0300 Subject: [PATCH] db: Appwrite schema (isletmem db, 11 tables, indexes) + SDK helpers - Created database 'isletmem' with 11 tables via Appwrite MCP: tenant_settings, customers, services, software, customer_software, calendar_events, tasks, finance_entries, invoices, invoice_items, audit_logs - All tables use rowSecurity=true; row-level perms scoped to Appwrite Team (tenant) - 18 indexes (composite on tenantId + queried columns; unique for invoice numbers, tenant_settings) - src/lib/appwrite/schema.ts as TS source of truth (DATABASE_ID, TABLES, row types) - src/lib/appwrite/client.ts (browser SDK) - src/lib/appwrite/server.ts (node-appwrite admin + session clients) - .env.example template, .env.local for dev (gitignored) - typecheck script added --- .env.example | 11 +++ .gitignore | 1 + package.json | 5 +- pnpm-lock.yaml | 75 +++++++++++++++++ src/lib/appwrite/client.ts | 19 +++++ src/lib/appwrite/schema.ts | 167 +++++++++++++++++++++++++++++++++++++ src/lib/appwrite/server.ts | 69 +++++++++++++++ 7 files changed, 346 insertions(+), 1 deletion(-) create mode 100644 .env.example create mode 100644 src/lib/appwrite/client.ts create mode 100644 src/lib/appwrite/schema.ts create mode 100644 src/lib/appwrite/server.ts diff --git a/.env.example b/.env.example new file mode 100644 index 0000000..cc9ce0c --- /dev/null +++ b/.env.example @@ -0,0 +1,11 @@ +# Appwrite (self-hosted, v1.9.0) +NEXT_PUBLIC_APPWRITE_ENDPOINT=https://db.kovaksoft.com/v1 +NEXT_PUBLIC_APPWRITE_PROJECT_ID= +NEXT_PUBLIC_APPWRITE_DATABASE_ID=isletmem + +# Server-only — Appwrite API key with sufficient scopes +# (databases.read/write, tables.read/write, users.read/write, teams.read/write) +APPWRITE_API_KEY= + +# App +APP_URL=http://localhost:3000 diff --git a/.gitignore b/.gitignore index cdb2380..e0604ba 100644 --- a/.gitignore +++ b/.gitignore @@ -32,6 +32,7 @@ yarn-error.log* # env files (can opt-in for committing if needed) .env* +!.env.example # vercel .vercel diff --git a/package.json b/package.json index f592dbf..200a31e 100644 --- a/package.json +++ b/package.json @@ -6,7 +6,8 @@ "dev": "next dev", "build": "next build", "start": "next start", - "lint": "next lint" + "lint": "next lint", + "typecheck": "tsc --noEmit" }, "dependencies": { "@dnd-kit/core": "^6.3.1", @@ -37,6 +38,7 @@ "@radix-ui/react-tooltip": "^1.2.8", "@tailwindcss/postcss": "^4.1.18", "@tanstack/react-table": "^8.21.3", + "appwrite": "^25.0.0", "class-variance-authority": "^0.7.1", "clsx": "^2.1.1", "cmdk": "^1.1.1", @@ -44,6 +46,7 @@ "lucide-react": "^0.562.0", "next": "16.1.1", "next-themes": "^0.4.6", + "node-appwrite": "^24.0.0", "postcss": "^8.5.6", "react": "19.2.3", "react-day-picker": "^9.13.0", diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 6abb01e..13c1910 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -92,6 +92,9 @@ importers: '@tanstack/react-table': specifier: ^8.21.3 version: 8.21.3(react-dom@19.2.3(react@19.2.3))(react@19.2.3) + appwrite: + specifier: ^25.0.0 + version: 25.0.0 class-variance-authority: specifier: ^0.7.1 version: 0.7.1 @@ -113,6 +116,9 @@ importers: next-themes: specifier: ^0.4.6 version: 0.4.6(react-dom@19.2.3(react@19.2.3))(react@19.2.3) + node-appwrite: + specifier: ^24.0.0 + version: 24.0.0 postcss: specifier: ^8.5.6 version: 8.5.6 @@ -395,89 +401,105 @@ packages: resolution: {integrity: sha512-excjX8DfsIcJ10x1Kzr4RcWe1edC9PquDRRPx3YVCvQv+U5p7Yin2s32ftzikXojb1PIFc/9Mt28/y+iRklkrw==} cpu: [arm64] os: [linux] + libc: [glibc] '@img/sharp-libvips-linux-arm@1.2.4': resolution: {integrity: sha512-bFI7xcKFELdiNCVov8e44Ia4u2byA+l3XtsAj+Q8tfCwO6BQ8iDojYdvoPMqsKDkuoOo+X6HZA0s0q11ANMQ8A==} cpu: [arm] os: [linux] + libc: [glibc] '@img/sharp-libvips-linux-ppc64@1.2.4': resolution: {integrity: sha512-FMuvGijLDYG6lW+b/UvyilUWu5Ayu+3r2d1S8notiGCIyYU/76eig1UfMmkZ7vwgOrzKzlQbFSuQfgm7GYUPpA==} cpu: [ppc64] os: [linux] + libc: [glibc] '@img/sharp-libvips-linux-riscv64@1.2.4': resolution: {integrity: sha512-oVDbcR4zUC0ce82teubSm+x6ETixtKZBh/qbREIOcI3cULzDyb18Sr/Wcyx7NRQeQzOiHTNbZFF1UwPS2scyGA==} cpu: [riscv64] os: [linux] + libc: [glibc] '@img/sharp-libvips-linux-s390x@1.2.4': resolution: {integrity: sha512-qmp9VrzgPgMoGZyPvrQHqk02uyjA0/QrTO26Tqk6l4ZV0MPWIW6LTkqOIov+J1yEu7MbFQaDpwdwJKhbJvuRxQ==} cpu: [s390x] os: [linux] + libc: [glibc] '@img/sharp-libvips-linux-x64@1.2.4': resolution: {integrity: sha512-tJxiiLsmHc9Ax1bz3oaOYBURTXGIRDODBqhveVHonrHJ9/+k89qbLl0bcJns+e4t4rvaNBxaEZsFtSfAdquPrw==} cpu: [x64] os: [linux] + libc: [glibc] '@img/sharp-libvips-linuxmusl-arm64@1.2.4': resolution: {integrity: sha512-FVQHuwx1IIuNow9QAbYUzJ+En8KcVm9Lk5+uGUQJHaZmMECZmOlix9HnH7n1TRkXMS0pGxIJokIVB9SuqZGGXw==} cpu: [arm64] os: [linux] + libc: [musl] '@img/sharp-libvips-linuxmusl-x64@1.2.4': resolution: {integrity: sha512-+LpyBk7L44ZIXwz/VYfglaX/okxezESc6UxDSoyo2Ks6Jxc4Y7sGjpgU9s4PMgqgjj1gZCylTieNamqA1MF7Dg==} cpu: [x64] os: [linux] + libc: [musl] '@img/sharp-linux-arm64@0.34.5': resolution: {integrity: sha512-bKQzaJRY/bkPOXyKx5EVup7qkaojECG6NLYswgktOZjaXecSAeCWiZwwiFf3/Y+O1HrauiE3FVsGxFg8c24rZg==} engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0} cpu: [arm64] os: [linux] + libc: [glibc] '@img/sharp-linux-arm@0.34.5': resolution: {integrity: sha512-9dLqsvwtg1uuXBGZKsxem9595+ujv0sJ6Vi8wcTANSFpwV/GONat5eCkzQo/1O6zRIkh0m/8+5BjrRr7jDUSZw==} engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0} cpu: [arm] os: [linux] + libc: [glibc] '@img/sharp-linux-ppc64@0.34.5': resolution: {integrity: sha512-7zznwNaqW6YtsfrGGDA6BRkISKAAE1Jo0QdpNYXNMHu2+0dTrPflTLNkpc8l7MUP5M16ZJcUvysVWWrMefZquA==} engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0} cpu: [ppc64] os: [linux] + libc: [glibc] '@img/sharp-linux-riscv64@0.34.5': resolution: {integrity: sha512-51gJuLPTKa7piYPaVs8GmByo7/U7/7TZOq+cnXJIHZKavIRHAP77e3N2HEl3dgiqdD/w0yUfiJnII77PuDDFdw==} engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0} cpu: [riscv64] os: [linux] + libc: [glibc] '@img/sharp-linux-s390x@0.34.5': resolution: {integrity: sha512-nQtCk0PdKfho3eC5MrbQoigJ2gd1CgddUMkabUj+rBevs8tZ2cULOx46E7oyX+04WGfABgIwmMC0VqieTiR4jg==} engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0} cpu: [s390x] os: [linux] + libc: [glibc] '@img/sharp-linux-x64@0.34.5': resolution: {integrity: sha512-MEzd8HPKxVxVenwAa+JRPwEC7QFjoPWuS5NZnBt6B3pu7EG2Ge0id1oLHZpPJdn3OQK+BQDiw9zStiHBTJQQQQ==} engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0} cpu: [x64] os: [linux] + libc: [glibc] '@img/sharp-linuxmusl-arm64@0.34.5': resolution: {integrity: sha512-fprJR6GtRsMt6Kyfq44IsChVZeGN97gTD331weR1ex1c1rypDEABN6Tm2xa1wE6lYb5DdEnk03NZPqA7Id21yg==} engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0} cpu: [arm64] os: [linux] + libc: [musl] '@img/sharp-linuxmusl-x64@0.34.5': resolution: {integrity: sha512-Jg8wNT1MUzIvhBFxViqrEhWDGzqymo3sV7z7ZsaWbZNDLXRJZoRGrjulp60YYtV4wfY8VIKcWidjojlLcWrd8Q==} engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0} cpu: [x64] os: [linux] + libc: [musl] '@img/sharp-wasm32@0.34.5': resolution: {integrity: sha512-OdWTEiVkY2PHwqkbBI8frFxQQFekHaSSkUIJkwzclWZe64O1X4UlUjqqqLaPbUpMOQk6FBu/HtlGXNblIs0huw==} @@ -544,24 +566,28 @@ packages: engines: {node: '>= 10'} cpu: [arm64] os: [linux] + libc: [glibc] '@next/swc-linux-arm64-musl@16.1.1': resolution: {integrity: sha512-MFHrgL4TXNQbBPzkKKur4Fb5ICEJa87HM7fczFs2+HWblM7mMLdco3dvyTI+QmLBU9xgns/EeeINSZD6Ar+oLg==} engines: {node: '>= 10'} cpu: [arm64] os: [linux] + libc: [musl] '@next/swc-linux-x64-gnu@16.1.1': resolution: {integrity: sha512-20bYDfgOQAPUkkKBnyP9PTuHiJGM7HzNBbuqmD0jiFVZ0aOldz+VnJhbxzjcSabYsnNjMPsE0cyzEudpYxsrUQ==} engines: {node: '>= 10'} cpu: [x64] os: [linux] + libc: [glibc] '@next/swc-linux-x64-musl@16.1.1': resolution: {integrity: sha512-9pRbK3M4asAHQRkwaXwu601oPZHghuSC8IXNENgbBSyImHv/zY4K5udBusgdHkvJ/Tcr96jJwQYOll0qU8+fPA==} engines: {node: '>= 10'} cpu: [x64] os: [linux] + libc: [musl] '@next/swc-win32-arm64-msvc@16.1.1': resolution: {integrity: sha512-bdfQkggaLgnmYrFkSQfsHfOhk/mCYmjnrbRCGgkMcoOBZ4n+TRRSLmT/CU5SATzlBJ9TpioUyBW/vWFXTqQRiA==} @@ -1230,24 +1256,28 @@ packages: engines: {node: '>= 10'} cpu: [arm64] os: [linux] + libc: [glibc] '@tailwindcss/oxide-linux-arm64-musl@4.1.18': resolution: {integrity: sha512-1px92582HkPQlaaCkdRcio71p8bc8i/ap5807tPRDK/uw953cauQBT8c5tVGkOwrHMfc2Yh6UuxaH4vtTjGvHg==} engines: {node: '>= 10'} cpu: [arm64] os: [linux] + libc: [musl] '@tailwindcss/oxide-linux-x64-gnu@4.1.18': resolution: {integrity: sha512-v3gyT0ivkfBLoZGF9LyHmts0Isc8jHZyVcbzio6Wpzifg/+5ZJpDiRiUhDLkcr7f/r38SWNe7ucxmGW3j3Kb/g==} engines: {node: '>= 10'} cpu: [x64] os: [linux] + libc: [glibc] '@tailwindcss/oxide-linux-x64-musl@4.1.18': resolution: {integrity: sha512-bhJ2y2OQNlcRwwgOAGMY0xTFStt4/wyU6pvI6LSuZpRgKQwxTec0/3Scu91O8ir7qCR3AuepQKLU/kX99FouqQ==} engines: {node: '>= 10'} cpu: [x64] os: [linux] + libc: [musl] '@tailwindcss/oxide-wasm32-wasi@4.1.18': resolution: {integrity: sha512-LffYTvPjODiP6PT16oNeUQJzNVyJl1cjIebq/rWWBF+3eDst5JGEFSc5cWxyRCJ0Mxl+KyIkqRxk1XPEs9x8TA==} @@ -1442,41 +1472,49 @@ packages: resolution: {integrity: sha512-34gw7PjDGB9JgePJEmhEqBhWvCiiWCuXsL9hYphDF7crW7UgI05gyBAi6MF58uGcMOiOqSJ2ybEeCvHcq0BCmQ==} cpu: [arm64] os: [linux] + libc: [glibc] '@unrs/resolver-binding-linux-arm64-musl@1.11.1': resolution: {integrity: sha512-RyMIx6Uf53hhOtJDIamSbTskA99sPHS96wxVE/bJtePJJtpdKGXO1wY90oRdXuYOGOTuqjT8ACccMc4K6QmT3w==} cpu: [arm64] os: [linux] + libc: [musl] '@unrs/resolver-binding-linux-ppc64-gnu@1.11.1': resolution: {integrity: sha512-D8Vae74A4/a+mZH0FbOkFJL9DSK2R6TFPC9M+jCWYia/q2einCubX10pecpDiTmkJVUH+y8K3BZClycD8nCShA==} cpu: [ppc64] os: [linux] + libc: [glibc] '@unrs/resolver-binding-linux-riscv64-gnu@1.11.1': resolution: {integrity: sha512-frxL4OrzOWVVsOc96+V3aqTIQl1O2TjgExV4EKgRY09AJ9leZpEg8Ak9phadbuX0BA4k8U5qtvMSQQGGmaJqcQ==} cpu: [riscv64] os: [linux] + libc: [glibc] '@unrs/resolver-binding-linux-riscv64-musl@1.11.1': resolution: {integrity: sha512-mJ5vuDaIZ+l/acv01sHoXfpnyrNKOk/3aDoEdLO/Xtn9HuZlDD6jKxHlkN8ZhWyLJsRBxfv9GYM2utQ1SChKew==} cpu: [riscv64] os: [linux] + libc: [musl] '@unrs/resolver-binding-linux-s390x-gnu@1.11.1': resolution: {integrity: sha512-kELo8ebBVtb9sA7rMe1Cph4QHreByhaZ2QEADd9NzIQsYNQpt9UkM9iqr2lhGr5afh885d/cB5QeTXSbZHTYPg==} cpu: [s390x] os: [linux] + libc: [glibc] '@unrs/resolver-binding-linux-x64-gnu@1.11.1': resolution: {integrity: sha512-C3ZAHugKgovV5YvAMsxhq0gtXuwESUKc5MhEtjBpLoHPLYM+iuwSj3lflFwK3DPm68660rZ7G8BMcwSro7hD5w==} cpu: [x64] os: [linux] + libc: [glibc] '@unrs/resolver-binding-linux-x64-musl@1.11.1': resolution: {integrity: sha512-rV0YSoyhK2nZ4vEswT/QwqzqQXw5I6CjoaYMOX0TqBlWhojUf8P94mvI7nuJTeaCkkds3QE4+zS8Ko+GdXuZtA==} cpu: [x64] os: [linux] + libc: [musl] '@unrs/resolver-binding-wasm32-wasi@1.11.1': resolution: {integrity: sha512-5u4RkfxJm+Ng7IWgkzi3qrFOvLvQYnPBmjmZQ8+szTK/b31fQCnleNl1GgEt7nIsZRIf5PLhPwT0WM+q45x/UQ==} @@ -1515,6 +1553,10 @@ packages: resolution: {integrity: sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==} engines: {node: '>=8'} + appwrite@25.0.0: + resolution: {integrity: sha512-ZoNhS02x4Bfcur8MQCnZvskrSciSKwKlL+FZOmGtLr6g+1FyEyZufEgOaynStOsqHSlmyUywvluWYH5eLda9Pw==} + engines: {node: '>=18.0.0'} + argparse@2.0.1: resolution: {integrity: sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==} @@ -1584,6 +1626,9 @@ packages: resolution: {integrity: sha512-Sg0xJUNDU1sJNGdfGWhVHX0kkZ+HWcvmVymJbj6NSgZZmW/8S9Y2HQ5euytnIgakgxN6papOAWiwDo1ctFDcoQ==} hasBin: true + bignumber.js@9.3.1: + resolution: {integrity: sha512-Ko0uX15oIUS7wJ3Rb30Fs6SkVbLmPBAKdlm7q9+ak9bbIeFf0MwuBsQV6z7+X768/cHsfg+WlysDWJcmthjsjQ==} + brace-expansion@1.1.12: resolution: {integrity: sha512-9T9UjW3r0UW5c1Q7GTwllptXwhvYmEzFhzMfZ9H7FQWt+uZePjZPjBP/W1ZEyZ1twGWom5/56TF4lPcqjnDHcg==} @@ -2245,6 +2290,9 @@ packages: engines: {node: '>=6'} hasBin: true + json-bigint@1.0.0: + resolution: {integrity: sha512-SiPv/8VpZuWbvLSMtTDU8hEfrZWg/mH/nV/b4o0CYbSxu1UIQPLdwKOCIyLQX+VIPO5vrLX3i8qtqFyhdPSUSQ==} + json-buffer@3.0.1: resolution: {integrity: sha512-4bV5BfR2mqfQTJm+V5tPPdf+ZpuhiIvTuAB5g8kcrXOZpTT/QwwVRWBywX1ozr6lEuPdbHxwaJlm9G6mI2sfSQ==} @@ -2316,24 +2364,28 @@ packages: engines: {node: '>= 12.0.0'} cpu: [arm64] os: [linux] + libc: [glibc] lightningcss-linux-arm64-musl@1.30.2: resolution: {integrity: sha512-5Vh9dGeblpTxWHpOx8iauV02popZDsCYMPIgiuw97OJ5uaDsL86cnqSFs5LZkG3ghHoX5isLgWzMs+eD1YzrnA==} engines: {node: '>= 12.0.0'} cpu: [arm64] os: [linux] + libc: [musl] lightningcss-linux-x64-gnu@1.30.2: resolution: {integrity: sha512-Cfd46gdmj1vQ+lR6VRTTadNHu6ALuw2pKR9lYq4FnhvgBc4zWY1EtZcAc6EffShbb1MFrIPfLDXD6Xprbnni4w==} engines: {node: '>= 12.0.0'} cpu: [x64] os: [linux] + libc: [glibc] lightningcss-linux-x64-musl@1.30.2: resolution: {integrity: sha512-XJaLUUFXb6/QG2lGIW6aIk6jKdtjtcffUT0NKvIqhSBY3hh9Ch+1LCeH80dR9q9LBjG3ewbDjnumefsLsP6aiA==} engines: {node: '>= 12.0.0'} cpu: [x64] os: [linux] + libc: [musl] lightningcss-win32-arm64-msvc@1.30.2: resolution: {integrity: sha512-FZn+vaj7zLv//D/192WFFVA0RgHawIcHqLX9xuWiQt7P0PtdFEVaxgF9rjM/IRYHQXNnk61/H/gb2Ei+kUQ4xQ==} @@ -2438,6 +2490,12 @@ packages: sass: optional: true + node-appwrite@24.0.0: + resolution: {integrity: sha512-6oPNGkNHMcBAcu2GmaOTl4uCrabEcin0NzHRlE8lBfsduG+o4MtYjdLoXlA0IdGtlkFFa+qA4Hmkb7Z7By0ySQ==} + + node-fetch-native-with-agent@1.7.2: + resolution: {integrity: sha512-5MaOOCuJEvcckoz7/tjdx1M6OusOY6Xc5f459IaruGStWnKzlI1qpNgaAwmn4LmFYcsSlj+jBMk84wmmRxfk5g==} + node-releases@2.0.27: resolution: {integrity: sha512-nmh3lCkYZ3grZvqcCH+fjmQ7X+H0OeZgP40OierEaAptX4XofMh5kwNbWh7lBduUzCcV/8kZ+NDLCwm2iorIlA==} @@ -4295,6 +4353,10 @@ snapshots: dependencies: color-convert: 2.0.1 + appwrite@25.0.0: + dependencies: + json-bigint: 1.0.0 + argparse@2.0.1: {} aria-hidden@1.2.6: @@ -4386,6 +4448,8 @@ snapshots: baseline-browser-mapping@2.9.11: {} + bignumber.js@9.3.1: {} + brace-expansion@1.1.12: dependencies: balanced-match: 1.0.2 @@ -5192,6 +5256,10 @@ snapshots: jsesc@3.1.0: {} + json-bigint@1.0.0: + dependencies: + bignumber.js: 9.3.1 + json-buffer@3.0.1: {} json-schema-traverse@0.4.1: {} @@ -5353,6 +5421,13 @@ snapshots: - '@babel/core' - babel-plugin-macros + node-appwrite@24.0.0: + dependencies: + json-bigint: 1.0.0 + node-fetch-native-with-agent: 1.7.2 + + node-fetch-native-with-agent@1.7.2: {} + node-releases@2.0.27: {} object-assign@4.1.1: {} diff --git a/src/lib/appwrite/client.ts b/src/lib/appwrite/client.ts new file mode 100644 index 0000000..24fd824 --- /dev/null +++ b/src/lib/appwrite/client.ts @@ -0,0 +1,19 @@ +import { Account, Avatars, Client, Databases, Storage, TablesDB, Teams } from "appwrite"; + +const endpoint = process.env.NEXT_PUBLIC_APPWRITE_ENDPOINT; +const projectId = process.env.NEXT_PUBLIC_APPWRITE_PROJECT_ID; + +if (!endpoint || !projectId) { + throw new Error( + "Missing NEXT_PUBLIC_APPWRITE_ENDPOINT or NEXT_PUBLIC_APPWRITE_PROJECT_ID. Check .env.local.", + ); +} + +export const client = new Client().setEndpoint(endpoint).setProject(projectId); + +export const account = new Account(client); +export const teams = new Teams(client); +export const databases = new Databases(client); +export const tablesDB = new TablesDB(client); +export const storage = new Storage(client); +export const avatars = new Avatars(client); diff --git a/src/lib/appwrite/schema.ts b/src/lib/appwrite/schema.ts new file mode 100644 index 0000000..f1280f4 --- /dev/null +++ b/src/lib/appwrite/schema.ts @@ -0,0 +1,167 @@ +import type { Models } from "appwrite"; + +export const DATABASE_ID = "isletmem"; + +export const TABLES = { + tenantSettings: "tenant_settings", + customers: "customers", + services: "services", + software: "software", + customerSoftware: "customer_software", + calendarEvents: "calendar_events", + tasks: "tasks", + financeEntries: "finance_entries", + invoices: "invoices", + invoiceItems: "invoice_items", + auditLogs: "audit_logs", +} as const; + +export type TableId = (typeof TABLES)[keyof typeof TABLES]; + +type Row = Models.Document; + +export type TenantRole = "owner" | "admin" | "member"; + +export interface TenantSettings extends Row { + tenantId: string; + companyName: string; + companyTaxId?: string; + companyAddress?: string; + companyEmail?: string; + companyPhone?: string; + logo?: string; + defaultVatRate?: number; + invoicePrefix?: string; + invoiceCounter?: number; +} + +export type CustomerStatus = "active" | "passive"; + +export interface Customer extends Row { + tenantId: string; + createdBy: string; + name: string; + email?: string; + phone?: string; + taxId?: string; + address?: string; + notes?: string; + status?: CustomerStatus; +} + +export type BillingPeriod = "monthly" | "yearly" | "onetime"; + +export interface Service extends Row { + tenantId: string; + createdBy: string; + customerId: string; + name: string; + description?: string; + unitPrice: number; + recurring?: boolean; + billingPeriod?: BillingPeriod; +} + +export interface Software extends Row { + tenantId: string; + createdBy: string; + name: string; + version?: string; + description?: string; + defaultFee?: number; +} + +export interface CustomerSoftware extends Row { + tenantId: string; + createdBy: string; + customerId: string; + softwareId: string; + startDate?: string; + endDate?: string; + fee?: number; + billingPeriod?: BillingPeriod; + notes?: string; +} + +export interface CalendarEvent extends Row { + tenantId: string; + createdBy: string; + title: string; + description?: string; + start: string; + end: string; + allDay?: boolean; + customerId?: string; + color?: string; +} + +export type TaskStatus = "backlog" | "todo" | "in_progress" | "done"; +export type TaskPriority = "low" | "medium" | "high" | "urgent"; + +export interface Task extends Row { + tenantId: string; + createdBy: string; + title: string; + description?: string; + status?: TaskStatus; + priority?: TaskPriority; + dueDate?: string; + assigneeId?: string; + customerId?: string; + order?: number; +} + +export type FinanceType = "income" | "expense" | "debt" | "receivable"; +export type PaymentMethod = "cash" | "transfer" | "card" | "check" | "other"; + +export interface FinanceEntry extends Row { + tenantId: string; + createdBy: string; + type: FinanceType; + amount: number; + date: string; + description?: string; + customerId?: string; + invoiceId?: string; + paymentMethod?: PaymentMethod; +} + +export type InvoiceStatus = "draft" | "sent" | "paid" | "overdue" | "cancelled"; + +export interface Invoice extends Row { + tenantId: string; + createdBy: string; + number: string; + customerId: string; + issueDate: string; + dueDate: string; + status?: InvoiceStatus; + subtotal?: number; + vatTotal?: number; + total?: number; + notes?: string; +} + +export interface InvoiceItem extends Row { + tenantId: string; + createdBy: string; + invoiceId: string; + description: string; + quantity: number; + unitPrice: number; + vatRate?: number; + lineTotal: number; +} + +export type AuditAction = "create" | "update" | "delete"; + +export interface AuditLog extends Row { + tenantId: string; + userId: string; + action: AuditAction; + entityType: string; + entityId: string; + changes?: string; + ipAddress?: string; + userAgent?: string; +} diff --git a/src/lib/appwrite/server.ts b/src/lib/appwrite/server.ts new file mode 100644 index 0000000..c4282d4 --- /dev/null +++ b/src/lib/appwrite/server.ts @@ -0,0 +1,69 @@ +import "server-only"; + +import { cookies } from "next/headers"; +import { + Account, + Client, + Databases, + Storage, + TablesDB, + Teams, + Users, +} from "node-appwrite"; + +const endpoint = process.env.NEXT_PUBLIC_APPWRITE_ENDPOINT; +const projectId = process.env.NEXT_PUBLIC_APPWRITE_PROJECT_ID; +const apiKey = process.env.APPWRITE_API_KEY; + +if (!endpoint || !projectId) { + throw new Error( + "Missing NEXT_PUBLIC_APPWRITE_ENDPOINT or NEXT_PUBLIC_APPWRITE_PROJECT_ID. Check .env.local.", + ); +} + +export const APPWRITE_SESSION_COOKIE = "isletmem-session"; + +function baseClient() { + return new Client().setEndpoint(endpoint!).setProject(projectId!); +} + +export function createAdminClient() { + if (!apiKey) { + throw new Error("Missing APPWRITE_API_KEY. Required for admin operations."); + } + const client = baseClient().setKey(apiKey); + return { + client, + account: new Account(client), + teams: new Teams(client), + users: new Users(client), + databases: new Databases(client), + tablesDB: new TablesDB(client), + storage: new Storage(client), + }; +} + +export async function createSessionClient() { + const session = (await cookies()).get(APPWRITE_SESSION_COOKIE); + if (!session?.value) { + throw new Error("No active session."); + } + const client = baseClient().setSession(session.value); + return { + client, + account: new Account(client), + teams: new Teams(client), + databases: new Databases(client), + tablesDB: new TablesDB(client), + storage: new Storage(client), + }; +} + +export async function getCurrentUser() { + try { + const { account } = await createSessionClient(); + return await account.get(); + } catch { + return null; + } +}