create-devoplus-one-app 0.0.0-stage → 0.1.0-next.158

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (122) hide show
  1. package/README.md +62 -2
  2. package/dist/cli.mjs +17 -0
  3. package/package.json +35 -3
  4. package/templates/0001_init.sql.tpl +10 -0
  5. package/templates/AGENTS.md.tpl +286 -0
  6. package/templates/AGENT_GUIDE.md.tpl +36 -0
  7. package/templates/README.md.tpl +58 -0
  8. package/templates/_shared/AGENTS.writes.md.tpl +38 -0
  9. package/templates/_shared/README.tail.md.tpl +20 -0
  10. package/templates/_shared/files/src/app.test.ts.tpl +36 -0
  11. package/templates/_shared/files/src/index.ts.tpl +8 -0
  12. package/templates/_shared/files/src/lib/client.ts.tpl +213 -0
  13. package/templates/_shared/files/src/lib/files.ts.tpl +37 -0
  14. package/templates/_shared/files/src/lib/http.ts.tpl +101 -0
  15. package/templates/_shared/files/src/lib/mail.ts.tpl +26 -0
  16. package/templates/_shared/files/src/lib/model.ts.tpl +31 -0
  17. package/templates/_shared/files/src/lib/page.ts.tpl +71 -0
  18. package/templates/_shared/files/src/lib/record.ts.tpl +246 -0
  19. package/templates/_shared/files/src/lib/server.ts.tpl +132 -0
  20. package/templates/_shared/files/src/shell.test.ts.tpl +56 -0
  21. package/templates/agent.ts.tpl +23 -0
  22. package/templates/bakim-takvimi/AGENTS.template.md.tpl +40 -0
  23. package/templates/bakim-takvimi/README.md.tpl +45 -0
  24. package/templates/bakim-takvimi/files/AGENT_GUIDE.md.tpl +52 -0
  25. package/templates/bakim-takvimi/files/one.data.json +45 -0
  26. package/templates/bakim-takvimi/files/src/agent.ts.tpl +22 -0
  27. package/templates/bakim-takvimi/files/src/app.ts.tpl +259 -0
  28. package/templates/bakim-takvimi/files/src/jobs.ts.tpl +170 -0
  29. package/templates/bakim-takvimi/files/src/names.ts.tpl +5 -0
  30. package/templates/bakim-takvimi/files/src/queries.ts.tpl +71 -0
  31. package/templates/bakim-takvimi/files/src/schedule.test.ts.tpl +58 -0
  32. package/templates/bakim-takvimi/files/src/schedule.ts.tpl +106 -0
  33. package/templates/bakim-takvimi/files/src/shell.ts.tpl +299 -0
  34. package/templates/bakim-takvimi/manifest.json.tpl +64 -0
  35. package/templates/egitim-sertifika/AGENTS.template.md.tpl +41 -0
  36. package/templates/egitim-sertifika/README.md.tpl +46 -0
  37. package/templates/egitim-sertifika/files/AGENT_GUIDE.md.tpl +53 -0
  38. package/templates/egitim-sertifika/files/one.data.json +43 -0
  39. package/templates/egitim-sertifika/files/src/agent.ts.tpl +22 -0
  40. package/templates/egitim-sertifika/files/src/app.ts.tpl +312 -0
  41. package/templates/egitim-sertifika/files/src/certificate.test.ts.tpl +83 -0
  42. package/templates/egitim-sertifika/files/src/certificate.ts.tpl +163 -0
  43. package/templates/egitim-sertifika/files/src/jobs.ts.tpl +219 -0
  44. package/templates/egitim-sertifika/files/src/names.ts.tpl +5 -0
  45. package/templates/egitim-sertifika/files/src/queries.ts.tpl +115 -0
  46. package/templates/egitim-sertifika/files/src/shell.ts.tpl +345 -0
  47. package/templates/egitim-sertifika/manifest.json.tpl +57 -0
  48. package/templates/envanter/AGENTS.template.md.tpl +28 -0
  49. package/templates/envanter/README.md.tpl +40 -0
  50. package/templates/envanter/files/AGENT_GUIDE.md.tpl +47 -0
  51. package/templates/envanter/files/one.data.json +62 -0
  52. package/templates/envanter/files/src/agent.ts.tpl +20 -0
  53. package/templates/envanter/files/src/app.ts.tpl +204 -0
  54. package/templates/envanter/files/src/jobs.ts.tpl +30 -0
  55. package/templates/envanter/files/src/queries.ts.tpl +53 -0
  56. package/templates/envanter/files/src/shell.ts.tpl +265 -0
  57. package/templates/envanter/files/src/stock.test.ts.tpl +31 -0
  58. package/templates/envanter/files/src/stock.ts.tpl +33 -0
  59. package/templates/envanter/manifest.json.tpl +56 -0
  60. package/templates/hedef-takibi/AGENTS.template.md.tpl +37 -0
  61. package/templates/hedef-takibi/README.md.tpl +52 -0
  62. package/templates/hedef-takibi/files/AGENT_GUIDE.md.tpl +48 -0
  63. package/templates/hedef-takibi/files/one.data.json +76 -0
  64. package/templates/hedef-takibi/files/src/agent.ts.tpl +26 -0
  65. package/templates/hedef-takibi/files/src/app.ts.tpl +740 -0
  66. package/templates/hedef-takibi/files/src/progress.test.ts.tpl +53 -0
  67. package/templates/hedef-takibi/files/src/progress.ts.tpl +81 -0
  68. package/templates/hedef-takibi/files/src/queries.ts.tpl +83 -0
  69. package/templates/hedef-takibi/files/src/shell.ts.tpl +455 -0
  70. package/templates/hedef-takibi/manifest.json.tpl +38 -0
  71. package/templates/index.ts.tpl +262 -0
  72. package/templates/markdown.d.ts.tpl +6 -0
  73. package/templates/musteri-ziyaret/AGENTS.template.md.tpl +47 -0
  74. package/templates/musteri-ziyaret/README.md.tpl +48 -0
  75. package/templates/musteri-ziyaret/files/AGENT_GUIDE.md.tpl +44 -0
  76. package/templates/musteri-ziyaret/files/one.data.json +53 -0
  77. package/templates/musteri-ziyaret/files/src/agent.ts.tpl +21 -0
  78. package/templates/musteri-ziyaret/files/src/app.ts.tpl +340 -0
  79. package/templates/musteri-ziyaret/files/src/customers.ts.tpl +85 -0
  80. package/templates/musteri-ziyaret/files/src/queries.ts.tpl +67 -0
  81. package/templates/musteri-ziyaret/files/src/shell.ts.tpl +272 -0
  82. package/templates/musteri-ziyaret/files/src/visit.test.ts.tpl +65 -0
  83. package/templates/musteri-ziyaret/files/src/visit.ts.tpl +98 -0
  84. package/templates/musteri-ziyaret/manifest.json.tpl +37 -0
  85. package/templates/saha-denetimi/AGENTS.template.md.tpl +32 -0
  86. package/templates/saha-denetimi/README.md.tpl +44 -0
  87. package/templates/saha-denetimi/files/AGENT_GUIDE.md.tpl +55 -0
  88. package/templates/saha-denetimi/files/one.data.json +87 -0
  89. package/templates/saha-denetimi/files/src/agent.ts.tpl +41 -0
  90. package/templates/saha-denetimi/files/src/app.ts.tpl +444 -0
  91. package/templates/saha-denetimi/files/src/queries.ts.tpl +143 -0
  92. package/templates/saha-denetimi/files/src/score.test.ts.tpl +48 -0
  93. package/templates/saha-denetimi/files/src/score.ts.tpl +102 -0
  94. package/templates/saha-denetimi/files/src/shell.ts.tpl +585 -0
  95. package/templates/saha-denetimi/manifest.json.tpl +49 -0
  96. package/templates/shell.test.ts.tpl +30 -0
  97. package/templates/shell.ts.tpl +201 -0
  98. package/templates/uygunsuzluk-dof/AGENTS.template.md.tpl +40 -0
  99. package/templates/uygunsuzluk-dof/README.md.tpl +46 -0
  100. package/templates/uygunsuzluk-dof/files/AGENT_GUIDE.md.tpl +56 -0
  101. package/templates/uygunsuzluk-dof/files/one.data.json +99 -0
  102. package/templates/uygunsuzluk-dof/files/src/agent.ts.tpl +45 -0
  103. package/templates/uygunsuzluk-dof/files/src/app.ts.tpl +431 -0
  104. package/templates/uygunsuzluk-dof/files/src/capa.test.ts.tpl +53 -0
  105. package/templates/uygunsuzluk-dof/files/src/capa.ts.tpl +114 -0
  106. package/templates/uygunsuzluk-dof/files/src/jobs.ts.tpl +127 -0
  107. package/templates/uygunsuzluk-dof/files/src/queries.ts.tpl +102 -0
  108. package/templates/uygunsuzluk-dof/files/src/shell.ts.tpl +441 -0
  109. package/templates/uygunsuzluk-dof/manifest.json.tpl +56 -0
  110. package/templates/ziyaretci-kaydi/AGENTS.template.md.tpl +35 -0
  111. package/templates/ziyaretci-kaydi/README.md.tpl +46 -0
  112. package/templates/ziyaretci-kaydi/files/AGENT_GUIDE.md.tpl +39 -0
  113. package/templates/ziyaretci-kaydi/files/one.data.json +42 -0
  114. package/templates/ziyaretci-kaydi/files/src/agent.ts.tpl +13 -0
  115. package/templates/ziyaretci-kaydi/files/src/app.ts.tpl +293 -0
  116. package/templates/ziyaretci-kaydi/files/src/jobs.ts.tpl +49 -0
  117. package/templates/ziyaretci-kaydi/files/src/notice.ts.tpl +48 -0
  118. package/templates/ziyaretci-kaydi/files/src/queries.ts.tpl +45 -0
  119. package/templates/ziyaretci-kaydi/files/src/shell.ts.tpl +252 -0
  120. package/templates/ziyaretci-kaydi/files/src/visitor.test.ts.tpl +34 -0
  121. package/templates/ziyaretci-kaydi/files/src/visitor.ts.tpl +57 -0
  122. package/templates/ziyaretci-kaydi/manifest.json.tpl +38 -0
