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,87 @@
1
+ {
2
+ "version": 1,
3
+ "entities": {
4
+ "checklists": {
5
+ "title": { "tr": "Kontrol listeleri", "en": "Checklists" },
6
+ "fields": {
7
+ "name": { "type": "text", "title": { "tr": "Ad", "en": "Name" }, "required": true, "unique": true, "maxLength": 120 },
8
+ "description": { "type": "longText", "title": { "tr": "Açıklama", "en": "Description" }, "maxLength": 2000 },
9
+ "active": { "type": "boolean", "title": { "tr": "Kullanımda", "en": "In use" }, "required": true, "default": true }
10
+ }
11
+ },
12
+ "checklist_items": {
13
+ "title": { "tr": "Kontrol maddeleri", "en": "Checklist items" },
14
+ "fields": {
15
+ "checklist_id": { "type": "text", "title": { "tr": "Kontrol listesi", "en": "Checklist" }, "required": true, "maxLength": 36, "index": true },
16
+ "position": { "type": "number", "title": { "tr": "Sıra", "en": "Order" }, "required": true, "default": 1, "min": 1, "max": 999 },
17
+ "section": { "type": "text", "title": { "tr": "Bölüm", "en": "Section" }, "maxLength": 80 },
18
+ "title": { "type": "text", "title": { "tr": "Madde", "en": "Item" }, "required": true, "maxLength": 200 },
19
+ "weight": { "type": "number", "title": { "tr": "Ağırlık", "en": "Weight" }, "required": true, "default": 1, "min": 1, "max": 10 },
20
+ "critical": { "type": "boolean", "title": { "tr": "Kritik", "en": "Critical" }, "required": true, "default": false }
21
+ }
22
+ },
23
+ "audits": {
24
+ "title": { "tr": "Denetimler", "en": "Audits" },
25
+ "fields": {
26
+ "checklist_id": { "type": "text", "title": { "tr": "Kontrol listesi (kimlik)", "en": "Checklist (id)" }, "required": true, "maxLength": 36, "index": true },
27
+ "checklist_name": { "type": "text", "title": { "tr": "Kontrol listesi", "en": "Checklist" }, "maxLength": 120 },
28
+ "location_id": { "type": "text", "title": { "tr": "İş yeri (kimlik)", "en": "Workplace (id)" }, "maxLength": 24, "index": true },
29
+ "location_code": { "type": "text", "title": { "tr": "Mağaza kodu", "en": "Store code" }, "maxLength": 40 },
30
+ "location_name": { "type": "text", "title": { "tr": "Denetlenen yer", "en": "Audited location" }, "required": true, "maxLength": 120, "index": true },
31
+ "auditor_name": { "type": "text", "title": { "tr": "Denetçi", "en": "Auditor" }, "maxLength": 120 },
32
+ "status": {
33
+ "type": "choice",
34
+ "title": { "tr": "Durum", "en": "Status" },
35
+ "required": true,
36
+ "default": "draft",
37
+ "index": true,
38
+ "choices": [
39
+ { "value": "draft", "title": { "tr": "Taslak", "en": "Draft" } },
40
+ { "value": "completed", "title": { "tr": "Tamamlandı", "en": "Completed" } }
41
+ ]
42
+ },
43
+ "submitted_at": { "type": "datetime", "title": { "tr": "Tamamlanma", "en": "Completed at" }, "index": true },
44
+ "score": { "type": "number", "title": { "tr": "Puan", "en": "Score" }, "min": 0, "max": 100 },
45
+ "result": {
46
+ "type": "choice",
47
+ "title": { "tr": "Sonuç", "en": "Result" },
48
+ "index": true,
49
+ "choices": [
50
+ { "value": "passed", "title": { "tr": "Başarılı", "en": "Passed" } },
51
+ { "value": "needs_improvement", "title": { "tr": "Geliştirilmeli", "en": "Needs improvement" } },
52
+ { "value": "failed", "title": { "tr": "Başarısız", "en": "Failed" } }
53
+ ]
54
+ },
55
+ "critical_failed": { "type": "boolean", "title": { "tr": "Kritik madde uygun değil", "en": "Critical item failed" }, "required": true, "default": false },
56
+ "compliant_count": { "type": "number", "title": { "tr": "Uygun", "en": "Compliant" }, "required": true, "default": 0, "min": 0 },
57
+ "noncompliant_count": { "type": "number", "title": { "tr": "Uygun değil", "en": "Non-compliant" }, "required": true, "default": 0, "min": 0 },
58
+ "not_applicable_count": { "type": "number", "title": { "tr": "Uygulanamaz", "en": "Not applicable" }, "required": true, "default": 0, "min": 0 },
59
+ "general_note": { "type": "longText", "title": { "tr": "Genel not", "en": "General note" }, "maxLength": 2000 }
60
+ }
61
+ },
62
+ "audit_answers": {
63
+ "title": { "tr": "Denetim cevapları", "en": "Audit answers" },
64
+ "fields": {
65
+ "audit_id": { "type": "text", "title": { "tr": "Denetim", "en": "Audit" }, "required": true, "maxLength": 36, "index": true },
66
+ "item_id": { "type": "text", "title": { "tr": "Madde (kimlik)", "en": "Item (id)" }, "required": true, "maxLength": 36 },
67
+ "position": { "type": "number", "title": { "tr": "Sıra", "en": "Order" }, "required": true, "default": 1, "min": 1 },
68
+ "section": { "type": "text", "title": { "tr": "Bölüm", "en": "Section" }, "maxLength": 80 },
69
+ "title": { "type": "text", "title": { "tr": "Madde", "en": "Item" }, "required": true, "maxLength": 200 },
70
+ "weight": { "type": "number", "title": { "tr": "Ağırlık", "en": "Weight" }, "required": true, "default": 1, "min": 1, "max": 10 },
71
+ "critical": { "type": "boolean", "title": { "tr": "Kritik", "en": "Critical" }, "required": true, "default": false },
72
+ "result": {
73
+ "type": "choice",
74
+ "title": { "tr": "Sonuç", "en": "Result" },
75
+ "choices": [
76
+ { "value": "compliant", "title": { "tr": "Uygun", "en": "Compliant" } },
77
+ { "value": "noncompliant", "title": { "tr": "Uygun değil", "en": "Non-compliant" } },
78
+ { "value": "not_applicable", "title": { "tr": "Uygulanamaz", "en": "Not applicable" } }
79
+ ]
80
+ },
81
+ "note": { "type": "text", "title": { "tr": "Not", "en": "Note" }, "maxLength": 500 },
82
+ "photo": { "type": "file", "title": { "tr": "Fotoğraf", "en": "Photo" } },
83
+ "photo_name": { "type": "text", "title": { "tr": "Fotoğraf adı", "en": "Photo name" }, "maxLength": 200 }
84
+ }
85
+ }
86
+ }
87
+ }
@@ -0,0 +1,41 @@
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 TAMAMLANMIŞ denetimleri okur ve yalnız yöneticiye açıktır (manifest `permission`). Denetim açan, cevap
4
+ // yazan ya da tamamlayan araç bilinçli olarak yoktur.
5
+ import { defineAgentTools } from '@devoplus/one-sdk'
6
+ import type { Env } from './app.ts'
7
+ import { failedItems, locationScores, recentAudits } from './queries.ts'
8
+
9
+ export const agentTools = defineAgentTools<Env>({
10
+ 'recent-audit-results': {
11
+ handler: async ({ input, env }) => {
12
+ const limit = typeof input.limit === 'number' ? input.limit : 10
13
+ const audits = await recentAudits(env, limit, typeof input.location === 'string' ? input.location : undefined)
14
+ const failed = await failedItems(env, audits.map((a) => a.id))
15
+ return {
16
+ audits: audits.map((a) => ({
17
+ location: a.location_name,
18
+ checklist: a.checklist_name,
19
+ auditor: a.auditor_name,
20
+ completedAt: a.submitted_at,
21
+ score: a.score,
22
+ result: a.result,
23
+ criticalFailed: a.critical_failed,
24
+ failedItems: failed.get(a.id) ?? [],
25
+ })),
26
+ }
27
+ },
28
+ },
29
+ 'location-scores': {
30
+ handler: async ({ env }) => ({
31
+ locations: (await locationScores(env, 50)).map((l) => ({
32
+ location: l.location,
33
+ audits: l.audits,
34
+ lastScore: l.last_score,
35
+ averageScore: l.average,
36
+ lastAuditAt: l.last_at,
37
+ criticalFailures: l.critical_failures,
38
+ })),
39
+ }),
40
+ },
41
+ })
@@ -0,0 +1,444 @@
1
+ // {{nameTr}} — saha denetimi (mağaza ve şube denetimi). Uçlar (hepsi oturum ister; kimlik platformdan gelir):
2
+ // GET /api/checklists[?…] · POST · PATCH/DELETE /api/checklists/{id} kontrol listeleri (yazma: yönetici)
3
+ // GET /api/checklist_items[?…] · POST · PATCH/DELETE /api/checklist_items/{id} maddeler (yazma: yönetici)
4
+ // GET /api/workplaces iş yerleri (organizasyon şemasından; okunamazsa boş + available:false)
5
+ // GET /api/audits[?…] denetimlerim (yalnız benim yaptıklarım) — hazır liste + Excel
6
+ // POST /api/audits { checklist_id, location_id? | location_name } — taslak açar
7
+ // GET /api/audits/{id}/answers denetim + maddeler (denetçi ya da yönetici)
8
+ // PUT /api/audits/{id}/answers { answers: [{ id, result?, note?, photo? }], general_note? } — yalnız taslak, yalnız denetçi
9
+ // POST /api/audits/{id}/submit tamamla: puan SUNUCUDA hesaplanır; sonra denetim değişmez
10
+ // DELETE /api/audits/{id} taslağı sil (tamamlanmış denetim silinmez)
11
+ // GET /api/audits/{id}/report.html yazdırılabilir denetim raporu
12
+ // GET /api/audits/{id}/answers/{answerId}/photo madde fotoğrafı
13
+ // GET /api/all/audits[?…] bütün denetimler (yönetici) — hazır liste + Excel + yazdırma
14
+ // GET /api/dashboard pano: son 30 gün özeti, yerlere göre puan, son denetimler (yönetici)
15
+ // POST /api/files fotoğraf yükleme (yalnız kendi ön ekinize)
16
+ // Puan, sonuç, sayılar ve durum istemciden ALINMAZ. Denetim başlarken kontrol listesinin maddeleri denetime KOPYALANIR:
17
+ // liste sonradan değişse de geçmiş denetimler ve puanları değişmez.
18
+ import {
19
+ DataValidationError,
20
+ OneError,
21
+ createCrudHandler,
22
+ foldSearchText,
23
+ getMyOrgContext,
24
+ getOrgSchema,
25
+ hasPermission,
26
+ localizedText,
27
+ logAudit,
28
+ logError,
29
+ renderPrintableReport,
30
+ reportResponse,
31
+ requirePermission,
32
+ requireUser,
33
+ type ExportColumn,
34
+ type OneContext,
35
+ type OneFilesBinding,
36
+ } from '@devoplus/one-sdk'
37
+ import manifest from '../one.manifest.json' with { type: 'json' }
38
+ import { agentTools } from './agent.ts'
39
+ import { dataModel } from './data/types.ts'
40
+ import { fileResponse, uploadFile } from './lib/files.ts'
41
+ import { fieldError, forward, matchId, nowIso, onlyFields, ownFileKey, readJsonObject } from './lib/http.ts'
42
+ import { choices } from './lib/model.ts'
43
+ import { applied, insertWhen, recordWrite, refuseActingSession, sql } from './lib/record.ts'
44
+ import { createApp, json, type AppDefinition, type BaseEnv, type RouteInput } from './lib/server.ts'
45
+ import { answersOf, findAudit, locationScores, recentAudits, summary, type AnswerRow, type AuditRow } from './queries.ts'
46
+ import { CHECKLIST_ITEMS_MAX, NOTE_MAX, WEIGHT_MAX, WEIGHT_MIN, computeScore, isAnswerResult, isValidWeight, submitProblems } from './score.ts'
47
+ import { renderShell, renderShellScript } from './shell.ts'
48
+
49
+ export interface Env extends BaseEnv {
50
+ /** Bu kurulumun dosya deposu (manifest services.files) — madde fotoğrafları. */
51
+ FILES: OneFilesBinding
52
+ }
53
+
54
+ export const APP_NAME = { tr: {{nameTrJs}}, en: {{nameEnJs}} }
55
+ /** Yönetici izni (manifest `permissions`): kontrol listelerini yönetir, bütün denetimleri ve panoyu görür. */
56
+ export const MANAGE_PERMISSION = '{{permissionPrefix}}Manage'
57
+
58
+ export const checklists = createCrudHandler({ model: dataModel, entity: 'checklists', records: 'shared', canWrite: MANAGE_PERMISSION })
59
+ export const checklistItems = createCrudHandler({ model: dataModel, entity: 'checklist_items', records: 'shared', canWrite: MANAGE_PERMISSION })
60
+ /** Denetimlerim: herkes yalnız KENDİ yaptığı denetimleri görür. */
61
+ export const myAudits = createCrudHandler({ model: dataModel, entity: 'audits', records: 'own' })
62
+ /** Bütün denetimler: yalnız yönetici; salt okunur. */
63
+ export const allAudits = createCrudHandler({ model: dataModel, entity: 'audits', records: 'shared', basePath: '/api/all', canRead: MANAGE_PERMISSION })
64
+
65
+ const ITEM_FIELDS = ['position', 'section', 'title', 'weight', 'critical'] as const
66
+ const GENERAL_NOTE_MAX = 2000
67
+ const LOCATION_MAX = 120
68
+ const ORG_ID_RE = /^[0-9a-f]{24}$/i
69
+ const UUID_RE = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/
70
+ const PHOTO_RE = /^\/api\/audits\/([0-9a-f-]{36})\/answers\/([0-9a-f-]{36})\/photo$/
71
+ /** Satır sonu ve sekme dışındaki denetim karakterleri. */
72
+ const CONTROL_RE = /[\u0000-\u0008\u000b-\u001f\u007f]/
73
+ const RESULT_LABELS = Object.fromEntries(choices(dataModel, 'audit_answers', 'result').map((c) => [c.value, c.label]))
74
+ const AUDIT_RESULT_LABELS = Object.fromEntries(choices(dataModel, 'audits', 'result').map((c) => [c.value, c.label]))
75
+
76
+ async function myName(input: RouteInput<Env>): Promise<string | null> {
77
+ try {
78
+ return (await getMyOrgContext(input.api()))?.fullName ?? null
79
+ } catch (error) {
80
+ // Ad bir kolaylıktır: okunamazsa denetim yine açılır (denetçi adı boş kalır).
81
+ logError(input.ctx, 'kişi adı okunamadı', error)
82
+ return null
83
+ }
84
+ }
85
+
86
+ /** Denetimi görebilen: denetçinin kendisi ya da yönetici. Göremeyen için "yok" — kimlik yoklanamaz. */
87
+ async function visibleAudit(env: Env, ctx: OneContext, id: string): Promise<{ audit: AuditRow; own: boolean }> {
88
+ const user = requireUser(ctx)
89
+ const audit = await findAudit(env, id)
90
+ const own = audit !== null && audit.created_by === user.sub
91
+ if (!audit || (!own && !hasPermission(ctx, MANAGE_PERMISSION))) throw new OneError(404, 'APP-E-404', 'Denetim bulunamadı.')
92
+ return { audit, own }
93
+ }
94
+
95
+ /** Değiştirme: yalnız denetçi ve yalnız taslak. */
96
+ async function editableAudit(env: Env, ctx: OneContext, id: string): Promise<AuditRow> {
97
+ const { audit, own } = await visibleAudit(env, ctx, id)
98
+ if (!own) throw new OneError(403, 'APP-E-403', 'Denetimi yalnız denetimi yapan kişi doldurabilir.')
99
+ if (audit.status !== 'draft') throw new OneError(409, 'APP-E-020', 'Tamamlanmış denetim değiştirilemez.')
100
+ return audit
101
+ }
102
+
103
+ function text(raw: unknown, field: string, max: number, required: boolean, multiline = false): string | null {
104
+ if (raw === null || raw === undefined || raw === '') {
105
+ if (required) throw fieldError(field, 'Bu alan zorunlu.')
106
+ return null
107
+ }
108
+ if (typeof raw !== 'string') throw fieldError(field, 'Metin olmalı.')
109
+ const value = raw.trim()
110
+ if (value === '' && required) throw fieldError(field, 'Bu alan zorunlu.')
111
+ if ([...value].length > max) throw fieldError(field, `En çok ${max} karakter olabilir.`)
112
+ if (CONTROL_RE.test(value) || (!multiline && /[\r\n\t]/.test(value))) throw fieldError(field, 'Geçersiz karakter var.')
113
+ return value || null
114
+ }
115
+
116
+ /** Denetlenen yer: organizasyon şemasındaki iş yeri (sunucuda doğrulanır) ya da elle yazılmış ad. */
117
+ async function resolveLocation(input: RouteInput<Env>, body: Record<string, unknown>): Promise<{ id: string | null; code: string | null; name: string }> {
118
+ const rawId = body.location_id
119
+ if (rawId !== undefined && rawId !== null && rawId !== '') {
120
+ if (typeof rawId !== 'string' || !ORG_ID_RE.test(rawId)) throw fieldError('location_id', 'Listeden bir iş yeri seçin.')
121
+ let workplaces
122
+ try {
123
+ workplaces = (await getOrgSchema(input.api())).workplaces
124
+ } catch (error) {
125
+ logError(input.ctx, 'organizasyon şeması okunamadı', error)
126
+ throw new OneError(503, 'APP-E-503', 'İş yeri listesi şu an okunamıyor; yerin adını elle yazın ya da biraz sonra yeniden deneyin.')
127
+ }
128
+ const wp = workplaces.find((w) => w.id.toLowerCase() === rawId.toLowerCase() && w.isActive !== false)
129
+ if (!wp) throw fieldError('location_id', 'İş yeri bulunamadı.')
130
+ const name = (localizedText(wp.workplaceName, 'tr') || wp.workplaceCode || '').slice(0, LOCATION_MAX)
131
+ return { id: wp.id.toLowerCase(), code: wp.storeCode ?? null, name: name || wp.id }
132
+ }
133
+ return { id: null, code: null, name: text(body.location_name, 'location_name', LOCATION_MAX, true)! }
134
+ }
135
+
136
+ async function workplaces(input: RouteInput<Env>): Promise<Response> {
137
+ requireUser(input.ctx)
138
+ try {
139
+ const schema = await getOrgSchema(input.api())
140
+ const items = schema.workplaces
141
+ .filter((w) => w.isActive !== false)
142
+ .map((w) => ({ id: w.id.toLowerCase(), code: w.storeCode ?? null, label: { tr: localizedText(w.workplaceName, 'tr') || w.workplaceCode, en: localizedText(w.workplaceName, 'en') || w.workplaceCode } }))
143
+ .sort((a, b) => a.label.tr.localeCompare(b.label.tr, 'tr'))
144
+ return json({ available: true, items })
145
+ } catch (error) {
146
+ // Şema okunamazsa ekran serbest metne döner; denetim yine açılabilir.
147
+ logError(input.ctx, 'iş yerleri okunamadı', error)
148
+ return json({ available: false, items: [] })
149
+ }
150
+ }
151
+
152
+ async function itemWrite(input: RouteInput<Env>, id: string | null): Promise<Response> {
153
+ const { request, env, ctx, method } = input
154
+ requirePermission(ctx, MANAGE_PERMISSION)
155
+ if (id && method === 'DELETE') return (await checklistItems.handle(request, env.DB, ctx))!
156
+ const body = onlyFields(await readJsonObject(request), id ? ITEM_FIELDS : ['checklist_id', ...ITEM_FIELDS])
157
+ if ('weight' in body && !isValidWeight(body.weight)) throw fieldError('weight', `Ağırlık ${WEIGHT_MIN}-${WEIGHT_MAX} arası bir tam sayı olmalı.`)
158
+ if (id) return (await checklistItems.handle(forward(request, `${checklistItems.path}/${id}`, 'PATCH', body), env.DB, ctx))!
159
+ const user = requireUser(ctx)
160
+ refuseActingSession(ctx)
161
+ const listId = typeof body.checklist_id === 'string' ? body.checklist_id : ''
162
+ if (!UUID_RE.test(listId)) throw fieldError('checklist_id', 'Kontrol listesi bulunamadı.')
163
+ const write = recordWrite(dataModel, 'checklist_items', body, 'create')
164
+ const itemId = crypto.randomUUID()
165
+ // ⛔ "Liste var" ve "madde sayısı sınırın altında" koşulları eklemeyle AYNI ifadededir: önce sayıp sonra eklemek, aynı
166
+ // anda gelen iki ekleme isteğinin ikisinin de sınırın altında görüp sınırı aşmasına ya da silinmiş bir listeye madde
167
+ // eklenmesine izin verirdi.
168
+ const when = sql(
169
+ 'EXISTS (SELECT 1 FROM "checklists" c WHERE c.id = ? AND c.deleted_at IS NULL) AND (SELECT COUNT(*) FROM "checklist_items" WHERE checklist_id = ? AND deleted_at IS NULL) < ?',
170
+ listId,
171
+ listId,
172
+ CHECKLIST_ITEMS_MAX,
173
+ )
174
+ if (!(await applied(write, insertWhen(env.DB, write, { id: itemId, actor: user.sub, now: nowIso(), when })))) {
175
+ const list = await env.DB.prepare('SELECT id FROM "checklists" WHERE id = ? AND deleted_at IS NULL').bind(listId).first<{ id: string }>()
176
+ if (!list) throw fieldError('checklist_id', 'Kontrol listesi bulunamadı.')
177
+ throw new OneError(409, 'APP-E-021', `Bir kontrol listesinde en çok ${CHECKLIST_ITEMS_MAX} madde olabilir.`)
178
+ }
179
+ const res = (await checklistItems.handle(forward(request, `${checklistItems.path}/${itemId}`, 'GET'), env.DB, ctx))!
180
+ if (!res.ok) return res
181
+ const out = json(await res.json(), 201)
182
+ out.headers.set('location', `${checklistItems.path}/${itemId}`)
183
+ return out
184
+ }
185
+
186
+ async function createAudit(input: RouteInput<Env>): Promise<Response> {
187
+ const { request, env, ctx } = input
188
+ const user = requireUser(ctx)
189
+ refuseActingSession(ctx)
190
+ // ⛔ Puan, durum ve sayılar burada GÖNDERİLEMEZ (alan listesi dışındaki her ad 400).
191
+ const body = onlyFields(await readJsonObject(request), ['checklist_id', 'location_id', 'location_name'])
192
+ const listId = typeof body.checklist_id === 'string' ? body.checklist_id : ''
193
+ const list = UUID_RE.test(listId)
194
+ ? await env.DB.prepare('SELECT id, name, active FROM "checklists" WHERE id = ? AND deleted_at IS NULL').bind(listId).first<{ id: string; name: string; active: number }>()
195
+ : null
196
+ if (!list) throw fieldError('checklist_id', 'Kontrol listesi bulunamadı.')
197
+ if (list.active !== 1) throw new OneError(409, 'APP-E-021', 'Bu kontrol listesi kullanımda değil.')
198
+ const items = await env.DB.prepare(
199
+ 'SELECT id, position, section, title, weight, critical FROM "checklist_items" WHERE checklist_id = ? AND deleted_at IS NULL ORDER BY position, created_at LIMIT ?',
200
+ )
201
+ .bind(listId, CHECKLIST_ITEMS_MAX + 1)
202
+ .all<{ id: string; position: number; section: string | null; title: string; weight: number; critical: number }>()
203
+ if (items.results.length === 0) throw new OneError(409, 'APP-E-022', 'Kontrol listesinde henüz madde yok.')
204
+ // ⛔ Maddeler KESİLMEZ: sınırı aşan liste (sınır sonradan düşürülmüş ya da veri elle eklenmiş) denetimi eksik maddeyle
205
+ // açıp puanı eksik kümeden hesaplatırdı. Bir fazla satır okunur; aşılmışsa denetim açılmaz ve nedeni söylenir.
206
+ if (items.results.length > CHECKLIST_ITEMS_MAX) {
207
+ const total = await env.DB.prepare('SELECT COUNT(*) AS n FROM "checklist_items" WHERE checklist_id = ? AND deleted_at IS NULL').bind(listId).first<{ n: number }>()
208
+ throw new OneError(409, 'APP-E-021', `Kontrol listesinde ${total?.n ?? items.results.length} madde var; bir denetim en çok ${CHECKLIST_ITEMS_MAX} madde taşır. Listeden madde çıkarın.`)
209
+ }
210
+ const location = await resolveLocation(input, body)
211
+ const auditor = await myName(input)
212
+ const now = nowIso()
213
+ const id = crypto.randomUUID()
214
+ const audit = recordWrite(
215
+ dataModel,
216
+ 'audits',
217
+ { checklist_id: listId, checklist_name: list.name, location_id: location.id, location_code: location.code, location_name: location.name, auditor_name: auditor ? [...auditor.trim()].slice(0, 120).join('') || null : null, status: 'draft' },
218
+ 'create',
219
+ )
220
+ // Denetim ve maddelerinin kopyası TEK toplu yazımdır: ya hepsi yazılır ya hiçbiri. ⛔ Denetim yalnız liste HÂLÂ
221
+ // kullanımda, silinmemiş ve adı okuduğumuz adsa açılır (aynı ifadede); maddeler yalnız denetim açıldıysa eklenir.
222
+ // Madde kopyaları okunduğu andaki listedir: arada değişen bir madde, denetim o değişiklikten önce açılmış gibi kalır.
223
+ const statements = [
224
+ insertWhen(env.DB, audit, {
225
+ id,
226
+ actor: user.sub,
227
+ now,
228
+ when: sql('EXISTS (SELECT 1 FROM "checklists" c WHERE c.id = ? AND c.active = 1 AND c.deleted_at IS NULL AND c.name = ?)', listId, list.name),
229
+ }),
230
+ ...items.results.map((item, index) => {
231
+ const answer = recordWrite(
232
+ dataModel,
233
+ 'audit_answers',
234
+ { audit_id: id, item_id: item.id, position: index + 1, section: item.section, title: item.title, weight: item.weight, critical: item.critical === 1 },
235
+ 'create',
236
+ )
237
+ return insertWhen(env.DB, answer, { id: crypto.randomUUID(), actor: user.sub, now, when: sql('EXISTS (SELECT 1 FROM "audits" WHERE id = ?)', id) })
238
+ }),
239
+ ]
240
+ const [opened] = await env.DB.batch(statements)
241
+ if (Number(opened?.meta.changes ?? 0) !== 1) throw new OneError(409, 'APP-E-012', 'Kontrol listesi bu arada değişti; sayfayı yenileyip yeniden deneyin.')
242
+ logAudit(ctx, 'audit.start', id, { items: items.results.length })
243
+ return json({ audit: await findAudit(env, id), answers: await answersOf(env, id) }, 201)
244
+ }
245
+
246
+ function answerView(a: AnswerRow) {
247
+ return { ...a, critical: a.critical === 1 }
248
+ }
249
+
250
+ function auditView(a: AuditRow) {
251
+ return { ...a, critical_failed: a.critical_failed === 1 }
252
+ }
253
+
254
+ async function readAnswers({ env, ctx }: RouteInput<Env>, id: string): Promise<Response> {
255
+ const { audit, own } = await visibleAudit(env, ctx, id)
256
+ return json({ audit: auditView(audit), answers: (await answersOf(env, id)).map(answerView), editable: own && audit.status === 'draft' })
257
+ }
258
+
259
+ const fold = (value: string | null): string | null => (value === null ? null : foldSearchText(value))
260
+
261
+ async function saveAnswers(input: RouteInput<Env>, id: string): Promise<Response> {
262
+ const { request, env, ctx } = input
263
+ const user = requireUser(ctx)
264
+ refuseActingSession(ctx)
265
+ const body = onlyFields(await readJsonObject(request), ['answers', 'general_note'])
266
+ await editableAudit(env, ctx, id)
267
+ const existing = new Map((await answersOf(env, id)).map((a) => [a.id, a]))
268
+ const raw = body.answers ?? []
269
+ if (!Array.isArray(raw) || raw.length > CHECKLIST_ITEMS_MAX) throw fieldError('answers', 'Cevaplar bir liste olmalı.')
270
+ const now = nowIso()
271
+ const updates = raw.map((entry, index) => {
272
+ if (typeof entry !== 'object' || entry === null || Array.isArray(entry)) throw fieldError(`answers.${index}`, 'Cevap bir nesne olmalı.')
273
+ const answer = onlyFields(entry as Record<string, unknown>, ['id', 'result', 'note', 'photo'])
274
+ const target = typeof answer.id === 'string' ? existing.get(answer.id) : undefined
275
+ if (!target) throw fieldError(`answers.${index}.id`, 'Madde bu denetimde yok.')
276
+ const sets: string[] = []
277
+ const values: (string | number | null)[] = []
278
+ if ('result' in answer) {
279
+ const result = answer.result === '' || answer.result === null ? null : answer.result
280
+ if (result !== null && !isAnswerResult(result)) throw fieldError(`answers.${index}.result`, 'Uygun, uygun değil ya da uygulanamaz seçin.')
281
+ sets.push('result = ?')
282
+ values.push(result)
283
+ }
284
+ if ('note' in answer) {
285
+ const note = text(answer.note, `answers.${index}.note`, NOTE_MAX, false, true)
286
+ sets.push('note = ?', 'note_fold = ?')
287
+ values.push(note, fold(note))
288
+ }
289
+ if ('photo' in answer) {
290
+ const key = ownFileKey(answer.photo, user.sub, `answers.${index}.photo`)
291
+ const name = key && typeof answer.photo === 'object' && answer.photo !== null && typeof (answer.photo as { name?: unknown }).name === 'string' ? (answer.photo as { name: string }).name.slice(0, 200) : null
292
+ // Ad gönderilmediyse satırdaki ad KORUNUR (aynı ifadede; okunmuş bir değer geri yazılmaz). Fotoğraf silinince ad da silinir.
293
+ if (key) sets.push('photo = ?', 'photo_name = coalesce(?, photo_name)', 'photo_name_fold = coalesce(?, photo_name_fold)')
294
+ else sets.push('photo = ?', 'photo_name = ?', 'photo_name_fold = ?')
295
+ values.push(key, name, fold(name))
296
+ }
297
+ // ⛔ Madde yalnız denetim HÂLÂ bu kişinin taslağıysa güncellenir (aynı toplu yazımda tamamlanan denetim değişmez).
298
+ return env.DB.prepare(
299
+ `UPDATE "audit_answers" SET ${[...sets, 'updated_at = ?', 'updated_by = ?'].join(', ')} WHERE id = ? AND audit_id = ? AND deleted_at IS NULL ` +
300
+ `AND EXISTS (SELECT 1 FROM "audits" WHERE id = ? AND created_by = ? AND status = 'draft' AND deleted_at IS NULL)`,
301
+ ).bind(...values, now, user.sub, target.id, id, id, user.sub)
302
+ })
303
+ // Genel not yalnız GÖNDERİLDİYSE yazılır: başka bir sekmede kaydedilmiş notu okunmuş eski değerle ezmez.
304
+ const noteSet = 'general_note' in body ? text(body.general_note, 'general_note', GENERAL_NOTE_MAX, false, true) : undefined
305
+ // Denetimin `updated_at`'i her kayıtta ilerler: tamamlama, okuduğu cevaplar bu arada değiştiyse durur (409).
306
+ const touch = env.DB.prepare(
307
+ `UPDATE "audits" SET ${noteSet === undefined ? '' : 'general_note = ?, general_note_fold = ?, '}updated_at = ?, updated_by = ? WHERE id = ? AND created_by = ? AND status = 'draft' AND deleted_at IS NULL`,
308
+ ).bind(...(noteSet === undefined ? [] : [noteSet, fold(noteSet)]), now, user.sub, id, user.sub)
309
+ const [touched] = await env.DB.batch([touch, ...updates])
310
+ if (Number(touched?.meta.changes ?? 0) !== 1) throw new OneError(409, 'APP-E-020', 'Tamamlanmış denetim değiştirilemez.')
311
+ return readAnswers(input, id)
312
+ }
313
+
314
+ async function submit(input: RouteInput<Env>, id: string): Promise<Response> {
315
+ const { request, env, ctx } = input
316
+ const user = requireUser(ctx)
317
+ refuseActingSession(ctx)
318
+ // Gövde boş olmalıdır: puan, sonuç ya da sayı GÖNDERİLEMEZ (400).
319
+ if (request.body !== null) onlyFields(await readJsonObject(request), [])
320
+ const audit = await editableAudit(env, ctx, id)
321
+ const answers = await answersOf(env, id)
322
+ const scored = answers.map((a) => ({ weight: a.weight, critical: a.critical === 1, result: a.result, note: a.note }))
323
+ const problems = submitProblems(scored)
324
+ if (problems.length > 0) throw new DataValidationError(400, 'APP-E-400', 'Denetim tamamlanamadı: eksik maddeler var.', problems.map((message) => ({ field: 'answers', message })))
325
+ const s = computeScore(scored)
326
+ const now = nowIso()
327
+ const res = await env.DB.prepare(
328
+ "UPDATE \"audits\" SET status = 'completed', submitted_at = ?, score = ?, result = ?, critical_failed = ?, compliant_count = ?, noncompliant_count = ?, not_applicable_count = ?, updated_at = ?, updated_by = ? " +
329
+ "WHERE id = ? AND created_by = ? AND status = 'draft' AND updated_at = ? AND deleted_at IS NULL",
330
+ )
331
+ .bind(now, s.score, s.result, s.criticalFailed ? 1 : 0, s.compliant, s.noncompliant, s.notApplicable, now, user.sub, id, user.sub, audit.updated_at)
332
+ .run()
333
+ if (Number(res.meta.changes ?? 0) !== 1) throw new OneError(409, 'APP-E-012', 'Denetim bu arada değişti; sayfayı yenileyip yeniden deneyin.')
334
+ logAudit(ctx, 'audit.submit', id, { score: s.score, result: s.result })
335
+ const after = await findAudit(env, id)
336
+ return json(after ? auditView(after) : { id })
337
+ }
338
+
339
+ async function deleteDraft({ env, ctx }: RouteInput<Env>, id: string): Promise<Response> {
340
+ const user = requireUser(ctx)
341
+ refuseActingSession(ctx)
342
+ await editableAudit(env, ctx, id)
343
+ const now = nowIso()
344
+ const [audit] = await env.DB.batch([
345
+ env.DB.prepare("UPDATE \"audits\" SET deleted_at = ?, updated_at = ?, updated_by = ? WHERE id = ? AND created_by = ? AND status = 'draft' AND deleted_at IS NULL").bind(now, now, user.sub, id, user.sub),
346
+ env.DB.prepare(
347
+ "UPDATE \"audit_answers\" SET deleted_at = ? WHERE audit_id = ? AND deleted_at IS NULL AND EXISTS (SELECT 1 FROM \"audits\" WHERE id = ? AND deleted_at = ?)",
348
+ ).bind(now, id, id, now),
349
+ ])
350
+ if (Number(audit?.meta.changes ?? 0) !== 1) throw new OneError(409, 'APP-E-020', 'Tamamlanmış denetim silinemez.')
351
+ logAudit(ctx, 'audit.delete', id)
352
+ return new Response(null, { status: 204 })
353
+ }
354
+
355
+ const trDate = (iso: string): string => new Intl.DateTimeFormat('tr-TR', { timeZone: 'Europe/Istanbul', day: '2-digit', month: '2-digit', year: 'numeric' }).format(new Date(iso))
356
+
357
+ async function report({ env, ctx, url }: RouteInput<Env>, id: string): Promise<Response> {
358
+ const { audit } = await visibleAudit(env, ctx, id)
359
+ const lang = url.searchParams.get('_lang') === 'en' ? 'en' : 'tr'
360
+ const answers = await answersOf(env, id)
361
+ const en = lang === 'en'
362
+ const columns: ExportColumn[] = [
363
+ { key: 'position', title: { tr: 'Sıra', en: 'No.' }, type: 'number', width: 6 },
364
+ { key: 'section', title: { tr: 'Bölüm', en: 'Section' } },
365
+ { key: 'title', title: { tr: 'Madde', en: 'Item' }, width: 40 },
366
+ { key: 'weight', title: { tr: 'Ağırlık', en: 'Weight' }, type: 'number', width: 8 },
367
+ { key: 'critical', title: { tr: 'Kritik', en: 'Critical' }, type: 'boolean', width: 8 },
368
+ { key: 'result', title: { tr: 'Sonuç', en: 'Result' }, labels: RESULT_LABELS },
369
+ { key: 'note', title: { tr: 'Not', en: 'Note' }, width: 40 },
370
+ ]
371
+ const rows = answers.map((a) => ({ position: a.position, section: a.section ?? '', title: a.title, weight: a.weight, critical: a.critical === 1, result: a.result ?? '', note: a.note ?? '' }))
372
+ const resultLabel = audit.result ? (AUDIT_RESULT_LABELS[audit.result]?.[lang] ?? audit.result) : ''
373
+ const subtitle = [
374
+ audit.checklist_name ?? '',
375
+ `${en ? 'Auditor' : 'Denetçi'}: ${audit.auditor_name ?? '-'}`,
376
+ audit.submitted_at ? `${en ? 'Date' : 'Tarih'}: ${trDate(audit.submitted_at)}` : en ? 'Draft (not completed)' : 'Taslak (tamamlanmadı)',
377
+ audit.score !== null ? `${en ? 'Score' : 'Puan'}: ${audit.score} - ${resultLabel}${audit.critical_failed === 1 ? (en ? ' (critical item failed)' : ' (kritik madde uygun değil)') : ''}` : '',
378
+ ]
379
+ .filter(Boolean)
380
+ .join(' - ')
381
+ const title = `${en ? 'Audit report' : 'Denetim raporu'}: ${audit.location_name}`
382
+ return reportResponse(renderPrintableReport({ title, subtitle, columns, rows, lang, orientation: 'landscape' }))
383
+ }
384
+
385
+ async function photo({ env, ctx }: RouteInput<Env>, auditId: string, answerId: string): Promise<Response> {
386
+ await visibleAudit(env, ctx, auditId)
387
+ const row = await env.DB.prepare('SELECT photo FROM "audit_answers" WHERE id = ? AND audit_id = ? AND deleted_at IS NULL').bind(answerId, auditId).first<{ photo: string | null }>()
388
+ if (!row?.photo) throw new OneError(404, 'APP-E-404', 'Bu maddede fotoğraf yok.')
389
+ return fileResponse(env.FILES, row.photo)
390
+ }
391
+
392
+ async function dashboard({ env, ctx }: RouteInput<Env>): Promise<Response> {
393
+ requirePermission(ctx, MANAGE_PERMISSION)
394
+ const since = new Date(Date.now() - 30 * 86_400_000).toISOString()
395
+ const [totals, locations, recent] = await Promise.all([summary(env, since), locationScores(env, 12), recentAudits(env, 20)])
396
+ return json({ summary: totals, locations, recent })
397
+ }
398
+
399
+ async function routes(input: RouteInput<Env>): Promise<Response | null> {
400
+ const { url, method, env, ctx } = input
401
+ const path = url.pathname
402
+ if (path === '/api/files' && method === 'POST') return uploadFile(input.request, env.FILES, ctx)
403
+ if (path === '/api/workplaces' && method === 'GET') return workplaces(input)
404
+ if (path === '/api/dashboard' && method === 'GET') return dashboard(input)
405
+ if (path === allAudits.path || path.startsWith(`${allAudits.path}/`)) {
406
+ if (method !== 'GET' && method !== 'HEAD') throw new OneError(405, 'APP-E-405', 'Bu liste yalnız okunur.')
407
+ return allAudits.handle(input.request, env.DB, ctx)
408
+ }
409
+ if (path === checklistItems.path && method === 'POST') return itemWrite(input, null)
410
+ const itemId = matchId(path, checklistItems.path)
411
+ if (itemId && (method === 'PATCH' || method === 'DELETE')) return itemWrite(input, itemId)
412
+ if (path === checklistItems.path || path.startsWith(`${checklistItems.path}/`)) return checklistItems.handle(input.request, env.DB, ctx)
413
+ if (path === checklists.path || path.startsWith(`${checklists.path}/`)) return checklists.handle(input.request, env.DB, ctx)
414
+
415
+ const photoMatch = PHOTO_RE.exec(path)
416
+ if (photoMatch && method === 'GET') return photo(input, photoMatch[1]!, photoMatch[2]!)
417
+ if (path === myAudits.path && method === 'POST') return createAudit(input)
418
+ let id = matchId(path, myAudits.path, 'answers')
419
+ if (id && method === 'GET') return readAnswers(input, id)
420
+ if (id && method === 'PUT') return saveAnswers(input, id)
421
+ id = matchId(path, myAudits.path, 'submit')
422
+ if (id && method === 'POST') return submit(input, id)
423
+ id = matchId(path, myAudits.path, 'report.html')
424
+ if (id && method === 'GET') return report(input, id)
425
+ id = matchId(path, myAudits.path)
426
+ if (id && method === 'DELETE') return deleteDraft(input, id)
427
+ if (path === myAudits.path || path.startsWith(`${myAudits.path}/`)) {
428
+ if (method !== 'GET' && method !== 'HEAD') throw new OneError(405, 'APP-E-405', 'Denetim bu adresten değiştirilemez; cevaplar için denetimin cevap ucunu kullanın.')
429
+ return myAudits.handle(input.request, env.DB, ctx)
430
+ }
431
+ return null
432
+ }
433
+
434
+ export const definition: Omit<AppDefinition<Env>, 'guide'> = {
435
+ manifest,
436
+ tools: agentTools,
437
+ page: renderShell,
438
+ script: renderShellScript,
439
+ routes,
440
+ }
441
+
442
+ export function buildApp(guide: string) {
443
+ return createApp<Env>({ ...definition, guide })
444
+ }