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.
- package/README.md +62 -2
- package/dist/cli.mjs +17 -0
- package/package.json +35 -3
- package/templates/0001_init.sql.tpl +10 -0
- package/templates/AGENTS.md.tpl +286 -0
- package/templates/AGENT_GUIDE.md.tpl +36 -0
- package/templates/README.md.tpl +58 -0
- package/templates/_shared/AGENTS.writes.md.tpl +38 -0
- package/templates/_shared/README.tail.md.tpl +20 -0
- package/templates/_shared/files/src/app.test.ts.tpl +36 -0
- package/templates/_shared/files/src/index.ts.tpl +8 -0
- package/templates/_shared/files/src/lib/client.ts.tpl +213 -0
- package/templates/_shared/files/src/lib/files.ts.tpl +37 -0
- package/templates/_shared/files/src/lib/http.ts.tpl +101 -0
- package/templates/_shared/files/src/lib/mail.ts.tpl +26 -0
- package/templates/_shared/files/src/lib/model.ts.tpl +31 -0
- package/templates/_shared/files/src/lib/page.ts.tpl +71 -0
- package/templates/_shared/files/src/lib/record.ts.tpl +246 -0
- package/templates/_shared/files/src/lib/server.ts.tpl +132 -0
- package/templates/_shared/files/src/shell.test.ts.tpl +56 -0
- package/templates/agent.ts.tpl +23 -0
- package/templates/bakim-takvimi/AGENTS.template.md.tpl +40 -0
- package/templates/bakim-takvimi/README.md.tpl +45 -0
- package/templates/bakim-takvimi/files/AGENT_GUIDE.md.tpl +52 -0
- package/templates/bakim-takvimi/files/one.data.json +45 -0
- package/templates/bakim-takvimi/files/src/agent.ts.tpl +22 -0
- package/templates/bakim-takvimi/files/src/app.ts.tpl +259 -0
- package/templates/bakim-takvimi/files/src/jobs.ts.tpl +170 -0
- package/templates/bakim-takvimi/files/src/names.ts.tpl +5 -0
- package/templates/bakim-takvimi/files/src/queries.ts.tpl +71 -0
- package/templates/bakim-takvimi/files/src/schedule.test.ts.tpl +58 -0
- package/templates/bakim-takvimi/files/src/schedule.ts.tpl +106 -0
- package/templates/bakim-takvimi/files/src/shell.ts.tpl +299 -0
- package/templates/bakim-takvimi/manifest.json.tpl +64 -0
- package/templates/egitim-sertifika/AGENTS.template.md.tpl +41 -0
- package/templates/egitim-sertifika/README.md.tpl +46 -0
- package/templates/egitim-sertifika/files/AGENT_GUIDE.md.tpl +53 -0
- package/templates/egitim-sertifika/files/one.data.json +43 -0
- package/templates/egitim-sertifika/files/src/agent.ts.tpl +22 -0
- package/templates/egitim-sertifika/files/src/app.ts.tpl +312 -0
- package/templates/egitim-sertifika/files/src/certificate.test.ts.tpl +83 -0
- package/templates/egitim-sertifika/files/src/certificate.ts.tpl +163 -0
- package/templates/egitim-sertifika/files/src/jobs.ts.tpl +219 -0
- package/templates/egitim-sertifika/files/src/names.ts.tpl +5 -0
- package/templates/egitim-sertifika/files/src/queries.ts.tpl +115 -0
- package/templates/egitim-sertifika/files/src/shell.ts.tpl +345 -0
- package/templates/egitim-sertifika/manifest.json.tpl +57 -0
- package/templates/envanter/AGENTS.template.md.tpl +28 -0
- package/templates/envanter/README.md.tpl +40 -0
- package/templates/envanter/files/AGENT_GUIDE.md.tpl +47 -0
- package/templates/envanter/files/one.data.json +62 -0
- package/templates/envanter/files/src/agent.ts.tpl +20 -0
- package/templates/envanter/files/src/app.ts.tpl +204 -0
- package/templates/envanter/files/src/jobs.ts.tpl +30 -0
- package/templates/envanter/files/src/queries.ts.tpl +53 -0
- package/templates/envanter/files/src/shell.ts.tpl +265 -0
- package/templates/envanter/files/src/stock.test.ts.tpl +31 -0
- package/templates/envanter/files/src/stock.ts.tpl +33 -0
- package/templates/envanter/manifest.json.tpl +56 -0
- package/templates/hedef-takibi/AGENTS.template.md.tpl +37 -0
- package/templates/hedef-takibi/README.md.tpl +52 -0
- package/templates/hedef-takibi/files/AGENT_GUIDE.md.tpl +48 -0
- package/templates/hedef-takibi/files/one.data.json +76 -0
- package/templates/hedef-takibi/files/src/agent.ts.tpl +26 -0
- package/templates/hedef-takibi/files/src/app.ts.tpl +740 -0
- package/templates/hedef-takibi/files/src/progress.test.ts.tpl +53 -0
- package/templates/hedef-takibi/files/src/progress.ts.tpl +81 -0
- package/templates/hedef-takibi/files/src/queries.ts.tpl +83 -0
- package/templates/hedef-takibi/files/src/shell.ts.tpl +455 -0
- package/templates/hedef-takibi/manifest.json.tpl +38 -0
- package/templates/index.ts.tpl +262 -0
- package/templates/markdown.d.ts.tpl +6 -0
- package/templates/musteri-ziyaret/AGENTS.template.md.tpl +47 -0
- package/templates/musteri-ziyaret/README.md.tpl +48 -0
- package/templates/musteri-ziyaret/files/AGENT_GUIDE.md.tpl +44 -0
- package/templates/musteri-ziyaret/files/one.data.json +53 -0
- package/templates/musteri-ziyaret/files/src/agent.ts.tpl +21 -0
- package/templates/musteri-ziyaret/files/src/app.ts.tpl +340 -0
- package/templates/musteri-ziyaret/files/src/customers.ts.tpl +85 -0
- package/templates/musteri-ziyaret/files/src/queries.ts.tpl +67 -0
- package/templates/musteri-ziyaret/files/src/shell.ts.tpl +272 -0
- package/templates/musteri-ziyaret/files/src/visit.test.ts.tpl +65 -0
- package/templates/musteri-ziyaret/files/src/visit.ts.tpl +98 -0
- package/templates/musteri-ziyaret/manifest.json.tpl +37 -0
- package/templates/saha-denetimi/AGENTS.template.md.tpl +32 -0
- package/templates/saha-denetimi/README.md.tpl +44 -0
- package/templates/saha-denetimi/files/AGENT_GUIDE.md.tpl +55 -0
- package/templates/saha-denetimi/files/one.data.json +87 -0
- package/templates/saha-denetimi/files/src/agent.ts.tpl +41 -0
- package/templates/saha-denetimi/files/src/app.ts.tpl +444 -0
- package/templates/saha-denetimi/files/src/queries.ts.tpl +143 -0
- package/templates/saha-denetimi/files/src/score.test.ts.tpl +48 -0
- package/templates/saha-denetimi/files/src/score.ts.tpl +102 -0
- package/templates/saha-denetimi/files/src/shell.ts.tpl +585 -0
- package/templates/saha-denetimi/manifest.json.tpl +49 -0
- package/templates/shell.test.ts.tpl +30 -0
- package/templates/shell.ts.tpl +201 -0
- package/templates/uygunsuzluk-dof/AGENTS.template.md.tpl +40 -0
- package/templates/uygunsuzluk-dof/README.md.tpl +46 -0
- package/templates/uygunsuzluk-dof/files/AGENT_GUIDE.md.tpl +56 -0
- package/templates/uygunsuzluk-dof/files/one.data.json +99 -0
- package/templates/uygunsuzluk-dof/files/src/agent.ts.tpl +45 -0
- package/templates/uygunsuzluk-dof/files/src/app.ts.tpl +431 -0
- package/templates/uygunsuzluk-dof/files/src/capa.test.ts.tpl +53 -0
- package/templates/uygunsuzluk-dof/files/src/capa.ts.tpl +114 -0
- package/templates/uygunsuzluk-dof/files/src/jobs.ts.tpl +127 -0
- package/templates/uygunsuzluk-dof/files/src/queries.ts.tpl +102 -0
- package/templates/uygunsuzluk-dof/files/src/shell.ts.tpl +441 -0
- package/templates/uygunsuzluk-dof/manifest.json.tpl +56 -0
- package/templates/ziyaretci-kaydi/AGENTS.template.md.tpl +35 -0
- package/templates/ziyaretci-kaydi/README.md.tpl +46 -0
- package/templates/ziyaretci-kaydi/files/AGENT_GUIDE.md.tpl +39 -0
- package/templates/ziyaretci-kaydi/files/one.data.json +42 -0
- package/templates/ziyaretci-kaydi/files/src/agent.ts.tpl +13 -0
- package/templates/ziyaretci-kaydi/files/src/app.ts.tpl +293 -0
- package/templates/ziyaretci-kaydi/files/src/jobs.ts.tpl +49 -0
- package/templates/ziyaretci-kaydi/files/src/notice.ts.tpl +48 -0
- package/templates/ziyaretci-kaydi/files/src/queries.ts.tpl +45 -0
- package/templates/ziyaretci-kaydi/files/src/shell.ts.tpl +252 -0
- package/templates/ziyaretci-kaydi/files/src/visitor.test.ts.tpl +34 -0
- package/templates/ziyaretci-kaydi/files/src/visitor.ts.tpl +57 -0
- 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
|
+
}
|