@@ -0,0 +1,262 @@
1
+ /// <reference path="./markdown.d.ts" />
2
+ // {{nameTr}} — Devoplus One uygulaması (iskelet: create-devoplus-one-app). Uçlar:
3
+ // GET / kabuk (sayfa; manifest `navigation` yolları da kabuğu döndürür — sekmeleri Portal çizer)
4
+ // GET /shell.js kabuğun betiği (satır içi betik platformda kapalıdır)
5
+ // GET /one-bridge.js · /one-ui.css Devoplus One köprüsü ve görsel dili (SDK clientAssetResponse)
6
+ // GET /api/me oturumdaki kullanıcı: dispatch bağlamı + CA Users/me
7
+ // GET /api/notes bu kurulumun veritabanındaki notlarım (PortalId kolonu YOK — veritabanı zaten bu portala aittir, D-17)
8
+ // POST /api/notes not ekle { text }
9
+ // DELETE /api/notes/{id} notumu sil
10
+ {{#files}}
11
+ // GET/POST /api/files · GET/DELETE /api/files/{key} bu kurulumun dosya deposu (SDK OneFiles; env.FILES) — kullanıcı yalnız kendi dosyalarını görür
12
+ {{/files}}
13
+ {{#cache}}
14
+ // GET/PUT/DELETE /api/cache bu kurulumun önbelleği (SDK OneCache; env.CACHE)
15
+ {{/cache}}
16
+ // POST /__one/agent/list-notes Devoplus yapay zekâsının aracı — YALNIZ platformun imzalı yolundan (src/agent.ts)
17
+ // GET /.well-known/one-agent.json · one-agent.md yapay zekâ kataloğu (manifest `agent`) ve rehberi (AGENT_GUIDE.md)
18
+ // Kimlik X-One-* başlıklarından OKUNUR (dispatch yazar); bu dosya o başlıkları asla üretmez.
19
+ import {
20
+ BackendClient,
21
+ agentManifestResponse,
22
+ clientAssetResponse,
23
+ handleAgentRequest,
24
+ navigationFromManifest,
25
+ scriptResponse,
26
+ {{#cache}}
27
+ OneCache,
28
+ {{/cache}}
29
+ {{#files}}
30
+ OneFiles,
31
+ {{/files}}
32
+ logAudit,
33
+ problemResponse,
34
+ readBoundedBody,
35
+ readContext,
36
+ requireUser,
37
+ {{#files}}
38
+ sanitizeFileName,
39
+ {{/files}}
40
+ type OneApp,
41
+ {{#cache}}
42
+ type OneCacheBinding,
43
+ {{/cache}}
44
+ type OneContext,
45
+ type OneDatabase,
46
+ {{#files}}
47
+ type OneFilesBinding,
48
+ {{/files}}
49
+ } from '@devoplus/one-sdk'
50
+ import manifest from '../one.manifest.json' with { type: 'json' }
51
+ import guide from '../AGENT_GUIDE.md' with { type: 'text' }
52
+ import { agentTools } from './agent.ts'
53
+ import { normalizeLang, normalizeTheme, renderShell, renderShellScript } from './shell.ts'
54
+
55
+ // Menü manifest'ten gelir (tek kaynak): Portal sekmeleri çizer, sayfa betiği bir kez üretilir.
56
+ const NAVIGATION = navigationFromManifest(manifest)
57
+ // Salt-okunur uyarısı manifestin Vekalet / Yerine Görüntüle kipine uyar (actingSessions: "write" ise gösterilmez).
58
+ const SHELL_SCRIPT = renderShellScript(NAVIGATION, {
59
+ actingWrites: (manifest as { actingSessions?: string }).actingSessions === 'write',
60
+ })
61
+ const isPage = (pathname: string): boolean => pathname === '/' || NAVIGATION.some((item) => item.path === pathname)
62
+
63
+ interface Env {
64
+ /** Bu kurulumun veritabanı — yalnız bu portala aittir (D-17); tabloya PortalId eklenmez. */
65
+ DB: OneDatabase
66
+ {{#files}}
67
+ /** Bu kurulumun dosya deposu (manifest services.files → env.FILES). */
68
+ FILES: OneFilesBinding
69
+ {{/files}}
70
+ {{#cache}}
71
+ /** Bu kurulumun önbelleği (manifest services.cache → env.CACHE). TTL zorunludur; kalıcı veri için DEĞİL. */
72
+ CACHE: OneCacheBinding
73
+ {{/cache}}
74
+ /** CA backend tabanı; one-ci `vars` olarak yazar ve outbound allowlist'indedir. */
75
+ API_BASE: string
76
+ }
77
+
78
+ interface MeResponse {
79
+ id: string
80
+ firstName?: string
81
+ lastName?: string
82
+ email?: string
83
+ }
84
+
85
+ interface NoteRow {
86
+ id: number
87
+ text: string
88
+ createdAt: string
89
+ }
90
+
91
+ const NOTE_MAX_LENGTH = 500
92
+ const NOTE_BODY_MAX_BYTES = 16 * 1024
93
+ {{#files}}
94
+ const UPLOAD_PREFIX = 'uploads/'
95
+ const UPLOAD_MAX_BYTES = 5 * 1024 * 1024
96
+ // Yüklenen anahtar `<ön ek>/<uuid>-<ad>`: indirme adı için uuid ön eki soyulur.
97
+ const UPLOAD_ID_RE = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}-/
98
+
99
+ /**
100
+ * Her kullanıcının dosyaları KENDİ ön ekindedir (`uploads/<sub>/`); liste, indirme ve silme bu ön ekin dışına
101
+ * çıkamaz. Tek bir ortak ön ek kullanılsaydı aynı adla yüklenen dosya bir başkasınınkini EZERDİ ve anahtarı bilen
102
+ * herkes silebilirdi. `sub` URL kodlanır ki anahtar parçası `/` ya da kontrol karakteri taşımasın.
103
+ */
104
+ function uploadPrefixFor(sub: string): string {
105
+ return `${UPLOAD_PREFIX}${encodeURIComponent(sub)}/`
106
+ }
107
+ {{/files}}
108
+
109
+ function json(body: unknown, status = 200): Response {
110
+ return Response.json(body, { status })
111
+ }
112
+
113
+ /** JSON gövdeyi SINIRLI okur: hazır `.json()` metodu gövdenin tamamını belleğe aldıktan SONRA ayrıştırırdı. */
114
+ async function readJsonBody(request: Request, maxBytes: number): Promise<unknown> {
115
+ const buffer = await readBoundedBody(request, maxBytes)
116
+ try {
117
+ return JSON.parse(new TextDecoder().decode(buffer))
118
+ } catch {
119
+ return null
120
+ }
121
+ }
122
+
123
+ export default {
124
+ async fetch(request, env): Promise<Response> {
125
+ const { pathname, searchParams } = new URL(request.url)
126
+ const method = request.method
127
+ // `catch` bağlamı görsün diye dışarıda: beklenmeyen hata Event Logger'a uygulama ve portal bağlamıyla yazılır (D-45).
128
+ let context: OneContext | undefined
129
+ try {
130
+ // Devoplus yapay zekâsı: imzalı araç çağrısı (`/__one/agent/…`) ve katalog/rehber (`/.well-known/one-agent.*`) —
131
+ // SDK karşılar; yönlendirmeden ÖNCE durur.
132
+ const agent = (await handleAgentRequest(request, env, agentTools, manifest)) ?? agentManifestResponse(request, manifest, guide)
133
+ if (agent) return agent
134
+
135
+ const ctx = readContext(request.headers)
136
+ context = ctx
137
+
138
+ if (method === 'GET') {
139
+ const asset = clientAssetResponse(pathname)
140
+ if (asset) return asset
141
+ if (pathname === '/shell.js') return scriptResponse(SHELL_SCRIPT)
142
+ }
143
+
144
+ if (method === 'GET' && isPage(pathname)) {
145
+ return new Response(renderShell(normalizeLang(searchParams.get('lang')), normalizeTheme(searchParams.get('theme'))), {
146
+ headers: { 'content-type': 'text/html; charset=utf-8', 'cache-control': 'no-store' },
147
+ })
148
+ }
149
+
150
+ if (pathname === '/api/me' && method === 'GET') {
151
+ const user = requireUser(ctx)
152
+ const api = new BackendClient({ baseUrl: env.API_BASE, authorization: request.headers.get('authorization') })
153
+ const me = await api.get<MeResponse>('Users/me')
154
+ return json({ app: ctx.app, portalKey: ctx.portalKey, sub: user.sub, scope: user.scope, me })
155
+ }
156
+
157
+ // ── Notlar: bu kurulumun KENDİ veritabanı. Satırlar `sub` ile kişiye bağlıdır; portal süzgeci GEREKMEZ (veritabanı zaten bu portalın). ──
158
+ if (pathname === '/api/notes' && method === 'GET') {
159
+ const user = requireUser(ctx)
160
+ const rows = await env.DB.prepare('SELECT id, text, created_at AS createdAt FROM notes WHERE sub = ? ORDER BY id DESC LIMIT 50')
161
+ .bind(user.sub)
162
+ .all<NoteRow>()
163
+ return json({ notes: rows.results })
164
+ }
165
+
166
+ if (pathname === '/api/notes' && method === 'POST') {
167
+ const user = requireUser(ctx)
168
+ const body = (await readJsonBody(request, NOTE_BODY_MAX_BYTES)) as { text?: unknown } | null
169
+ const text = typeof body?.text === 'string' ? body.text.trim() : ''
170
+ if (text.length === 0 || text.length > NOTE_MAX_LENGTH) return json({ detail: `Not metni 1–${NOTE_MAX_LENGTH} karakter olmalı.` }, 400)
171
+ const result = await env.DB.prepare('INSERT INTO notes (sub, text) VALUES (?, ?)').bind(user.sub, text).run()
172
+ const id = Number(result.meta.last_row_id)
173
+ logAudit(ctx, '{{slug}}.note.create', String(id))
174
+ return json({ id }, 201)
175
+ }
176
+
177
+ if (pathname.startsWith('/api/notes/') && method === 'DELETE') {
178
+ const user = requireUser(ctx)
179
+ const id = Number(pathname.slice('/api/notes/'.length))
180
+ if (!Number.isInteger(id) || id <= 0) return json({ detail: 'Geçersiz not kimliği.' }, 400)
181
+ // Yalnız kendi notu: `AND sub = ?` olmasaydı kimliği bilen başkasının notunu silerdi.
182
+ const result = await env.DB.prepare('DELETE FROM notes WHERE id = ? AND sub = ?').bind(id, user.sub).run()
183
+ if (result.meta.changes === 0) return json({ detail: 'Not bulunamadı.' }, 404)
184
+ logAudit(ctx, '{{slug}}.note.delete', String(id))
185
+ return json({ deleted: id })
186
+ }
187
+ {{#files}}
188
+
189
+ // ── Dosyalar: bu kurulumun KENDİ dosya deposu (SDK OneFiles anahtar/boyut kurallarını uygular) ──────────────
190
+ if (pathname === '/api/files' && method === 'GET') {
191
+ const user = requireUser(ctx)
192
+ const files = new OneFiles(env.FILES)
193
+ // Yalnız kendi ön eki listelenir. Liste imleci istemciye OLDUĞU GİBİ döner ve `?cursor=` ile geri alınır
194
+ // (sıra leksikografiktir, yükleme sırası değil).
195
+ const cursor = searchParams.get('cursor')
196
+ const page = await files.list({ prefix: uploadPrefixFor(user.sub), limit: 20, ...(cursor ? { cursor } : {}) })
197
+ return json({ files: page.files, truncated: page.truncated, cursor: page.cursor ?? null })
198
+ }
199
+
200
+ if (pathname === '/api/files' && method === 'POST') {
201
+ const user = requireUser(ctx)
202
+ const files = new OneFiles(env.FILES)
203
+ const name = sanitizeFileName(request.headers.get('x-file-name') ?? 'dosya.bin')
204
+ const contentType = request.headers.get('content-type') ?? 'application/octet-stream'
205
+ // Sınır gövde belleğe alınmadan ÖNCE uygulanır (Content-Length), parçalı akışta okuma sırasında kesilir.
206
+ const body = await readBoundedBody(request, UPLOAD_MAX_BYTES)
207
+ // Anahtar her yüklemede benzersizdir (uuid): aynı adla ikinci yükleme öncekini EZMEZ, iki kayıt olur.
208
+ const key = `${uploadPrefixFor(user.sub)}${crypto.randomUUID()}-${name}`
209
+ const stored = await files.put(key, body, { contentType, maxBytes: UPLOAD_MAX_BYTES, metadata: { sub: user.sub } })
210
+ logAudit(ctx, '{{slug}}.file.put', stored.key, { size: stored.size })
211
+ return json(stored, 201)
212
+ }
213
+
214
+ if (pathname.startsWith('/api/files/') && (method === 'GET' || method === 'DELETE')) {
215
+ const user = requireUser(ctx)
216
+ const files = new OneFiles(env.FILES)
217
+ const key = decodeURIComponent(pathname.slice('/api/files/'.length))
218
+ // Başkasının ön eki 404'tür (403 değil): anahtar uzayı yoklanamaz, "var ama senin değil" sızmaz.
219
+ if (!key.startsWith(uploadPrefixFor(user.sub))) return json({ detail: 'Dosya bulunamadı.' }, 404)
220
+ if (method === 'DELETE') {
221
+ await files.delete(key)
222
+ logAudit(ctx, '{{slug}}.file.delete', key, { sub: user.sub })
223
+ return json({ deleted: key })
224
+ }
225
+ const object = await files.get(key)
226
+ if (!object) return json({ detail: 'Dosya bulunamadı.' }, 404)
227
+ const headers = new Headers()
228
+ object.writeHttpMetadata(headers)
229
+ headers.set('etag', object.httpEtag)
230
+ const downloadName = (key.split('/').pop() ?? 'dosya').replace(UPLOAD_ID_RE, '')
231
+ headers.set('content-disposition', `attachment; filename="${downloadName}"`)
232
+ return new Response(object.body, { headers })
233
+ }
234
+ {{/files}}
235
+ {{#cache}}
236
+
237
+ // ── Önbellek: bu kurulumun KENDİ önbelleği. TTL zorunlu; önbellek nihai tutarlıdır — sayaç/kilit/kalıcı kayıt için DEĞİL. ──
238
+ if (pathname === '/api/cache' && (method === 'GET' || method === 'PUT' || method === 'DELETE')) {
239
+ const user = requireUser(ctx)
240
+ const cache = new OneCache(env.CACHE)
241
+ const key = `me:${user.sub}`
242
+ if (method === 'GET') {
243
+ const value = await cache.get(key)
244
+ return json({ key, hit: value !== null, value })
245
+ }
246
+ if (method === 'DELETE') {
247
+ await cache.delete(key)
248
+ return json({ deleted: key })
249
+ }
250
+ const value = { writtenAt: new Date().toISOString(), sub: user.sub }
251
+ await cache.put(key, value, { ttlSeconds: 300 })
252
+ return json({ key, value }, 201)
253
+ }
254
+ {{/cache}}
255
+
256
+ return json({ detail: 'Bulunamadı.' }, 404)
257
+ } catch (error) {
258
+ // OneError → RFC 9457 ProblemDetails (status + code); bilinmeyen hata 500 ONE-SDK-E-999 (ayrıntı sızdırılmaz).
259
+ return problemResponse(error, context)
260
+ }
261
+ },
262
+ } satisfies OneApp<Env>
@@ -0,0 +1,6 @@
1
+ // `import guide from '../AGENT_GUIDE.md' with { type: 'text' }` — Markdown dosyası derlemede metin olarak gömülür
2
+ // (`one build` ve yayın aynı paketleyiciyi kullanır). Bu bildirim yalnız tip denetimi içindir.
3
+ declare module '*.md' {
4
+ const text: string
5
+ export default text
6
+ }
@@ -0,0 +1,47 @@
1
+ ## Bu uygulamanın şablonu: müşteri ziyaret raporu (`musteri-ziyaret`)
2
+
3
+ Uygulama `create-devoplus-one-app --template musteri-ziyaret` ile üretildi. Kullanıcının ihtiyacını bu şablona göre
4
+ DEĞİŞTİRİN; baştan yazmayın. Dosya haritası:
5
+
6
+ | Dosya | Ne |
7
+ |---|---|
8
+ | `one.data.json` | Veri modeli: `visits` (müşteri görüşmesi). Değişince `pnpm data:generate` |
9
+ | `src/app.ts` | Uçlar: ziyaretlerim (kendi kaydım), ekip ziyaretleri (sunucuda hesaplanan ekip + Excel), tüm ziyaretler, müşteri listesi, özet |
10
+ | `src/visit.ts` | Saf kurallar: tarih ve sonraki adım, hafta aralığı, **ekip hesabı `teamOf` (`TEAM_DEPTH`)** + `visit.test.ts` |
11
+ | `src/customers.ts` | **ERP müşteri listesi — YER TUTUCU `CUSTOMER_QUERY`** (DEXA okuma sorgusu; bağlı değilse elle giriş) |
12
+ | `src/shell.ts` | Sayfa: ziyaretlerim (özet + form + müşteri seçici), ekip ziyaretleri, tüm ziyaretler |
13
+ | `src/agent.ts` · `src/queries.ts` · `AGENT_GUIDE.md` | Devoplus yapay zekâsının aracı ve rehberi |
14
+ | `src/lib/*` | Şablonların ortak parçaları — gerekmedikçe değiştirmeyin |
15
+
16
+ ### ERP müşteri listesini bağlamak (`src/customers.ts`)
17
+
18
+ ⛔ Sorgu anahtarı ve kolon adları **UYDURULMAZ**. `CUSTOMER_QUERY = { key, codeColumn, nameColumn }` üçü de boşken
19
+ uygulama DEXA'ya hiç gitmez, `/api/customers` `state: "not-configured"` döner ve müşteri elle girilir. Bağlamak için:
20
+
21
+ 1. Kullanıcıdan ya da yöneticiden kurumun DEXA Query Store'undaki **müşteri listesi okuma sorgusunun anahtarını** ve
22
+ sorgunun döndürdüğü **müşteri kodu** ile **müşteri adı** kolonlarının adlarını öğrenin. Sorgu parametre almayan bir
23
+ liste sorgusu olmalıdır. Sorgu yoksa erişim talebiyle `new-query` isteyin (taslak SQL yalnız öneridir).
24
+ 2. Üç değeri `CUSTOMER_QUERY`'ye yazın.
25
+ 3. `one.manifest.json`'a `scopes` içine `"dexa.read"` ve `services` içine `"dexa": { "queryKeys": ["<anahtar>"] }`
26
+ ekleyin — manifest şeması ikisini birlikte ister.
27
+ 4. Erişim talepleri: sorgunun uygulamaya açılması `dexa-query` (`queryKey`), yetki alanı `scope` (`dexa.read`).
28
+
29
+ Durumlar: `ok` · `not-configured` · `not-permitted` (yetki alanı belirteçte yok ya da sunucu 401/403 döndü) ·
30
+ `unavailable` (ERP'ye ulaşılamıyor; boş liste "müşteri yok" DEĞİLDİR) · `misconfigured` (yanıtta kolonlar yok). `ok`
31
+ dışındaki her durumda kayıt elle girilen adla yapılır ve `customer_source = "manual"` olur.
32
+
33
+ Değişmezler (bozmayın):
34
+
35
+ - **ERP'ye YAZILMAZ.** Bu şablon yalnız okur; ERP'ye yazma (`dexa.write`) istenirse bunun ayrı bir onay ve işlem
36
+ anahtarı gerektirdiğini söyleyin.
37
+ - **Müşteri kaynağını SUNUCU belirler:** ERP listesi bağlıyken kod verilen kayıtta kod listede olmalıdır (yoksa 400) ve
38
+ müşteri adı ERP'den alınır (`customer_source = "erp"`); istemcinin gönderdiği ad o durumda yok sayılır.
39
+ - **Temsilci SUNUCUDAN gelir:** `rep_org_id` ve `rep_name` `Users/me`'den yazılır, gövdede gönderilemez (400).
40
+ `rep_org_id` Organization Schema kimliğidir, oturum kimliği (`sub`) DEĞİLDİR.
41
+ - **Ekip her istekte organizasyon şemasından SUNUCUDA hesaplanır** (`getOrgUsersWithManagers` + `teamOf`, fonksiyonel
42
+ yönetici). Kayıtta yönetici TUTULMAZ; istemciden ekip ya da temsilci süzgeci alınmaz. Ekip dışındaki tek kayıt 404.
43
+ Ekip süzgeci tek bağlı parametredir (`rep_org_id IN (SELECT value FROM json_each(?))`): kimlikleri SQL metnine
44
+ yazmayın. Ekip KESİLMEZ (üst sınır yok): kesilseydi sondaki kişilerin ziyaretleri yöneticiden sessizce gizlenirdi.
45
+ - **Ziyaretlerim yalnız kayıt sahibine açıktır** (`records: 'own'`); tüm ziyaretler yalnız okunur ve izinle kapılıdır.
46
+ - Zamanlanmış iş yoktur. "Sonraki adım tarihi gelince hatırlat" istenirse zamanlanmış işin e-postayı ancak manifest
47
+ `jobs.services` beyanı ve kurulumdaki yönetici onayıyla gönderebileceğini söyleyin.
@@ -0,0 +1,48 @@
1
+ # {{nameTr}}
2
+
3
+ Satış ekibinin müşteri görüşmelerini kaydettiği ve yöneticilerin ekiplerinin görüşmelerini izlediği bir uygulama.
4
+ Devoplus Portal'ın içinde, Portal menüsünden açılır. Bu uygulama Devoplus One'ın **müşteri ziyaret raporu**
5
+ şablonundan üretildi; ihtiyacınıza göre değiştirilecek bir başlangıç noktasıdır.
6
+
7
+ ## Ne yapar
8
+
9
+ 1. **Ziyaretlerim:** satış temsilcisi her görüşmeyi kaydeder: müşteri, ziyaret tarihi, görüşme türü (ziyaret, telefon,
10
+ çevrimiçi), görüşme notu, sonuç ve sonraki adım (tarihiyle). Ekranın üstünde bu haftaki görüşme sayısı, tarihi
11
+ geçmiş sonraki adımlar ve yaklaşan sonraki adımların listesi durur. Kendi görüşmelerini düzenler, sonraki adımı
12
+ tamamlandı olarak işaretler, listeyi Excel'e ya da CSV'ye aktarır.
13
+ 2. **Müşteri seçimi:** kurumunuz ERP müşteri listesini bağladıysa müşteri listeden aranıp seçilir ve adı ERP'den
14
+ alınır. Liste bağlı değilse, uygulamanın izni yoksa ya da ERP'ye o an ulaşılamıyorsa ekran bunu söyler ve müşteri
15
+ adı elle yazılır; kayıt "Elle girildi" olarak işaretlenir. Uygulama ERP'ye **hiçbir şey yazmaz**, yalnız okur.
16
+ 3. **Ekip ziyaretleri:** yönetici, organizasyon şemasında kendisine bağlı çalışanların görüşmelerini görür, arar,
17
+ süzer ve Excel'e aktarır. Ekip her açılışta organizasyon şemasından hesaplanır; şemada yöneticisi değişen çalışanın
18
+ görüşmeleri yeni yöneticisine görünür.
19
+ 4. **Tüm ziyaretler:** bütün ziyaretleri görme izni olan kişi (ör. satış müdürü) bütün ekibin görüşmelerini görür.
20
+ 5. **Devoplus asistanı:** "Bu hafta kaç müşteri görüşmesi yaptım?" ya da "Yaklaşan sonraki adımlarım neler?" gibi
21
+ sorular Devoplus'un yapay zekâ asistanına sorulduğunda cevap bu uygulamadan gelir.
22
+
23
+ ## Kimler ne görür
24
+
25
+ | Kim | Ne yapabilir |
26
+ |---|---|
27
+ | Uygulamayı kullanma izni olan herkes ({{permissionCode}}) | Kendi görüşmelerini kaydeder ve görür; yöneticiyse ekibinin görüşmelerini görür |
28
+ | Bütün ziyaretleri görme izni ({{permissionPrefix}}Manage) | Bütün ekibin görüşmelerini görür ve Excel'e aktarır |
29
+
30
+ İzinleri portal yöneticisi **Yetki Yönetimi** ekranından verir. Kimin kimin yöneticisi olduğu Devoplus'un
31
+ organizasyon şemasından okunur; uygulamada ayrıca tanımlanmaz.
32
+
33
+ ## ERP müşteri listesini bağlamak
34
+
35
+ Uygulama ilk üretildiğinde ERP'ye bağlı değildir ve müşteri elle yazılır. Bağlamak için kurumunuzun ERP'sinde müşteri
36
+ listesini döndüren bir sorgu gerekir. Yapay zekâ aracınıza "müşteri listesini ERP'den alalım" demeniz yeterlidir: araç
37
+ sorgunun bilgilerini sizden ya da yöneticinizden ister, uygulamanın ayarına yazar ve gereken izni yöneticiden ister.
38
+ Sorgu hazır değilse yöneticiden yeni bir sorgu yazılmasını isteyebilir.
39
+
40
+ ## Neleri değiştirebilirsiniz
41
+
42
+ - "Sonuçlara *fiyat teklifi verildi* ekle" (ilk yayından önce netleştirin).
43
+ - "Görüşmeye katılan müşteri yetkilisinin adı da yazılsın" — yeni alan.
44
+ - "Yöneticinin yöneticisi de görsün" — ekip derinliği tek bir ayardır.
45
+ - "Sonraki adımın tarihi gelince temsilciye hatırlatılsın" — yeni bildirim.
46
+
47
+ Bilinmesi gerekenler: ziyaret tarihi bugünden ileri olamaz (gerçekleşen görüşme kaydedilir) ve bir yıldan eski olamaz.
48
+ Sonraki adım yazıldıysa tarihi de verilmelidir.
@@ -0,0 +1,44 @@
1
+ # {{nameTr}} — Devoplus yapay zekâsı için rehber
2
+
3
+ Bu dosyayı **Devoplus yapay zekâsı** okur (uygulama adresinde `/.well-known/one-agent.md`). Bir aracı eklerken,
4
+ değiştirirken ya da kaldırırken bu dosyayı AYNI değişiklikte güncelleyin; yayın kapısı dört başlığı ve her aracın adını
5
+ arar.
6
+
7
+ ## Uygulama ne yapar
8
+
9
+ {{nameTr}}, satış ekibinin müşteri görüşmelerini kaydettiği bir rapor uygulamasıdır. Temsilci her görüşmeyi (ziyaret,
10
+ telefon ya da çevrimiçi) müşteri, tarih, görüşme notu, sonuç ve sonraki adımla kaydeder. Müşteri, kurum bağladıysa ERP
11
+ müşteri listesinden seçilir; bağlı değilse elle yazılır. Yönetici, organizasyon şemasında kendisine bağlı çalışanların
12
+ görüşmelerini görür. Kullanıcı "bu hafta kaç müşteri görüşmesi yaptım", "yaklaşan sonraki adımlarım neler", "en çok
13
+ hangi müşteriyle görüştük" gibi şeyler sorduğunda bu uygulamaya başvurun. Görüşme kaydeden ya da ERP'ye yazan araç
14
+ yoktur.
15
+
16
+ ## Veri
17
+
18
+ - **Görüşme** (`visits`): müşteri kodu ve adı, müşteri kaynağı (`erp` ERP listesinden · `manual` elle girildi), satış
19
+ temsilcisi, ziyaret tarihi, görüşme türü (`visit` ziyaret · `phone` telefon · `online` çevrimiçi), görüşme notu,
20
+ sonuç (`interested` ilgileniyor · `order` sipariş/anlaşma · `follow_up` takip gerekiyor · `no_interest`
21
+ ilgilenmiyor), sonraki adım, sonraki adımın tarihi ve tamamlandı mı.
22
+ - Hafta pazartesi başlar, pazar biter (Türkiye saati).
23
+
24
+ ## Araçlar
25
+
26
+ ### `weekly-visit-summary` — bu haftaki ziyaret özeti (salt okunur)
27
+
28
+ - **Ne zaman:** kullanıcı bu haftaki müşteri görüşmelerini, sonuçlarını ya da yaklaşan sonraki adımlarını sorduğunda.
29
+ - **Girdi:** `scope` — `mine` (varsayılan: kullanıcının kendi görüşmeleri) ya da `all` (bütün ekip; yalnız bütün
30
+ ziyaretleri görme izni olan kişide çalışır, yoksa 403 döner — o zaman `mine` ile yeniden deneyin ve kullanıcıya
31
+ yalnız kendi görüşmelerini görebildiğini söyleyin).
32
+ - **Çıktı:** `{ "scope": "mine", "from": "2026-10-19", "to": "2026-10-25", "total": 7, "byType": { "visit": 4, "phone": 3 },
33
+ "byOutcome": { "interested": 3, "order": 2, "follow_up": 2 }, "topCustomers": [{ "customer_name", "visits" }],
34
+ "upcomingNextSteps": [{ "customer_name", "next_step", "next_step_date", "rep_name" }], "overdueNextSteps": 1 }` —
35
+ `topCustomers` en çok 5, `upcomingNextSteps` en çok 10 kayıt (en yakın tarih önce).
36
+ - **Dikkat:** "Ekibimin görüşmeleri" sorusunu bu araç cevaplamaz (ekip organizasyon şemasından ekranda hesaplanır);
37
+ kullanıcıyı uygulamanın Ekip ziyaretleri sekmesine yönlendirin.
38
+
39
+ ## Örnek istekler
40
+
41
+ - "Bu hafta kaç müşteri görüşmesi yaptım?" → `weekly-visit-summary` → `total`, `byType`
42
+ - "Yaklaşan sonraki adımlarım neler?" → `weekly-visit-summary` → `upcomingNextSteps`
43
+ - "Bu hafta bütün ekip kaç sipariş aldı?" → `weekly-visit-summary` (`scope: "all"`) → `byOutcome.order`
44
+ - "Yarın ABC firmasını ziyaret ettiğimi kaydet." → araç yok; kullanıcıyı uygulamanın Ziyaretlerim sekmesine yönlendirin.
@@ -0,0 +1,53 @@
1
+ {
2
+ "version": 1,
3
+ "entities": {
4
+ "visits": {
5
+ "title": { "tr": "Müşteri ziyaretleri", "en": "Customer visits" },
6
+ "fields": {
7
+ "customer_code": { "type": "text", "title": { "tr": "Müşteri kodu", "en": "Customer code" }, "maxLength": 40, "index": true },
8
+ "customer_name": { "type": "text", "title": { "tr": "Müşteri", "en": "Customer" }, "required": true, "maxLength": 160 },
9
+ "customer_source": {
10
+ "type": "choice",
11
+ "title": { "tr": "Müşteri kaynağı", "en": "Customer source" },
12
+ "required": true,
13
+ "default": "manual",
14
+ "choices": [
15
+ { "value": "erp", "title": { "tr": "ERP müşteri listesi", "en": "ERP customer list" } },
16
+ { "value": "manual", "title": { "tr": "Elle girildi", "en": "Entered manually" } }
17
+ ]
18
+ },
19
+ "rep_org_id": { "type": "text", "title": { "tr": "Satış temsilcisi (kimlik)", "en": "Sales rep (id)" }, "maxLength": 24, "index": true },
20
+ "rep_name": { "type": "text", "title": { "tr": "Satış temsilcisi", "en": "Sales rep" }, "maxLength": 120 },
21
+ "visit_date": { "type": "date", "title": { "tr": "Ziyaret tarihi", "en": "Visit date" }, "required": true, "index": true },
22
+ "visit_type": {
23
+ "type": "choice",
24
+ "title": { "tr": "Görüşme türü", "en": "Contact type" },
25
+ "required": true,
26
+ "default": "visit",
27
+ "index": true,
28
+ "choices": [
29
+ { "value": "visit", "title": { "tr": "Ziyaret", "en": "Visit" } },
30
+ { "value": "phone", "title": { "tr": "Telefon", "en": "Phone" } },
31
+ { "value": "online", "title": { "tr": "Çevrimiçi", "en": "Online" } }
32
+ ]
33
+ },
34
+ "notes": { "type": "longText", "title": { "tr": "Görüşme notu", "en": "Notes" }, "maxLength": 4000 },
35
+ "outcome": {
36
+ "type": "choice",
37
+ "title": { "tr": "Sonuç", "en": "Outcome" },
38
+ "required": true,
39
+ "index": true,
40
+ "choices": [
41
+ { "value": "interested", "title": { "tr": "İlgileniyor", "en": "Interested" } },
42
+ { "value": "order", "title": { "tr": "Sipariş / anlaşma", "en": "Order / deal" } },
43
+ { "value": "follow_up", "title": { "tr": "Takip gerekiyor", "en": "Follow-up needed" } },
44
+ { "value": "no_interest", "title": { "tr": "İlgilenmiyor", "en": "Not interested" } }
45
+ ]
46
+ },
47
+ "next_step": { "type": "text", "title": { "tr": "Sonraki adım", "en": "Next step" }, "maxLength": 200 },
48
+ "next_step_date": { "type": "date", "title": { "tr": "Sonraki adım tarihi", "en": "Next step date" }, "index": true },
49
+ "next_step_done": { "type": "boolean", "title": { "tr": "Sonraki adım tamamlandı", "en": "Next step done" }, "required": true, "default": false }
50
+ }
51
+ }
52
+ }
53
+ }
@@ -0,0 +1,21 @@
1
+ // Devoplus yapay zekâsının bu uygulamada çağırabileceği araçların İŞLEYİCİLERİ. Adı, açıklaması, girdi şeması,
2
+ // salt-okunurluğu ve izni `one.manifest.json` → `agent.tools`'tadır; rehber `AGENT_GUIDE.md`'dir. Yeni araç = üçü birlikte.
3
+ // Araçlar yalnız uygulamanın KENDİ veritabanını okur; ERP'ye gitmez. Ziyaret kaydeden ya da değiştiren araç bilinçli
4
+ // olarak yoktur. `all` kapsamı ekranla aynı kuralla bütün ziyaretleri görme iznine bağlıdır.
5
+ import { OneError, defineAgentTools, hasPermission } from '@devoplus/one-sdk'
6
+ import type { Env } from './app.ts'
7
+ import { overdueCount, upcomingSteps, weekSummary } from './queries.ts'
8
+ import { MANAGE_PERMISSION } from './visit.ts'
9
+
10
+ export const agentTools = defineAgentTools<Env>({
11
+ 'weekly-visit-summary': {
12
+ handler: async ({ input, ctx, env }) => {
13
+ const scope = input.scope === 'all' ? 'all' : 'mine'
14
+ if (scope === 'all' && !hasPermission(ctx, MANAGE_PERMISSION))
15
+ throw new OneError(403, 'APP-E-403', 'Bütün ziyaretleri görmek için izniniz yok; yalnız kendi ziyaretlerinizi sorabilirsiniz.')
16
+ const owner = scope === 'all' ? null : (ctx.user?.sub ?? '')
17
+ const [week, upcoming, overdue] = await Promise.all([weekSummary(env, owner), upcomingSteps(env, owner, new Date(), 10), overdueCount(env, owner)])
18
+ return { scope, ...week, upcomingNextSteps: upcoming.map(({ id: _id, ...rest }) => rest), overdueNextSteps: overdue }
19
+ },
20
+ },
21
+ })