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,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
|
+
}
|