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,127 @@
1
+ // Zamanlanmış görev (manifest `jobs`): her sabah 08:00 (İstanbul) termini geçmiş açık faaliyetleri sorumlusuna e-postayla
2
+ // hatırlatır. Zamanlanmış iş KULLANICISIZDIR: e-posta kişi adına değil UYGULAMANIN kimliğiyle gider. Bunun için manifest
3
+ // `jobs.services` içinde `mail.execute` beyan edilir VE kurulumda portal yöneticisi bu servisi ONAYLAR; onay yoksa
4
+ // Devoplus 403 döner — iş bunu "onay gerekiyor" olarak kaydeder (uygulamada ve Devoplus yapay zekâsında görünür).
5
+ //
6
+ // ⛔ "Günde bir kez" kuralı damgadadır: faaliyet önce o günün anahtarıyla koşullu olarak damgalanır (`reminded_on`), e-posta
7
+ // yalnız damgayı koyabilen faaliyetler için gider. Gönderim başarısızsa damga GERİ ALINIR (önceki değerine döner): ertesi
8
+ // gün yeniden denenir. Aynı gün ikinci kez gelen iş (yeniden deneme) aynı kişiye ikinci e-posta göndermez.
9
+ // Bir koşumda en çok REMINDER_OWNERS_PER_RUN kişiye gider; en uzun süredir hatırlatılmayan önce gelir, kalanlar ertesi güne.
10
+ // Hatırlatması ulaşmamış sorumlu (`reminder_failed_on`) sıranın SONUNA geçer: hiç ulaşamayan bir kişi (ör. ayrılmış bir
11
+ // çalışan) damgası geri alındığı için hep "en uzun süredir hatırlatılmayan" kalır ve öteki kişileri hatırlatmasız bırakırdı.
12
+ import { BackendClient, defineJobs, foldSearchText, logError, notify, systemNotice, type OneContext, type Recipients, type ScenarioMessage } from '@devoplus/one-sdk'
13
+ import { APP_NAME, type Env } from './app.ts'
14
+ import { REMINDER_ACTIONS_PER_MAIL, REMINDER_OWNERS_PER_RUN, istanbulDay, reminderMessage } from './capa.ts'
15
+ import type { MailOutcome } from './lib/mail.ts'
16
+ import { OVERDUE } from './queries.ts'
17
+
18
+ /** Sistemin yazdığı kayıtlarda `created_by` değeri (kişi kimliği değildir). */
19
+ export const SYSTEM_ACTOR = 'system:overdue-reminders'
20
+
21
+ /** İşte e-posta: onaysız servis 403 → `consent-needed` (portal yöneticisinin kurulum onayı gerekiyor). */
22
+ async function sendJobMail(ctx: OneContext, api: BackendClient, message: ScenarioMessage, recipients: Recipients): Promise<MailOutcome> {
23
+ try {
24
+ const mail = (await notify(api, message, { ...recipients, channels: ['mail'] })).mail
25
+ if (mail && 'error' in mail) {
26
+ logError(ctx, 'hatırlatma e-postası gönderilemedi', mail.error)
27
+ return mail.error.status === 403 ? 'consent-needed' : 'failed'
28
+ }
29
+ // Sunucu isteği kabul edip HİÇBİR alıcıyı kuyruğa almadıysa (pasif kişi, adres yok, sınır…) ileti kimseye gitmedi:
30
+ // "gönderildi" sayılmaz, damga geri alınır ve sonraki koşumda yeniden denenir.
31
+ if (!mail || mail.queued === 0) {
32
+ logError(ctx, 'hatırlatma e-postası hiçbir alıcıya ulaşmadı', mail?.skipped ?? [])
33
+ return 'failed'
34
+ }
35
+ return 'sent'
36
+ } catch (error) {
37
+ logError(ctx, 'hatırlatma e-postası gönderilemedi', error)
38
+ return 'failed'
39
+ }
40
+ }
41
+
42
+ export const jobs = defineJobs<Env>({
43
+ schedules: {
44
+ 'overdue-reminders': async (job, env, ctx) => {
45
+ const at = job.scheduledTime ? new Date(job.scheduledTime) : new Date()
46
+ const today = istanbulDay(at)
47
+ const pending = '(a.reminded_on IS NULL OR a.reminded_on <> ?)'
48
+ // Bu koşumda yazılacak kişiler SIRAYLA seçilir; bekleyen kişi SAYISI ayrı sayılır (okuma sınırıyla kesilmez): turun
49
+ // "ertelenen" sayısı, kaç kişi olursa olsun tamdır.
50
+ const [owners, waiting] = await env.DB.batch<{ owner: string; n: number }>([
51
+ env.DB.prepare(
52
+ `SELECT a.owner_org_id AS owner, MIN(coalesce(a.reminded_on, '')) AS last, MAX(coalesce(a.reminder_failed_on, '')) AS failed FROM "actions" a WHERE ${OVERDUE} AND ${pending} ` +
53
+ 'GROUP BY a.owner_org_id ORDER BY failed, last, MIN(a.due_date) LIMIT ?',
54
+ ).bind(today, today, REMINDER_OWNERS_PER_RUN),
55
+ env.DB.prepare(`SELECT COUNT(DISTINCT a.owner_org_id) AS n FROM "actions" a WHERE ${OVERDUE} AND ${pending}`).bind(today, today),
56
+ ])
57
+ // Uygulamanın kimliğiyle (belirteçsiz): platform işte kendi kimliğini ekler.
58
+ const api = new BackendClient({ baseUrl: env.API_BASE, authorization: null })
59
+ const batch = owners?.results ?? []
60
+ const run = { owners: 0, actions: 0, failed: 0, deferred: Math.max(0, Number(waiting?.results[0]?.n ?? 0) - batch.length), consentNeeded: false }
61
+ for (let i = 0; i < batch.length; i++) {
62
+ const owner = batch[i]!.owner
63
+ const items = await env.DB.prepare(
64
+ `SELECT a.id, a.description, a.nonconformity_title, a.due_date, a.reminded_on FROM "actions" a WHERE a.owner_org_id = ? AND ${OVERDUE} AND ${pending} ORDER BY a.due_date, a.created_at`,
65
+ )
66
+ .bind(owner, today, today)
67
+ .all<{ id: string; description: string; nonconformity_title: string | null; due_date: string; reminded_on: string | null }>()
68
+ if (items.results.length === 0) continue
69
+ // ⛔ Damga koşulludur ve faaliyeti seçen koşulun TAMAMINI yeniden ister: eşzamanlı ikinci koşum aynı faaliyeti
70
+ // damgalayamaz; okumadan sonra faaliyet tamamlanır, termini ileri alınır, kaydı kapanır ya da faaliyet BAŞKASINA
71
+ // verilirse damgalanmaz ve bu kişiye giden e-postada yer almaz (yeni sorumlunun hatırlatması da bastırılmaz).
72
+ const stamps = await env.DB.batch(
73
+ items.results.map((item) =>
74
+ env.DB.prepare(`UPDATE "actions" AS a SET reminded_on = ?, reminded_on_fold = ? WHERE a.id = ? AND a.owner_org_id = ? AND ${OVERDUE} AND ${pending}`).bind(
75
+ today,
76
+ foldSearchText(today),
77
+ item.id,
78
+ owner,
79
+ today,
80
+ today,
81
+ ),
82
+ ),
83
+ )
84
+ const stamped = items.results.filter((_, k) => Number(stamps[k]?.meta.changes ?? 0) === 1)
85
+ if (stamped.length === 0) continue
86
+ const shown = stamped.slice(0, REMINDER_ACTIONS_PER_MAIL)
87
+ const text = reminderMessage(shown, today, stamped.length - shown.length)
88
+ const outcome = await sendJobMail(ctx, api, systemNotice({ appName: APP_NAME.tr, title: text.title, message: text.message }), { toOrgUserIds: [owner] })
89
+ if (outcome === 'sent') {
90
+ run.owners++
91
+ run.actions += stamped.length
92
+ // Ulaştı: önceki başarısız denemenin izi silinir (sıradaki yerine döner).
93
+ await env.DB.batch(stamped.map((item) => env.DB.prepare('UPDATE "actions" SET reminder_failed_on = NULL WHERE id = ? AND reminder_failed_on IS NOT NULL').bind(item.id)))
94
+ continue
95
+ }
96
+ // Gönderilemedi: damga önceki değerine döner (ertesi gün yeniden denenir); ileti ULAŞMADIYSA sıranın sonuna geçer.
97
+ await env.DB.batch(
98
+ stamped.map((item) =>
99
+ env.DB.prepare('UPDATE "actions" SET reminded_on = ?, reminded_on_fold = ?, reminder_failed_on = coalesce(?, reminder_failed_on) WHERE id = ? AND reminded_on = ?').bind(
100
+ item.reminded_on,
101
+ item.reminded_on === null ? null : foldSearchText(item.reminded_on),
102
+ outcome === 'failed' ? today : null,
103
+ item.id,
104
+ today,
105
+ ),
106
+ ),
107
+ )
108
+ if (outcome === 'consent-needed') {
109
+ // Servis kurulumda onaylanmamış: diğer kişiler için de aynı yanıt gelir, denemeye devam edilmez.
110
+ run.consentNeeded = true
111
+ run.deferred += batch.length - i
112
+ break
113
+ }
114
+ run.failed++
115
+ }
116
+ const now = at.toISOString()
117
+ await env.DB.batch([
118
+ env.DB.prepare(
119
+ 'INSERT OR IGNORE INTO "reminder_runs" (id, created_at, created_by, updated_at, updated_by, run_key, reminded_owners, reminded_actions, failed, deferred_owners, consent_needed) VALUES (?, ?, ?, ?, ?, ?, 0, 0, 0, 0, 0)',
120
+ ).bind(crypto.randomUUID(), now, SYSTEM_ACTOR, now, SYSTEM_ACTOR, today),
121
+ env.DB.prepare(
122
+ 'UPDATE "reminder_runs" SET reminded_owners = reminded_owners + ?, reminded_actions = reminded_actions + ?, failed = failed + ?, deferred_owners = ?, consent_needed = ?, updated_at = ?, updated_by = ? WHERE run_key = ? AND deleted_at IS NULL',
123
+ ).bind(run.owners, run.actions, run.failed, run.deferred, run.consentNeeded ? 1 : 0, now, SYSTEM_ACTOR, today),
124
+ ])
125
+ },
126
+ },
127
+ })
@@ -0,0 +1,102 @@
1
+ // Veritabanı okumaları — ekran, zamanlanmış iş ve Devoplus yapay zekâsı araçları aynı sorguları kullanır.
2
+ import type { BaseEnv } from './lib/server.ts'
3
+
4
+ export interface NcRow {
5
+ id: string
6
+ created_by: string
7
+ title: string
8
+ source: string
9
+ severity: string
10
+ description: string
11
+ location: string | null
12
+ detected_on: string | null
13
+ reporter_name: string | null
14
+ root_cause: string | null
15
+ status: 'open' | 'planned' | 'verification' | 'closed'
16
+ attachment: string | null
17
+ attachment_name: string | null
18
+ closed_at: string | null
19
+ closed_by_name: string | null
20
+ closure_note: string | null
21
+ /** Doğrulamada etkisiz bulunup planlamaya döndüğü son an; sonraki doğrulama bundan SONRA eklenmiş bir faaliyet ister. */
22
+ verification_failed_at: string | null
23
+ created_at: string
24
+ updated_at: string
25
+ }
26
+
27
+ export interface ActionRow {
28
+ id: string
29
+ nonconformity_id: string
30
+ nonconformity_title: string | null
31
+ description: string
32
+ owner_org_id: string
33
+ owner_name: string | null
34
+ due_date: string
35
+ status: 'open' | 'done'
36
+ done_at: string | null
37
+ done_note: string | null
38
+ done_by_name: string | null
39
+ reminded_on: string | null
40
+ created_at: string
41
+ }
42
+
43
+ const NC_COLUMNS =
44
+ 'id, created_by, title, source, severity, description, location, detected_on, reporter_name, root_cause, status, attachment, attachment_name, closed_at, closed_by_name, closure_note, verification_failed_at, created_at, updated_at'
45
+ const ACTION_COLUMNS =
46
+ 'id, nonconformity_id, nonconformity_title, description, owner_org_id, owner_name, due_date, status, done_at, done_note, done_by_name, reminded_on, created_at'
47
+
48
+ export async function findNc(env: BaseEnv, id: string): Promise<NcRow | null> {
49
+ return env.DB.prepare(`SELECT ${NC_COLUMNS} FROM "nonconformities" WHERE id = ? AND deleted_at IS NULL`).bind(id).first<NcRow>()
50
+ }
51
+
52
+ export async function findAction(env: BaseEnv, id: string): Promise<ActionRow | null> {
53
+ return env.DB.prepare(`SELECT ${ACTION_COLUMNS} FROM "actions" WHERE id = ? AND deleted_at IS NULL`).bind(id).first<ActionRow>()
54
+ }
55
+
56
+ export async function actionsOf(env: BaseEnv, ncId: string): Promise<ActionRow[]> {
57
+ const rows = await env.DB.prepare(`SELECT ${ACTION_COLUMNS} FROM "actions" WHERE nonconformity_id = ? AND deleted_at IS NULL ORDER BY due_date, created_at`).bind(ncId).all<ActionRow>()
58
+ return rows.results
59
+ }
60
+
61
+ /** Gecikmiş açık faaliyet: termini bugünden önce, kaydı silinmemiş ve kapanmamış. `a` takma adıyla kullanılır. */
62
+ export const OVERDUE =
63
+ "a.status = 'open' AND a.deleted_at IS NULL AND a.due_date < ? AND EXISTS (SELECT 1 FROM \"nonconformities\" n WHERE n.id = a.nonconformity_id AND n.deleted_at IS NULL AND n.status <> 'closed')"
64
+
65
+ /** Geciken faaliyetler (en çok geciken önce); sorumlunun adında geçen ifadeyle süzülebilir. */
66
+ export async function overdueActions(env: BaseEnv, today: string, limit: number, owner?: string) {
67
+ const q = (owner ?? '').trim().toLowerCase()
68
+ const rows = await env.DB.prepare(
69
+ `SELECT a.id, a.description, a.owner_name, a.due_date, a.nonconformity_title, n.severity FROM "actions" a JOIN "nonconformities" n ON n.id = a.nonconformity_id WHERE ${OVERDUE}` +
70
+ (q ? " AND instr(lower(coalesce(a.owner_name, '')), ?) > 0" : '') +
71
+ ' ORDER BY a.due_date, a.created_at LIMIT ?',
72
+ )
73
+ .bind(...(q ? [today, q, limit] : [today, limit]))
74
+ .all<{ id: string; description: string; owner_name: string | null; due_date: string; nonconformity_title: string | null; severity: string }>()
75
+ return rows.results
76
+ }
77
+
78
+ /** Geciken faaliyet sayısı; `owner` verilirse `overdueActions` ile AYNI süzgeçle (liste ve toplam aynı kümeyi anlatır). */
79
+ export async function overdueCount(env: BaseEnv, today: string, owner?: string): Promise<number> {
80
+ const q = (owner ?? '').trim().toLowerCase()
81
+ const row = await env.DB.prepare(`SELECT COUNT(*) AS n FROM "actions" a WHERE ${OVERDUE}` + (q ? " AND instr(lower(coalesce(a.owner_name, '')), ?) > 0" : ''))
82
+ .bind(...(q ? [today, q] : [today]))
83
+ .first<{ n: number }>()
84
+ return row?.n ?? 0
85
+ }
86
+
87
+ /** Sayılar sunucuda hesaplanır: durum, açık kayıtların önem ve kaynak dağılımı. */
88
+ export async function counts(env: BaseEnv) {
89
+ const [byStatus, bySeverity, bySource] = await Promise.all([
90
+ env.DB.prepare('SELECT status AS key, COUNT(*) AS n FROM "nonconformities" WHERE deleted_at IS NULL GROUP BY status').all<{ key: string; n: number }>(),
91
+ env.DB.prepare("SELECT severity AS key, COUNT(*) AS n FROM \"nonconformities\" WHERE deleted_at IS NULL AND status <> 'closed' GROUP BY severity").all<{ key: string; n: number }>(),
92
+ env.DB.prepare("SELECT source AS key, COUNT(*) AS n FROM \"nonconformities\" WHERE deleted_at IS NULL AND status <> 'closed' GROUP BY source").all<{ key: string; n: number }>(),
93
+ ])
94
+ const map = (rows: { key: string; n: number }[]) => Object.fromEntries(rows.map((r) => [r.key, r.n]))
95
+ return { byStatus: map(byStatus.results), openBySeverity: map(bySeverity.results), openBySource: map(bySource.results) }
96
+ }
97
+
98
+ export async function lastReminderRun(env: BaseEnv) {
99
+ return env.DB.prepare(
100
+ 'SELECT run_key, reminded_owners, reminded_actions, failed, deferred_owners AS deferred, consent_needed FROM "reminder_runs" WHERE deleted_at IS NULL ORDER BY run_key DESC LIMIT 1',
101
+ ).first<{ run_key: string; reminded_owners: number; reminded_actions: number; failed: number; deferred: number; consent_needed: number }>()
102
+ }