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,143 @@
1
+ // Veritabanı okumaları — pano, rapor ve Devoplus yapay zekâsı araçları aynı sorguları kullanır. Yalnız TAMAMLANMIŞ
2
+ // denetimler sayılır (taslağın puanı yoktur).
3
+ import type { BaseEnv } from './lib/server.ts'
4
+
5
+ const DONE = "status = 'completed' AND deleted_at IS NULL"
6
+
7
+ export interface AuditRow {
8
+ id: string
9
+ created_by: string
10
+ checklist_id: string
11
+ checklist_name: string | null
12
+ location_id: string | null
13
+ location_code: string | null
14
+ location_name: string
15
+ auditor_name: string | null
16
+ status: 'draft' | 'completed'
17
+ submitted_at: string | null
18
+ score: number | null
19
+ result: string | null
20
+ critical_failed: number
21
+ compliant_count: number
22
+ noncompliant_count: number
23
+ not_applicable_count: number
24
+ general_note: string | null
25
+ created_at: string
26
+ updated_at: string
27
+ }
28
+
29
+ export interface AnswerRow {
30
+ id: string
31
+ item_id: string
32
+ position: number
33
+ section: string | null
34
+ title: string
35
+ weight: number
36
+ critical: number
37
+ result: 'compliant' | 'noncompliant' | 'not_applicable' | null
38
+ note: string | null
39
+ photo: string | null
40
+ photo_name: string | null
41
+ }
42
+
43
+ const AUDIT_COLUMNS =
44
+ 'id, created_by, checklist_id, checklist_name, location_id, location_code, location_name, auditor_name, status, submitted_at, score, result, ' +
45
+ 'critical_failed, compliant_count, noncompliant_count, not_applicable_count, general_note, created_at, updated_at'
46
+
47
+ export async function findAudit(env: BaseEnv, id: string): Promise<AuditRow | null> {
48
+ return env.DB.prepare(`SELECT ${AUDIT_COLUMNS} FROM "audits" WHERE id = ? AND deleted_at IS NULL`).bind(id).first<AuditRow>()
49
+ }
50
+
51
+ export async function answersOf(env: BaseEnv, auditId: string): Promise<AnswerRow[]> {
52
+ const rows = await env.DB.prepare(
53
+ 'SELECT id, item_id, position, section, title, weight, critical, result, note, photo, photo_name FROM "audit_answers" WHERE audit_id = ? AND deleted_at IS NULL ORDER BY position, created_at',
54
+ )
55
+ .bind(auditId)
56
+ .all<AnswerRow>()
57
+ return rows.results
58
+ }
59
+
60
+ const round1 = (n: number | null): number | null => (n === null ? null : Math.round(n * 10) / 10)
61
+
62
+ /** Son tamamlanmış denetimler (yer adında geçen ifadeyle süzülebilir; büyük/küçük harf yalnız Latin harflerde yok sayılır). */
63
+ export async function recentAudits(env: BaseEnv, limit: number, location?: string) {
64
+ const q = (location ?? '').trim().toLowerCase()
65
+ const rows = await env.DB.prepare(
66
+ `SELECT id, location_name, checklist_name, auditor_name, submitted_at, score, result, critical_failed FROM "audits" WHERE ${DONE}` +
67
+ (q ? ' AND instr(lower(location_name), ?) > 0' : '') +
68
+ ' ORDER BY submitted_at DESC LIMIT ?',
69
+ )
70
+ .bind(...(q ? [q, limit] : [limit]))
71
+ .all<{ id: string; location_name: string; checklist_name: string | null; auditor_name: string | null; submitted_at: string; score: number; result: string; critical_failed: number }>()
72
+ return rows.results.map((r) => ({ ...r, critical_failed: r.critical_failed === 1 }))
73
+ }
74
+
75
+ /** Uygun bulunmayan maddelerin başlıkları (denetim başına en çok `limit`). */
76
+ export async function failedItems(env: BaseEnv, auditIds: readonly string[], limit = 10): Promise<Map<string, string[]>> {
77
+ const out = new Map<string, string[]>()
78
+ if (auditIds.length === 0) return out
79
+ const rows = await env.DB.prepare(
80
+ `SELECT audit_id, title, critical FROM "audit_answers" WHERE result = 'noncompliant' AND deleted_at IS NULL AND audit_id IN (${auditIds.map(() => '?').join(', ')}) ORDER BY critical DESC, position`,
81
+ )
82
+ .bind(...auditIds)
83
+ .all<{ audit_id: string; title: string; critical: number }>()
84
+ for (const r of rows.results) {
85
+ const list = out.get(r.audit_id) ?? []
86
+ if (list.length < limit) list.push(r.critical === 1 ? `${r.title} (kritik)` : r.title)
87
+ out.set(r.audit_id, list)
88
+ }
89
+ return out
90
+ }
91
+
92
+ /**
93
+ * Yer başına: tamamlanmış denetim sayısı, son puan, ortalama puan, son tarih (sayılar sunucuda hesaplanır). Gruplama
94
+ * yerin KİMLİĞİYLE yapılır (org şemasındaki iş yeri `location_id`); aynı adı taşıyan iki iş yeri ("Merkez") ayrı satırdır.
95
+ * Elle yazılan yerlerin kimliği olmadığı için onlar adlarıyla ayrı bir anahtar uzayında gruplanır. Etiket son denetimdeki
96
+ * addır; mağaza kodu varsa yanına eklenir ki aynı adlı satırlar ayırt edilebilsin.
97
+ */
98
+ export async function locationScores(env: BaseEnv, limit = 50) {
99
+ const rows = await env.DB.prepare(
100
+ `WITH done AS (SELECT a.*, COALESCE('id:' || a.location_id, 'name:' || a.location_name) AS loc_key FROM "audits" a WHERE a.status = 'completed' AND a.deleted_at IS NULL) ` +
101
+ `SELECT d.loc_key, COUNT(*) AS audits, AVG(d.score) AS average, MAX(d.submitted_at) AS last_at, SUM(d.critical_failed) AS critical_failures, ` +
102
+ `(SELECT b.score FROM done b WHERE b.loc_key = d.loc_key ORDER BY b.submitted_at DESC LIMIT 1) AS last_score, ` +
103
+ `(SELECT b.location_name FROM done b WHERE b.loc_key = d.loc_key ORDER BY b.submitted_at DESC LIMIT 1) AS location_name, ` +
104
+ `(SELECT b.location_code FROM done b WHERE b.loc_key = d.loc_key ORDER BY b.submitted_at DESC LIMIT 1) AS location_code ` +
105
+ `FROM done d GROUP BY d.loc_key ORDER BY last_at DESC LIMIT ?`,
106
+ )
107
+ .bind(limit)
108
+ .all<{
109
+ loc_key: string
110
+ location_name: string
111
+ location_code: string | null
112
+ audits: number
113
+ average: number | null
114
+ last_at: string
115
+ last_score: number | null
116
+ critical_failures: number
117
+ }>()
118
+ return rows.results.map(({ loc_key, location_name, location_code, ...r }) => ({
119
+ ...r,
120
+ key: loc_key,
121
+ location: location_code ? `${location_name} (${location_code})` : location_name,
122
+ average: round1(r.average),
123
+ critical_failures: Number(r.critical_failures ?? 0),
124
+ }))
125
+ }
126
+
127
+ /** Pano özeti: son 30 gün. */
128
+ export async function summary(env: BaseEnv, since: string) {
129
+ const row = await env.DB.prepare(
130
+ `SELECT COUNT(*) AS audits, AVG(score) AS average, SUM(CASE WHEN result = 'failed' THEN 1 ELSE 0 END) AS failed, SUM(critical_failed) AS critical ` +
131
+ `FROM "audits" WHERE ${DONE} AND submitted_at >= ?`,
132
+ )
133
+ .bind(since)
134
+ .first<{ audits: number; average: number | null; failed: number | null; critical: number | null }>()
135
+ const drafts = await env.DB.prepare(`SELECT COUNT(*) AS n FROM "audits" WHERE status = 'draft' AND deleted_at IS NULL`).first<{ n: number }>()
136
+ return {
137
+ audits: row?.audits ?? 0,
138
+ average: round1(row?.average ?? null),
139
+ failed: Number(row?.failed ?? 0),
140
+ criticalFailures: Number(row?.critical ?? 0),
141
+ drafts: drafts?.n ?? 0,
142
+ }
143
+ }
@@ -0,0 +1,48 @@
1
+ import { describe, expect, it } from 'vitest'
2
+ import { CRITICAL_CAP, computeScore, isAnswerResult, isValidWeight, resultFor, submitProblems, type ScoredItem } from './score.ts'
3
+
4
+ const item = (result: ScoredItem['result'], weight = 1, critical = false): ScoredItem => ({ weight, critical, result })
5
+
6
+ describe('denetim puanı', () => {
7
+ it('ağırlıklı: uygun ağırlık / değerlendirilen ağırlık; "uygulanamaz" hesaba girmez', () => {
8
+ const s = computeScore([item('compliant', 3), item('noncompliant', 1), item('not_applicable', 5)])
9
+ expect(s).toEqual({ score: 75, result: 'needs_improvement', criticalFailed: false, compliant: 1, noncompliant: 1, notApplicable: 1 })
10
+ expect(computeScore([item('compliant', 2), item('compliant', 1), item('noncompliant', 1)]).score).toBe(75)
11
+ expect(computeScore([item('compliant', 1), item('noncompliant', 2)]).score).toBe(33.3)
12
+ })
13
+
14
+ it('kritik madde uygun değilse sonuç başarısız ve puan tavanda', () => {
15
+ const s = computeScore([item('compliant', 10), item('compliant', 10), item('noncompliant', 1, true)])
16
+ expect(s.criticalFailed).toBe(true)
17
+ expect(s.score).toBe(CRITICAL_CAP)
18
+ expect(s.result).toBe('failed')
19
+ // Tavanın altındaki puan olduğu gibi kalır.
20
+ expect(computeScore([item('noncompliant', 5, true), item('compliant', 1)]).score).toBe(16.7)
21
+ })
22
+
23
+ it('bantlar', () => {
24
+ expect(resultFor(85, false)).toBe('passed')
25
+ expect(resultFor(84.9, false)).toBe('needs_improvement')
26
+ expect(resultFor(60, false)).toBe('needs_improvement')
27
+ expect(resultFor(59.9, false)).toBe('failed')
28
+ expect(resultFor(100, true)).toBe('failed')
29
+ })
30
+
31
+ it('tamamlama eksikleri: cevapsız, notsuz "uygun değil", hiç değerlendirilmemiş', () => {
32
+ expect(submitProblems([item(null), { ...item('noncompliant'), note: ' ' }])).toHaveLength(2)
33
+ expect(submitProblems([{ ...item('noncompliant'), note: 'Raf etiketi yok' }])).toEqual([])
34
+ expect(submitProblems([item('not_applicable'), item('not_applicable')])).toHaveLength(1)
35
+ expect(submitProblems([])).toHaveLength(1)
36
+ expect(() => computeScore([item(null)])).toThrow()
37
+ expect(() => computeScore([item('not_applicable')])).toThrow()
38
+ })
39
+
40
+ it('cevap ve ağırlık doğrulama', () => {
41
+ expect(isAnswerResult('compliant')).toBe(true)
42
+ expect(isAnswerResult('ok')).toBe(false)
43
+ expect(isValidWeight(3)).toBe(true)
44
+ expect(isValidWeight(0)).toBe(false)
45
+ expect(isValidWeight(2.5)).toBe(false)
46
+ expect(isValidWeight(11)).toBe(false)
47
+ })
48
+ })
@@ -0,0 +1,102 @@
1
+ // Denetim kuralları — SAF fonksiyonlar (veritabanı ve ağ yok; `score.test.ts` sınar). Puan YALNIZ burada, sunucuda
2
+ // hesaplanır; istemcinin gönderdiği puan kabul edilmez. Kuralı değiştiren bu dosyayı ve testini birlikte değiştirir.
3
+ //
4
+ // puan = uygun maddelerin ağırlığı / değerlendirilen maddelerin ağırlığı × 100 (bir ondalık)
5
+ // "uygulanamaz" maddeler hesaba GİRMEZ (ne paya ne paydaya)
6
+ // kritik bir madde "uygun değil" ise sonuç her zaman BAŞARISIZ'dır ve puan CRITICAL_CAP'i aşamaz
7
+ // sonuç: puan ≥ PASS_MIN → başarılı · ≥ IMPROVE_MIN → geliştirilmeli · altı → başarısız
8
+ //
9
+ // ⚠️ İş kuralıdır — eşikleri ve kritik madde davranışını keşif görüşmesinde kullanıcıyla netleştirin.
10
+ export const ANSWER_RESULTS = ['compliant', 'noncompliant', 'not_applicable'] as const
11
+ export type AnswerResult = (typeof ANSWER_RESULTS)[number]
12
+ export type AuditResult = 'passed' | 'needs_improvement' | 'failed'
13
+
14
+ /** Başarılı sayılmanın alt sınırı (dahil). */
15
+ export const PASS_MIN = 85
16
+ /** "Geliştirilmeli" sayılmanın alt sınırı (dahil); altı başarısızdır. */
17
+ export const IMPROVE_MIN = 60
18
+ /** Kritik madde uygun değilse puanın çıkabileceği en yüksek değer (başarısız bandının içinde kalır). */
19
+ export const CRITICAL_CAP = IMPROVE_MIN - 1
20
+ /** Madde ağırlığı: tam sayı, 1-10. */
21
+ export const WEIGHT_MIN = 1
22
+ export const WEIGHT_MAX = 10
23
+ /** Bir kontrol listesindeki en çok madde (form ve rapor bu kadarıyla sınanmıştır). */
24
+ export const CHECKLIST_ITEMS_MAX = 60
25
+ /** Madde notunun en çok uzunluğu. */
26
+ export const NOTE_MAX = 500
27
+
28
+ export interface ScoredItem {
29
+ weight: number
30
+ critical: boolean
31
+ result: AnswerResult | null
32
+ }
33
+
34
+ export interface AuditScore {
35
+ /** 0-100, bir ondalık. */
36
+ score: number
37
+ result: AuditResult
38
+ criticalFailed: boolean
39
+ compliant: number
40
+ noncompliant: number
41
+ notApplicable: number
42
+ }
43
+
44
+ export function isAnswerResult(value: unknown): value is AnswerResult {
45
+ return typeof value === 'string' && (ANSWER_RESULTS as readonly string[]).includes(value)
46
+ }
47
+
48
+ /** Ağırlık doğrulama (madde tanımında): tam sayı, WEIGHT_MIN-WEIGHT_MAX. */
49
+ export function isValidWeight(value: unknown): value is number {
50
+ return typeof value === 'number' && Number.isInteger(value) && value >= WEIGHT_MIN && value <= WEIGHT_MAX
51
+ }
52
+
53
+ /** Puan bandı → sonuç. */
54
+ export function resultFor(score: number, criticalFailed: boolean): AuditResult {
55
+ if (criticalFailed || score < IMPROVE_MIN) return 'failed'
56
+ return score >= PASS_MIN ? 'passed' : 'needs_improvement'
57
+ }
58
+
59
+ /**
60
+ * Tamamlamadan önceki eksikler (madde sırası 1'den başlar): cevaplanmamış madde, notsuz "uygun değil" ve hiç
61
+ * değerlendirilmemiş (hepsi "uygulanamaz") denetim. Boş liste = tamamlanabilir.
62
+ */
63
+ export function submitProblems(items: readonly (ScoredItem & { note?: string | null })[]): string[] {
64
+ const problems: string[] = []
65
+ items.forEach((item, i) => {
66
+ if (item.result === null) problems.push(`${i + 1}. madde cevaplanmadı.`)
67
+ else if (item.result === 'noncompliant' && !(item.note ?? '').trim()) problems.push(`${i + 1}. madde "uygun değil": ne gördüğünüzü nota yazın.`)
68
+ })
69
+ if (items.length === 0) problems.push('Denetimde madde yok.')
70
+ else if (problems.length === 0 && items.every((item) => item.result === 'not_applicable'))
71
+ problems.push('En az bir madde "uygun" ya da "uygun değil" olarak değerlendirilmeli.')
72
+ return problems
73
+ }
74
+
75
+ /** Ağırlıklı puan. Önce `submitProblems` boş olmalıdır; değilse (cevapsız ya da hiç değerlendirilmemiş) fırlatır. */
76
+ export function computeScore(items: readonly ScoredItem[]): AuditScore {
77
+ let earned = 0
78
+ let possible = 0
79
+ let compliant = 0
80
+ let noncompliant = 0
81
+ let notApplicable = 0
82
+ let criticalFailed = false
83
+ for (const item of items) {
84
+ if (item.result === null) throw new Error('cevaplanmamış madde var')
85
+ if (item.result === 'not_applicable') {
86
+ notApplicable++
87
+ continue
88
+ }
89
+ possible += item.weight
90
+ if (item.result === 'compliant') {
91
+ compliant++
92
+ earned += item.weight
93
+ } else {
94
+ noncompliant++
95
+ if (item.critical) criticalFailed = true
96
+ }
97
+ }
98
+ if (possible === 0) throw new Error('değerlendirilen madde yok')
99
+ let score = Math.round((earned / possible) * 1000) / 10
100
+ if (criticalFailed) score = Math.min(score, CRITICAL_CAP)
101
+ return { score, result: resultFor(score, criticalFailed), criticalFailed, compliant, noncompliant, notApplicable }
102
+ }