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,53 @@
1
+ import { describe, expect, it } from 'vitest'
2
+ import { VALUE_MAX, directReports, goalProgress, keyResultProgress, parseValue, weightedProgress } from './progress.ts'
3
+
4
+ describe('ilerleme kuralları', () => {
5
+ it('ölçülebilir sonuç: başlangıçtan hedefe oran, bir ondalık', () => {
6
+ expect(keyResultProgress(0, 200, 50)).toBe(25)
7
+ expect(keyResultProgress(10, 40, 20)).toBe(33.3)
8
+ // Azalan hedef: 40 → 10, şu an 25 → yarı yol.
9
+ expect(keyResultProgress(40, 10, 25)).toBe(50)
10
+ })
11
+
12
+ it('hedef aşılırsa %100, başlangıcın gerisine düşülürse %0 (sınırlanır)', () => {
13
+ expect(keyResultProgress(0, 100, 130)).toBe(100)
14
+ expect(keyResultProgress(0, 100, -20)).toBe(0)
15
+ expect(keyResultProgress(40, 10, 50)).toBe(0)
16
+ expect(keyResultProgress(40, 10, 0)).toBe(100)
17
+ })
18
+
19
+ it('başlangıç ile hedef aynıysa: hedefe eşitse %100, değilse %0', () => {
20
+ expect(keyResultProgress(0, 0, 0)).toBe(100)
21
+ expect(keyResultProgress(0, 0, 3)).toBe(0)
22
+ expect(keyResultProgress(5, 5, 4)).toBe(0)
23
+ })
24
+
25
+ it('hedef: ölçülebilir sonuçların ortalaması; sonucu olmayan hedef %0', () => {
26
+ expect(goalProgress([100, 50, 0])).toBe(50)
27
+ expect(goalProgress([])).toBe(0)
28
+ })
29
+
30
+ it('ekip/dönem: ağırlıklı ortalama', () => {
31
+ expect(weightedProgress([{ progress: 100, weight: 3 }, { progress: 0, weight: 1 }])).toBe(75)
32
+ expect(weightedProgress([])).toBe(0)
33
+ })
34
+
35
+ it('değer: sayı ya da "12,5"; en çok dört ondalık; tavan', () => {
36
+ expect(parseValue('12,5')).toEqual({ ok: true, value: 12.5 })
37
+ expect(parseValue(-3)).toEqual({ ok: true, value: -3 })
38
+ expect(parseValue(1.23456)).toMatchObject({ ok: false })
39
+ expect(parseValue(VALUE_MAX * 10)).toMatchObject({ ok: false })
40
+ expect(parseValue('on iki')).toMatchObject({ ok: false })
41
+ })
42
+
43
+ it('ekip: işlevsel ya da operasyonel yöneticisi bu kişi olanlar (yalnız doğrudan bağlılar)', () => {
44
+ const base = { userCode: null, companyId: '', workplaceId: '', departmentId: '', employeeTypeId: '', titleId: '', positionId: '', isEmployee: true, isActive: true }
45
+ const users = [
46
+ { ...base, id: 'A'.repeat(24), fullName: 'Yönetici', functionalManager: '', operationalManager: '' },
47
+ { ...base, id: 'b'.repeat(24), fullName: 'Çalışan 1', functionalManager: 'a'.repeat(24), operationalManager: '' },
48
+ { ...base, id: 'c'.repeat(24), fullName: 'Çalışan 2', functionalManager: '', operationalManager: 'a'.repeat(24) },
49
+ { ...base, id: 'd'.repeat(24), fullName: 'Alt kademe', functionalManager: 'b'.repeat(24), operationalManager: '' },
50
+ ]
51
+ expect([...directReports(users, 'a'.repeat(24))].sort()).toEqual(['b'.repeat(24), 'c'.repeat(24)])
52
+ })
53
+ })
@@ -0,0 +1,81 @@
1
+ // İlerleme kuralları — SAF fonksiyonlar (veritabanı ve ağ yok). Sunucu her ilerlemeyi BURADA hesaplar; istemcinin
2
+ // gönderdiği ilerleme değeri hiçbir uçta kabul edilmez. Kuralı değiştiren bu dosyayı ve `progress.test.ts`'i birlikte
3
+ // değiştirir. ⚠️ Hedefin ilerlemesi veritabanında da aynı kuralla (ölçülebilir sonuçların ortalaması, bir ondalık)
4
+ // yeniden hesaplanır (`app.ts` → `GOAL_PROGRESS_SQL`): biri değişirse öteki de değişir.
5
+ import type { OrgUserWithManagers } from '@devoplus/one-sdk'
6
+
7
+ /** Bir değerin mutlak tavanı (yanlış yazılan fazladan sıfırları yakalar). */
8
+ export const VALUE_MAX = 1_000_000_000_000
9
+ /** Değer en çok dört ondalık taşır. */
10
+ export const VALUE_DECIMALS = 4
11
+
12
+ export function round1(n: number): number {
13
+ return Math.round(n * 10) / 10
14
+ }
15
+
16
+ function clampPercent(n: number): number {
17
+ return Math.min(100, Math.max(0, n))
18
+ }
19
+
20
+ /**
21
+ * Ölçülebilir sonucun ilerlemesi (yüzde, 0-100, bir ondalık): `(güncel - başlangıç) / (hedef - başlangıç)`.
22
+ * - Hedef başlangıçtan KÜÇÜKSE (ör. "şikâyet sayısını 40'tan 10'a indir") aynı formül azalmayı ilerleme sayar.
23
+ * - Hedef aşılırsa %100, başlangıcın gerisine düşülürse %0 (sınırlanır; %130 ya da -%20 yazılmaz).
24
+ * - Başlangıç ile hedef AYNIYSA (ör. "hata sayısını 0'da tut") oran tanımsızdır: güncel değer hedefe eşitse %100,
25
+ * değilse %0.
26
+ */
27
+ export function keyResultProgress(start: number, target: number, current: number): number {
28
+ if (!Number.isFinite(start) || !Number.isFinite(target) || !Number.isFinite(current)) return 0
29
+ if (start === target) return current === target ? 100 : 0
30
+ return round1(clampPercent(((current - start) / (target - start)) * 100))
31
+ }
32
+
33
+ /** Hedefin ilerlemesi: ölçülebilir sonuçlarının ilerlemelerinin ortalaması; ölçülebilir sonucu olmayan hedef %0. */
34
+ export function goalProgress(keyResults: readonly number[]): number {
35
+ if (keyResults.length === 0) return 0
36
+ return round1(keyResults.reduce((a, b) => a + b, 0) / keyResults.length)
37
+ }
38
+
39
+ /** Ekip ya da dönem ilerlemesi: hedeflerin AĞIRLIKLI ortalaması (ağırlık 1-100). Hedef yoksa %0. */
40
+ export function weightedProgress(goals: ReadonlyArray<{ progress: number; weight: number }>): number {
41
+ return weightedProgressOf(
42
+ goals.reduce((a, g) => a + (g.weight > 0 ? g.weight : 0), 0),
43
+ goals.reduce((a, g) => a + (g.weight > 0 ? g.weight * g.progress : 0), 0),
44
+ )
45
+ }
46
+
47
+ /**
48
+ * Aynı kural, toplamlardan: `weightSum` = pozitif ağırlıkların toplamı, `weightedSum` = Σ ağırlık × ilerleme. Dönem panosu
49
+ * toplamları SQL'de alır (`queries.ts` → `periodSummary`) ve oranı buradan kurar; iki yol aynı sonucu verir.
50
+ */
51
+ export function weightedProgressOf(weightSum: number, weightedSum: number): number {
52
+ if (!(weightSum > 0)) return 0
53
+ return round1(weightedSum / weightSum)
54
+ }
55
+
56
+ export type ValueCheck = { ok: true; value: number } | { ok: false; message: string }
57
+
58
+ /** Ölçülebilir sonuç değeri: sayı (ya da "12,5" gibi metin), sonlu, tavanın içinde, en çok dört ondalık. */
59
+ export function parseValue(raw: unknown): ValueCheck {
60
+ const value =
61
+ typeof raw === 'number' ? raw : typeof raw === 'string' && /^\s*-?\d+(?:[.,]\d+)?\s*$/.test(raw) ? Number(raw.trim().replace(',', '.')) : NaN
62
+ if (!Number.isFinite(value)) return { ok: false, message: 'Bir sayı girin.' }
63
+ if (Math.abs(value) > VALUE_MAX) return { ok: false, message: 'Değer çok büyük.' }
64
+ const scaled = value * 10 ** VALUE_DECIMALS
65
+ if (Math.abs(Math.round(scaled) - scaled) > 1e-6) return { ok: false, message: `Değer en çok ${VALUE_DECIMALS} ondalık olabilir.` }
66
+ return { ok: true, value }
67
+ }
68
+
69
+ /**
70
+ * Bir yöneticinin ekibi: organizasyon şemasında işlevsel YA DA operasyonel yöneticisi bu kişi olan çalışanlar
71
+ * (doğrudan bağlılar; alt kademeler dahil DEĞİL). Kimlikler org kimliğidir (oturum kimliği değil), küçük harfle.
72
+ */
73
+ export function directReports(users: readonly OrgUserWithManagers[], managerOrgId: string): Set<string> {
74
+ const me = managerOrgId.toLowerCase()
75
+ const out = new Set<string>()
76
+ for (const u of users) {
77
+ if (u.id.toLowerCase() === me) continue
78
+ if ((u.functionalManager ?? '').toLowerCase() === me || (u.operationalManager ?? '').toLowerCase() === me) out.add(u.id.toLowerCase())
79
+ }
80
+ return out
81
+ }
@@ -0,0 +1,83 @@
1
+ // Veritabanı okumaları — ekran, dönem panosu ve Devoplus yapay zekâsı aracı aynı sorguları kullanır. Görünürlük
2
+ // kararı ÇAĞIRANDADIR: `owners` verilirse yalnız o kişilerin hedefleri okunur (`null` = hepsi; yalnız hedef yöneticisi).
3
+ // Kişi listesi tek bağlı parametre olarak JSON dizisiyle verilir (`json_each`): kişi sayısı parametre sınırına takılmaz.
4
+ import type { BaseEnv } from './lib/server.ts'
5
+ import { round1, weightedProgressOf } from './progress.ts'
6
+
7
+ export interface PeriodRow {
8
+ id: string
9
+ code: string
10
+ name: string
11
+ status: string
12
+ start_date: string
13
+ end_date: string
14
+ }
15
+
16
+ /** Dönem: kimlikle, kodla ya da (ikisi de yoksa) başlangıcı en yeni olan. */
17
+ export async function findPeriod(env: BaseEnv, by: { id?: string | null; code?: string | null }): Promise<PeriodRow | null> {
18
+ const cols = 'id, code, name, status, start_date, end_date'
19
+ if (by.id) return env.DB.prepare(`SELECT ${cols} FROM "periods" WHERE id = ? AND deleted_at IS NULL`).bind(by.id).first<PeriodRow>()
20
+ if (by.code) return env.DB.prepare(`SELECT ${cols} FROM "periods" WHERE code = ? AND deleted_at IS NULL`).bind(by.code).first<PeriodRow>()
21
+ return env.DB.prepare(`SELECT ${cols} FROM "periods" WHERE deleted_at IS NULL ORDER BY start_date DESC, created_at DESC LIMIT 1`).first<PeriodRow>()
22
+ }
23
+
24
+ /** Görünürlük koşulu: `owners` null ise koşul yok, boş dizi ise hiçbir satır. */
25
+ export function ownerClause(owners: readonly string[] | null): { sql: string; params: string[] } {
26
+ if (owners === null) return { sql: '', params: [] }
27
+ return { sql: ' AND g.owner_org_id IN (SELECT value FROM json_each(?))', params: [JSON.stringify(owners)] }
28
+ }
29
+
30
+ export interface TeamSummary {
31
+ team_id: string | null
32
+ team_name: string | null
33
+ goals: number
34
+ progress: number
35
+ }
36
+
37
+ export interface PeriodSummary {
38
+ overall: number
39
+ goalCount: number
40
+ teams: TeamSummary[]
41
+ }
42
+
43
+ /**
44
+ * Dönem özeti: ekip başına hedef sayısı ve ağırlıklı ilerleme + dönemin genel ilerlemesi. Sayım ve ağırlıklı toplam
45
+ * SQL'de yapılır, yani satır sınırı yoktur: dönemde kaç hedef olursa olsun özet hepsini kapsar (eskiden ilk 5.000 hedef
46
+ * okunuyor ve kalanı sessizce atılıyordu). Ağırlıklı ortalamanın kuralı `progress.ts` → `weightedProgressOf`'tadır.
47
+ */
48
+ export async function periodSummary(env: BaseEnv, periodId: string, owners: readonly string[] | null): Promise<PeriodSummary> {
49
+ if (owners !== null && owners.length === 0) return { overall: 0, goalCount: 0, teams: [] }
50
+ const where = ownerClause(owners)
51
+ const rows = await env.DB.prepare(
52
+ 'SELECT g.team_id, MIN(g.team_name) AS team_name, COUNT(*) AS goals, ' +
53
+ 'SUM(CASE WHEN g.weight > 0 THEN g.weight ELSE 0 END) AS weight_sum, ' +
54
+ 'SUM(CASE WHEN g.weight > 0 THEN g.weight * g.progress ELSE 0 END) AS weighted_sum ' +
55
+ `FROM "goals" g WHERE g.deleted_at IS NULL AND g.period_id = ?${where.sql} GROUP BY g.team_id`,
56
+ )
57
+ .bind(periodId, ...where.params)
58
+ .all<{ team_id: string | null; team_name: string | null; goals: number; weight_sum: number | null; weighted_sum: number | null }>()
59
+ const teams = rows.results
60
+ .map((r) => ({
61
+ team_id: r.team_id,
62
+ team_name: r.team_name,
63
+ goals: Number(r.goals),
64
+ progress: weightedProgressOf(Number(r.weight_sum ?? 0), Number(r.weighted_sum ?? 0)),
65
+ }))
66
+ .sort((a, b) => (a.team_name ?? '').localeCompare(b.team_name ?? '', 'tr'))
67
+ const weightSum = rows.results.reduce((a, r) => a + Number(r.weight_sum ?? 0), 0)
68
+ const weightedSum = rows.results.reduce((a, r) => a + Number(r.weighted_sum ?? 0), 0)
69
+ return { overall: weightedProgressOf(weightSum, weightedSum), goalCount: teams.reduce((a, t) => a + t.goals, 0), teams }
70
+ }
71
+
72
+ /** En geride kalan hedefler (ilerlemesi en düşük önce); sıralama ve sınır SQL'de, dönemin bütün hedefleri arasından. */
73
+ export async function lowestGoals(env: BaseEnv, periodId: string, owners: readonly string[] | null, limit: number) {
74
+ if (owners !== null && owners.length === 0) return []
75
+ const where = ownerClause(owners)
76
+ const rows = await env.DB.prepare(
77
+ 'SELECT g.title, g.owner_name, g.team_name, g.progress FROM "goals" g ' +
78
+ `WHERE g.deleted_at IS NULL AND g.period_id = ?${where.sql} ORDER BY g.progress ASC, g.title ASC LIMIT ?`,
79
+ )
80
+ .bind(periodId, ...where.params, limit)
81
+ .all<{ title: string; owner_name: string | null; team_name: string | null; progress: number }>()
82
+ return rows.results.map((g) => ({ title: g.title, owner: g.owner_name, team: g.team_name, progress: round1(g.progress) }))
83
+ }