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,71 @@
1
+ // Sayfa iskeleti (şablonların ORTAK parçası — create-devoplus-one-app üretir). Sayfa üç dosyayı uygulamanın KENDİ adresinden yükler:
2
+ // `/one-ui.css` (görsel dil), `/one-bridge.js` (Portal köprüsü `window.one`), `/one-components.js` (hazır bileşenler) ve
3
+ // kendi betiği `/shell.js`. ⛔ Satır içi `<script>` ve başka adresten betik YAZILMAZ: platform ikisini de çalıştırmaz.
4
+ // Menüyü (sekmeleri), kullanıcı/portal üst bilgisini, bildirim ve onay pencerelerini Portal çizer — bu sayfa çizmez.
5
+ // ⛔ Kök öğelerde 100vh / height:100% kullanmayın: içerik iframe boyuna bağlanırsa yükseklik hiç küçülmez.
6
+
7
+ export type Lang = 'tr' | 'en'
8
+ export type Theme = 'light' | 'dark'
9
+ export type Strings = Record<Lang, Record<string, string>>
10
+
11
+ export function normalizeLang(raw: string | null | undefined): Lang {
12
+ return (raw ?? '').trim().toLowerCase().startsWith('en') ? 'en' : 'tr'
13
+ }
14
+
15
+ export function normalizeTheme(raw: string | null | undefined): Theme {
16
+ return (raw ?? '').trim().toLowerCase() === 'dark' ? 'dark' : 'light'
17
+ }
18
+
19
+ export function escapeHtml(value: string): string {
20
+ return value.replace(/&/g, '&amp;').replace(/</g, '&lt;').replace(/>/g, '&gt;').replace(/"/g, '&quot;')
21
+ }
22
+
23
+ /** Dile bağlı metin; anahtar sayfada `data-i18n` olarak kalır ve dil değişince `/shell.js` yeniden çevirir. */
24
+ export function translator(strings: Strings, lang: Lang): (key: string) => string {
25
+ return (key) => strings[lang][key] ?? strings.tr[key] ?? key
26
+ }
27
+
28
+ /** `<etiket data-i18n="anahtar">çeviri</etiket>` — metin kaçırılır (HTML yorumlanmaz). */
29
+ export function i18n(tx: (key: string) => string, key: string, tag = 'span', attributes = ''): string {
30
+ return `<${tag}${attributes ? ` ${attributes}` : ''} data-i18n="${escapeHtml(key)}">${escapeHtml(tx(key))}</${tag}>`
31
+ }
32
+
33
+ export interface PageOptions {
34
+ lang: Lang
35
+ theme: Theme
36
+ /** Sekme başlığı (kaçırılır). */
37
+ title: string
38
+ /** Sayfanın gövdesi; görünümler `<section data-view="/yol">` bloklarıdır. */
39
+ body: string
40
+ /** Sayfaya özel stil (satır içi stil serbesttir; renk yalnız `--one-*` değişkenlerinden). */
41
+ style?: string
42
+ }
43
+
44
+ /** İlk çizim `lang` / `theme` sorgusuyla yapılır; sonraki değişimler `/shell.js`'te `one.onContext` ile uygulanır. */
45
+ export function renderPage(options: PageOptions): string {
46
+ return `<!doctype html>
47
+ <html lang="${options.lang}" data-theme="${options.theme}"><head><meta charset="utf-8"><title>${escapeHtml(options.title)}</title>
48
+ <meta name="viewport" content="width=device-width, initial-scale=1">
49
+ <link rel="stylesheet" href="/one-ui.css">
50
+ <style>
51
+ .app-toolbar{display:flex;flex-wrap:wrap;gap:8px;align-items:center;margin:0 0 12px}
52
+ .app-grid{display:grid;grid-template-columns:repeat(auto-fit,minmax(min(100%,16rem),1fr));gap:12px}
53
+ .app-detail dl{display:grid;grid-template-columns:auto minmax(0,1fr);gap:4px 12px;margin:0}
54
+ .app-detail dt{font-weight:600}
55
+ .app-list{list-style:none;padding:0;margin:8px 0}
56
+ .app-list li{padding:8px 0;border-bottom:1px solid var(--one-border)}
57
+ ${options.style ?? ''}
58
+ </style></head><body>
59
+ <p id="status" class="one-muted" role="status" aria-live="polite"></p>
60
+ <div id="readOnly" class="one-message one-message-info" data-i18n="readOnly" hidden></div>
61
+ ${options.body}
62
+ <script src="/one-bridge.js"></script>
63
+ <script src="/one-components.js"></script>
64
+ <script src="/shell.js"></script>
65
+ </body></html>`
66
+ }
67
+
68
+ /** Görünüm bloğu: yol manifest `navigation`'daki yollardan biridir; ilk çizimde yalnız geçerli yol görünür. */
69
+ export function view(path: string, current: string, content: string): string {
70
+ return `<section data-view="${escapeHtml(path)}"${path === current ? '' : ' hidden'}>\n${content}\n</section>`
71
+ }
@@ -0,0 +1,246 @@
1
+ // Koşullu yazım (şablonların ORTAK parçası — create-devoplus-one-app üretir).
2
+ //
3
+ // Hazır kayıt ucu (`createCrudHandler`) yazımı yalnız kayıt kimliğine bağlar: "dönem hâlâ açıksa", "kayıt kapanmadıysa",
4
+ // "yazan kişi hâlâ sorumluysa" gibi bir ön koşulu yazımla AYNI ifadeye koyamaz. Ön koşulu önce okuyup sonra yazmak iki
5
+ // ayrı adımdır; araya giren bir istek (kapatma, silme, yeniden atama) koşulu geçersiz kılar ve yazım yine olur.
6
+ //
7
+ // ⛔ KURAL: bir uç yazmadan önce bir şey DENETLİYORSA (durum, sahiplik ya da sorumluluk, üst kaydın silinmemiş olması,
8
+ // okunup yazıma taşınan bir değer, alanlar arası bir kural) o denetim yazım ifadesinin KOŞULUNDA da durur. Bu dosyadaki
9
+ // `insertWhen` / `updateWhen` / `removeWhen` koşulu (`when`) ZORUNLU parametre olarak ister; etkilenen satır yoksa koşul
10
+ // tutmamıştır ve uç nedenini yeniden okuyup söyler (404 / 409). Değerler veri modelinin kuralıyla (kayıt ucunun kuralının
11
+ // aynısı) `recordWrite` ile doğrulanır.
12
+ import {
13
+ DataValidationError,
14
+ OneError,
15
+ foldSearchText,
16
+ isActingSession,
17
+ type FieldIssue,
18
+ type OneContext,
19
+ type OneDataModel,
20
+ type OneDatabase,
21
+ type OneStatement,
22
+ } from '@devoplus/one-sdk'
23
+
24
+ export type SqlValue = string | number | null
25
+
26
+ /** Yazılacak kolonlar ve değerleri (aynı sırada); metin alanlarının arama kolonu (`<alan>_fold`) dahil. */
27
+ export interface RecordWrite {
28
+ model: OneDataModel
29
+ entity: string
30
+ columns: string[]
31
+ values: SqlValue[]
32
+ }
33
+
34
+ /** Bağlı parametreleriyle bir SQL parçası (koşul ya da ek atama). */
35
+ export interface Sql {
36
+ sql: string
37
+ params: SqlValue[]
38
+ }
39
+
40
+ /** `sql('a = ? AND b = ?', 1, 2)` — parçayı parametreleriyle birlikte taşır (metne değer gömülmez). */
41
+ export function sql(text: string, ...params: SqlValue[]): Sql {
42
+ return { sql: text, params }
43
+ }
44
+
45
+ /** Parçaları `AND` ile birleştirir (boş metinli parça atlanır). */
46
+ export function and(...parts: Sql[]): Sql {
47
+ const used = parts.filter((p) => p.sql.trim() !== '')
48
+ return { sql: used.map((p) => `(${p.sql})`).join(' AND ') || '1 = 1', params: used.flatMap((p) => p.params) }
49
+ }
50
+
51
+ // Kayıt ucuyla aynı karakter kuralları: tek satırlık metinde yalnız sekme, çok satırlıda satır sonları da serbesttir.
52
+ // eslint-disable-next-line no-control-regex
53
+ const SINGLE_LINE_BAD = /[\u0000-\u0008\u000a-\u001f\u007f]/
54
+ // eslint-disable-next-line no-control-regex
55
+ const MULTI_LINE_BAD = /[\u0000-\u0008\u000b\u000c\u000e-\u001f\u007f]/
56
+ const DATE_RE = /^(\d{4})-(\d{2})-(\d{2})$/
57
+ const DATETIME_RE = /^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}(?::\d{2}(?:\.\d{1,3})?)?(?:Z|[+-]\d{2}:\d{2})$/
58
+ const TEXT_MAX = { text: 200, longText: 10_000 }
59
+ const FILE_KEY_MAX_LENGTH = 512
60
+ const FILE_KEY_MAX_BYTES = 1024
61
+
62
+ type Field = OneDataModel['entities'][string]['fields'][string]
63
+ type Coerced = { ok: true; value: SqlValue } | { ok: false; message: string }
64
+
65
+ function calendarDate(text: string): boolean {
66
+ const m = DATE_RE.exec(text)
67
+ if (!m) return false
68
+ const [y, mo, d] = [Number(m[1]), Number(m[2]), Number(m[3])]
69
+ const date = new Date(Date.UTC(y, mo - 1, d))
70
+ return y >= 1 && date.getUTCFullYear() === y && date.getUTCMonth() === mo - 1 && date.getUTCDate() === d
71
+ }
72
+
73
+ function coerce(f: Field, raw: unknown): Coerced {
74
+ const fail = (m: string): Coerced => ({ ok: false, message: `"${f.title.tr}" ${m}.` })
75
+ if (raw === null || raw === undefined) return { ok: true, value: null }
76
+ switch (f.type) {
77
+ case 'text':
78
+ case 'longText': {
79
+ if (typeof raw !== 'string') return fail('metin olmalı')
80
+ if (raw.trim() === '') return { ok: true, value: null }
81
+ if ((f.type === 'text' ? SINGLE_LINE_BAD : MULTI_LINE_BAD).test(raw))
82
+ return fail(f.type === 'text' ? 'tek satır olmalı ve denetim karakteri içermemeli' : 'denetim karakteri içermemeli')
83
+ const max = f.maxLength ?? TEXT_MAX[f.type]
84
+ if ([...raw].length > max) return fail(`en çok ${max} karakter olabilir`)
85
+ return { ok: true, value: raw }
86
+ }
87
+ case 'number': {
88
+ let n: number
89
+ if (typeof raw === 'number') n = raw
90
+ else if (typeof raw === 'string' && /^\s*-?\d+(?:[.,]\d+)?\s*$/.test(raw)) n = Number(raw.trim().replace(',', '.'))
91
+ else if (typeof raw === 'string' && raw.trim() === '') return { ok: true, value: null }
92
+ else return fail('sayı olmalı')
93
+ if (!Number.isFinite(n)) return fail('sonlu bir sayı olmalı')
94
+ if (f.min !== undefined && n < f.min) return fail(`en az ${f.min} olabilir`)
95
+ if (f.max !== undefined && n > f.max) return fail(`en çok ${f.max} olabilir`)
96
+ return { ok: true, value: n }
97
+ }
98
+ case 'boolean':
99
+ if (typeof raw !== 'boolean') return fail('evet ya da hayır (true/false) olmalı')
100
+ return { ok: true, value: raw ? 1 : 0 }
101
+ case 'date':
102
+ if (typeof raw !== 'string') return fail('tarih olmalı')
103
+ if (raw.trim() === '') return { ok: true, value: null }
104
+ return calendarDate(raw) ? { ok: true, value: raw } : fail('YYYY-AA-GG biçiminde geçerli bir tarih olmalı')
105
+ case 'choice': {
106
+ if (typeof raw !== 'string') return fail('seçeneklerden biri olmalı')
107
+ if (raw === '') return { ok: true, value: null }
108
+ const choices = f.choices ?? []
109
+ return choices.some((c) => c.value === raw) ? { ok: true, value: raw } : fail(`şunlardan biri olmalı: ${choices.map((c) => c.value).join(', ')}`)
110
+ }
111
+ case 'datetime': {
112
+ if (typeof raw !== 'string') return fail('tarih-saat olmalı')
113
+ if (raw.trim() === '') return { ok: true, value: null }
114
+ const t = DATETIME_RE.test(raw) && calendarDate(raw.slice(0, 10)) ? Date.parse(raw) : NaN
115
+ return Number.isFinite(t) ? { ok: true, value: new Date(t).toISOString() } : fail('saat dilimi belirtilmiş ISO biçiminde geçerli bir tarih-saat olmalı (ör. 2026-01-31T09:00:00Z)')
116
+ }
117
+ case 'file': {
118
+ // Anahtarın KULLANICININ ön ekinde olduğunu çağıran `ownFileKey` ile denetler; burada kayıt ucunun biçim kuralı.
119
+ if (typeof raw !== 'string') return fail('dosya anahtarı olmalı')
120
+ if (raw === '') return { ok: true, value: null }
121
+ if (raw.length > FILE_KEY_MAX_LENGTH || new TextEncoder().encode(raw).byteLength > FILE_KEY_MAX_BYTES) return fail(`dosya anahtarı 1-${FILE_KEY_MAX_LENGTH} karakter olmalı`)
122
+ // eslint-disable-next-line no-control-regex
123
+ if (raw.startsWith('/') || raw.split('/').some((p) => p === '' || p === '.' || p === '..') || /[\u0000-\u001f\u007f]/.test(raw)) return fail('dosya anahtarı geçersiz')
124
+ return { ok: true, value: raw }
125
+ }
126
+ default:
127
+ // Para ve kişi alanları bu yardımcıyla yazılmaz (kayıt ucunun kendi dönüşümü vardır).
128
+ throw new Error(`"${f.type}" türündeki alan koşullu yazımla yazılamaz`)
129
+ }
130
+ }
131
+
132
+ /**
133
+ * Gövdeyi veri modelinin kuralıyla doğrular ve yazılacak kolonları döndürür.
134
+ * - `create`: modeldeki HER alan yazılır; gönderilmeyen alana varsayılanı konur, zorunlu alan boşsa hata.
135
+ * - `update`: yalnız gönderilen alanlar; zorunlu alan boşaltılamaz; hiç alan yoksa hata.
136
+ * Modelde olmayan ad 400'dür. Benzersiz (`unique`) alanın çakışması yazımda `applied` ile alan hatasına (409) çevrilir.
137
+ */
138
+ export function recordWrite(model: OneDataModel, entity: string, body: Record<string, unknown>, mode: 'create' | 'update'): RecordWrite {
139
+ const fields = model.entities[entity]?.fields
140
+ if (!fields) throw new Error(`veri modelinde "${entity}" yok`)
141
+ const issues: FieldIssue[] = []
142
+ for (const key of Object.keys(body)) if (!(key in fields)) issues.push({ field: key, message: `"${key}" bu kayıtta bulunmayan bir alan.` })
143
+ const out: RecordWrite = { model, entity, columns: [], values: [] }
144
+ for (const [name, f] of Object.entries(fields)) {
145
+ const present = Object.prototype.hasOwnProperty.call(body, name)
146
+ if (!present && mode === 'update') continue
147
+ let value: SqlValue = null
148
+ if (present) {
149
+ const c = coerce(f, body[name])
150
+ if (!c.ok) {
151
+ issues.push({ field: name, message: c.message })
152
+ continue
153
+ }
154
+ value = c.value
155
+ }
156
+ if (value === null && mode === 'create' && f.default !== undefined) {
157
+ const d = coerce(f, f.default)
158
+ if (d.ok) value = d.value
159
+ }
160
+ if (value === null && f.required) {
161
+ issues.push({ field: name, message: `"${f.title.tr}" zorunlu.` })
162
+ continue
163
+ }
164
+ out.columns.push(name)
165
+ out.values.push(value)
166
+ if (f.type === 'text' || f.type === 'longText') {
167
+ out.columns.push(`${name}_fold`)
168
+ out.values.push(typeof value === 'string' ? foldSearchText(value) : null)
169
+ }
170
+ }
171
+ if (mode === 'update' && issues.length === 0 && out.columns.length === 0) issues.push({ field: '', message: 'Güncellenecek alan gönderilmedi.' })
172
+ if (issues.length > 0) throw new DataValidationError(400, 'APP-E-400', 'Kayıt doğrulanamadı.', issues)
173
+ return out
174
+ }
175
+
176
+ /**
177
+ * Kayıt ucunun Vekalet / Yerine Görüntüle koruması: platform bu oturumlarda yazmayı zaten durdurur; koşullu yazım kayıt
178
+ * ucunu atladığı için aynı savunma burada da durur.
179
+ */
180
+ export function refuseActingSession(ctx: OneContext): void {
181
+ if (isActingSession(ctx)) throw new OneError(403, 'APP-E-403', 'Vekalet ya da Yerine Görüntüle oturumunda bu uygulamada kayıt değiştirilemez; yalnız görüntüleyebilirsiniz.')
182
+ }
183
+
184
+ const q = (name: string): string => `"${name}"`
185
+
186
+ /**
187
+ * Koşullu ekleme: `INSERT INTO <varlık> (kimlik, damgalar, kolonlar) SELECT … WHERE <when>`. `when` eklemenin hangi
188
+ * koşulda geçerli olduğudur (ör. üst kayıt hâlâ açık ve silinmemiş); etkilenen satır yoksa koşul tutmamıştır.
189
+ */
190
+ export function insertWhen(db: OneDatabase, write: RecordWrite, o: { id: string; actor: string; now: string; when: Sql }): OneStatement {
191
+ const columns = ['id', 'created_at', 'created_by', 'updated_at', 'updated_by', ...write.columns]
192
+ return db
193
+ .prepare(`INSERT INTO ${q(write.entity)} (${columns.map(q).join(', ')}) SELECT ${columns.map(() => '?').join(', ')} WHERE ${o.when.sql}`)
194
+ .bind(o.id, o.now, o.actor, o.now, o.actor, ...write.values, ...o.when.params)
195
+ }
196
+
197
+ /**
198
+ * Koşullu güncelleme: yalnız `when` tutuyorsa ve satır silinmemişse yazar. `also` yazılacak ek atamalardır (ör. satırın
199
+ * önceki değerine bağlı bir sütun). `alias` verilirse koşul ve atamalar satırı bu adla anar.
200
+ */
201
+ export function updateWhen(db: OneDatabase, write: RecordWrite, o: { id: string; actor: string; now: string; when: Sql; also?: Sql; alias?: string }): OneStatement {
202
+ const row = o.alias ? `${q(write.entity)} AS ${o.alias}` : q(write.entity)
203
+ const col = (name: string) => (o.alias ? `${o.alias}.${q(name)}` : q(name))
204
+ const sets = [...write.columns.map((c) => `${q(c)} = ?`), ...(o.also ? [o.also.sql] : []), '"updated_at" = ?', '"updated_by" = ?']
205
+ return db
206
+ .prepare(`UPDATE ${row} SET ${sets.join(', ')} WHERE ${col('id')} = ? AND ${col('deleted_at')} IS NULL AND (${o.when.sql})`)
207
+ .bind(...write.values, ...(o.also?.params ?? []), o.now, o.actor, o.id, ...o.when.params)
208
+ }
209
+
210
+ /** Koşullu silme (silme kaydı: `deleted_at`). Satır yoksa ya da `when` tutmuyorsa hiçbir şey yazılmaz. */
211
+ export function removeWhen(db: OneDatabase, entity: string, o: { id: string; actor: string; now: string; when: Sql; alias?: string }): OneStatement {
212
+ const row = o.alias ? `${q(entity)} AS ${o.alias}` : q(entity)
213
+ const col = (name: string) => (o.alias ? `${o.alias}.${q(name)}` : q(name))
214
+ return db
215
+ .prepare(`UPDATE ${row} SET "deleted_at" = ?, "updated_at" = ?, "updated_by" = ? WHERE ${col('id')} = ? AND ${col('deleted_at')} IS NULL AND (${o.when.sql})`)
216
+ .bind(o.now, o.now, o.actor, o.id, ...o.when.params)
217
+ }
218
+
219
+ /** Yazımın sonucu: tam bir satır değiştiyse `true`. */
220
+ export function changedOne(result: { meta: { changes?: number } } | undefined): boolean {
221
+ return Number(result?.meta.changes ?? 0) === 1
222
+ }
223
+
224
+ /**
225
+ * İfadeyi (ya da toplu yazımı) çalıştırır; benzersizlik ihlalini kayıt ucunun ilettiği alan hatasına (409) çevirir.
226
+ * Dönen değer `run` işlevinin sonucudur.
227
+ */
228
+ export async function withUniqueCheck<T>(model: OneDataModel, entity: string, run: () => Promise<T>): Promise<T> {
229
+ try {
230
+ return await run()
231
+ } catch (error) {
232
+ const message = error instanceof Error ? error.message : String(error)
233
+ const m = /UNIQUE constraint failed:\s*"?([a-z0-9_]+)"?\."?([a-z0-9_]+)"?/i.exec(message)
234
+ if (!m) throw error
235
+ const field = m[1] === entity ? model.entities[entity]?.fields[m[2]!] : undefined
236
+ const name = field ? m[2]! : ''
237
+ throw new DataValidationError(409, 'APP-E-409', 'Bu değer başka bir kayıtta kullanılıyor.', [
238
+ { field: name, message: field ? `"${field.title.tr}" değeri başka bir kayıtta kullanılıyor.` : 'Bu değer başka bir kayıtta kullanılıyor.' },
239
+ ])
240
+ }
241
+ }
242
+
243
+ /** Tek ifadeyi çalıştırır: tam bir satır değiştiyse `true`; benzersizlik ihlali 409. */
244
+ export async function applied(write: Pick<RecordWrite, 'model' | 'entity'>, statement: OneStatement): Promise<boolean> {
245
+ return changedOne(await withUniqueCheck(write.model, write.entity, () => statement.run()))
246
+ }
@@ -0,0 +1,132 @@
1
+ // Uygulamanın istek yönlendiricisi (şablonların ORTAK parçası — create-devoplus-one-app üretir). Sıra bilinçlidir:
2
+ // 1. Devoplus yapay zekâsı: imzalı araç çağrısı `POST /__one/agent/<ad>` + katalog/rehber `/.well-known/one-agent.*`
3
+ // 2. Zamanlanmış görev ve kuyruk: `POST /__one/jobs/…` (yalnız platform çağırır; internetten kapalı)
4
+ // 3. Köprü, görsel dil, hazır bileşenler (`/one-bridge.js` · `/one-ui.css` · `/one-components.js`) ve `/shell.js`
5
+ // 4. Sayfa: manifest `navigation` yollarının hepsi aynı sayfayı döndürür (sekmeleri Portal çizer)
6
+ // 5. Kişi araması `/api/one/people` (manifest `orgschema.read` beyan ediyorsa)
7
+ // 6. `GET /api/session` — oturumdaki kişinin bu uygulamadaki izinleri (arayüz düğmeleri için; asıl kapı her uçta sunucuda)
8
+ // 7. Uygulamanın kendi uçları (`routes`)
9
+ // Kimlik X-One-* başlıklarından OKUNUR (platform yazar); bu dosya o başlıkları asla üretmez. Beklenmeyen hata
10
+ // ayrıntı sızdırmadan 500 döner ve olay günlüğüne bağlamıyla yazılır.
11
+ import {
12
+ BackendClient,
13
+ DataValidationError,
14
+ OneError,
15
+ PEOPLE_SEARCH_PATH,
16
+ agentManifestResponse,
17
+ clientAssetResponse,
18
+ handleAgentRequest,
19
+ handleJobRequest,
20
+ navigationFromManifest,
21
+ peopleSearchHandler,
22
+ problemResponse,
23
+ readContext,
24
+ requireUser,
25
+ scriptResponse,
26
+ type AgentManifest,
27
+ type AgentTools,
28
+ type OneApp,
29
+ type OneContext,
30
+ type OneDatabase,
31
+ type OneExecutionContext,
32
+ type OneJobsDefinition,
33
+ type OneNavigationItem,
34
+ } from '@devoplus/one-sdk'
35
+ import { normalizeLang, normalizeTheme, type Lang, type Theme } from './page.ts'
36
+
37
+ export interface BaseEnv {
38
+ /** Bu kurulumun veritabanı — yalnız bu portala aittir; tabloya portal kolonu eklenmez. */
39
+ DB: OneDatabase
40
+ /** Devoplus API tabanı; platform yazar. */
41
+ API_BASE: string
42
+ }
43
+
44
+ export interface RouteInput<Env extends BaseEnv> {
45
+ request: Request
46
+ env: Env
47
+ ctx: OneContext
48
+ url: URL
49
+ method: string
50
+ execution: OneExecutionContext
51
+ /** Kullanıcının uygulama belirteciyle Devoplus API istemcisi (backend kullanıcı yetkisi ∩ uygulama kapsamı uygular). */
52
+ api(): BackendClient
53
+ }
54
+
55
+ export interface AppManifest extends AgentManifest {
56
+ scopes?: readonly string[]
57
+ navigation?: unknown
58
+ actingSessions?: string
59
+ }
60
+
61
+ export interface AppDefinition<Env extends BaseEnv> {
62
+ manifest: AppManifest
63
+ /** AGENT_GUIDE.md (derlemede metin olarak gömülür). */
64
+ guide: string
65
+ tools: AgentTools<Env>
66
+ jobs?: OneJobsDefinition<Env>
67
+ /** Sayfa HTML'i (ilk çizim dili, teması ve görünür görünüm). */
68
+ page(lang: Lang, theme: Theme, path: string): string
69
+ /** `/shell.js` — sayfanın betiği. */
70
+ script(navigation: readonly OneNavigationItem[], options: { actingWrites: boolean }): string
71
+ routes(input: RouteInput<Env>): Promise<Response | null>
72
+ }
73
+
74
+ export function json(body: unknown, status = 200): Response {
75
+ return new Response(JSON.stringify(body), { status, headers: { 'content-type': 'application/json; charset=utf-8', 'cache-control': 'no-store' } })
76
+ }
77
+
78
+ /** RFC 9457 yanıtı; alan hataları (`errors: [{ field, message }]`) da taşınır — form onları alanın altında gösterir. */
79
+ export function appProblem(error: unknown, ctx?: OneContext): Response {
80
+ if (error instanceof DataValidationError) {
81
+ const body = { type: 'about:blank', title: error.code, status: error.status, detail: error.message, code: error.code, errors: error.errors }
82
+ return new Response(JSON.stringify(body), { status: error.status, headers: { 'content-type': 'application/problem+json; charset=utf-8' } })
83
+ }
84
+ return problemResponse(error, ctx)
85
+ }
86
+
87
+ export function createApp<Env extends BaseEnv>(definition: AppDefinition<Env>): OneApp<Env> {
88
+ const navigation = navigationFromManifest(definition.manifest)
89
+ const actingWrites = definition.manifest.actingSessions === 'write'
90
+ const script = definition.script(navigation, { actingWrites })
91
+ const pages = new Set(['/', ...navigation.map((item) => item.path)])
92
+ const peopleSearch = (definition.manifest.scopes ?? []).includes('orgschema.read')
93
+
94
+ return {
95
+ async fetch(request, env, execution) {
96
+ const url = new URL(request.url)
97
+ const method = request.method.toUpperCase()
98
+ let context: OneContext | undefined
99
+ try {
100
+ const agent = (await handleAgentRequest(request, env, definition.tools, definition.manifest)) ?? agentManifestResponse(request, definition.manifest, definition.guide)
101
+ if (agent) return agent
102
+ if (definition.jobs) {
103
+ const job = handleJobRequest(request, env, execution, definition.jobs)
104
+ if (job) return await job
105
+ }
106
+ const ctx = readContext(request.headers)
107
+ context = ctx
108
+ if (method === 'GET' || method === 'HEAD') {
109
+ const asset = clientAssetResponse(url.pathname)
110
+ if (asset) return asset
111
+ if (url.pathname === '/shell.js') return scriptResponse(script)
112
+ if (pages.has(url.pathname))
113
+ return new Response(definition.page(normalizeLang(url.searchParams.get('lang')), normalizeTheme(url.searchParams.get('theme')), url.pathname), {
114
+ headers: { 'content-type': 'text/html; charset=utf-8', 'cache-control': 'no-store' },
115
+ })
116
+ }
117
+ if (peopleSearch && url.pathname === PEOPLE_SEARCH_PATH) return await peopleSearchHandler(request, env, ctx)
118
+ if (url.pathname === '/api/session' && method === 'GET') {
119
+ // Yalnız arayüzün hangi düğmeleri göstereceği içindir: platform uygulamaya yalnız KENDİ ad alanındaki izinleri iletir.
120
+ const user = requireUser(ctx)
121
+ return json({ permissions: user.permissions, admin: user.admin, acting: ctx.actor != null })
122
+ }
123
+ const api = () => new BackendClient({ baseUrl: env.API_BASE, authorization: request.headers.get('authorization') })
124
+ const routed = await definition.routes({ request, env, ctx, url, method, execution, api })
125
+ if (routed) return routed
126
+ throw new OneError(404, 'APP-E-404', 'Bulunamadı.')
127
+ } catch (error) {
128
+ return appProblem(error, context)
129
+ }
130
+ },
131
+ }
132
+ }
@@ -0,0 +1,56 @@
1
+ import { describe, expect, it } from 'vitest'
2
+ import manifest from '../one.manifest.json' with { type: 'json' }
3
+ import { CORE_STRINGS } from './lib/client.ts'
4
+ import { STRINGS, renderShell, renderShellScript } from './shell.ts'
5
+
6
+ const navigation = (manifest.navigation ?? []) as { path: string; label: { tr: string; en?: string } }[]
7
+
8
+ describe('sayfa', () => {
9
+ it('iki dilin anahtar kümeleri aynıdır (yeni metin iki dile birden eklenir)', () => {
10
+ expect(Object.keys(STRINGS.en).sort()).toEqual(Object.keys(STRINGS.tr).sort())
11
+ })
12
+
13
+ it('sayfa metinleri ortak çekirdeğin anahtarlarıyla çakışmaz (aynı ad ortak uyarının yerine geçerdi)', () => {
14
+ for (const lang of ['tr', 'en'] as const) expect(Object.keys(CORE_STRINGS[lang]).filter((k) => k in STRINGS[lang]), lang).toEqual([])
15
+ })
16
+
17
+ it('satır içi betik yok; köprü, hazır bileşenler ve sayfa betiği kendi adresinden, bu sırayla yüklenir', () => {
18
+ const html = renderShell('tr', 'light', '/')
19
+ expect(html).not.toMatch(/<script(?![^>]*\bsrc=)[^>]*>/)
20
+ expect(html).not.toMatch(/\son[a-z]+="/)
21
+ const bridge = html.indexOf('<script src="/one-bridge.js"></script>')
22
+ const components = html.indexOf('<script src="/one-components.js"></script>')
23
+ const shell = html.indexOf('<script src="/shell.js"></script>')
24
+ expect(bridge).toBeGreaterThan(0)
25
+ expect(components).toBeGreaterThan(bridge)
26
+ expect(shell).toBeGreaterThan(components)
27
+ })
28
+
29
+ it('menüdeki her yolun bir görünümü var; ilk çizimde yalnız istenen görünüm açık', () => {
30
+ for (const item of navigation) {
31
+ const html = renderShell('tr', 'light', item.path)
32
+ expect(html, item.path).toContain(`<section data-view="${item.path}">`)
33
+ for (const other of navigation.filter((o) => o.path !== item.path)) expect(html).toContain(`<section data-view="${other.path}" hidden>`)
34
+ }
35
+ })
36
+
37
+ it('sayfadaki her metin anahtarı iki dilde de var', () => {
38
+ for (const lang of ['tr', 'en'] as const) {
39
+ const html = renderShell(lang, 'light', '/')
40
+ for (const m of html.matchAll(/data-i18n(?:-ph|-label)?="([^"]+)"/g)) expect(STRINGS[lang][m[1]!], `${lang}: ${m[1]}`).toBeTypeOf('string')
41
+ }
42
+ })
43
+
44
+ it('sayfa betiği Portal ile yalnız köprüden konuşur, menüyü bildirir ve kullanıcı metnini HTML olarak yazmaz', () => {
45
+ const script = renderShellScript(navigation)
46
+ expect(script).not.toMatch(/postMessage/)
47
+ expect(script).not.toMatch(/innerHTML|outerHTML|insertAdjacentHTML|document\.write/)
48
+ expect(script).toContain('one.start({ navigation: NAVIGATION })')
49
+ // Betik sözdizimi geçerli (tarayıcıda çalışmayan bir betik sayfayı sessizce boş bırakırdı).
50
+ expect(() => new Function(script)).not.toThrow()
51
+ })
52
+
53
+ it('ilk çizim dil ve temayı uygular', () => {
54
+ expect(renderShell('en', 'dark', '/')).toContain('<html lang="en" data-theme="dark">')
55
+ })
56
+ })
@@ -0,0 +1,23 @@
1
+ // Devoplus yapay zekâsının bu uygulamada çağırabileceği araçların İŞLEYİCİLERİ. Aracın adı, açıklaması, girdi şeması,
2
+ // salt-okunurluğu ve izni `one.manifest.json` → `agent.tools`'tadır; yapay zekânın okuduğu kullanım rehberi
3
+ // `AGENT_GUIDE.md`'dir. Yeni araç = üçü birlikte: manifest beyanı + buradaki işleyici + rehberdeki anlatım.
4
+ // Girdi SDK tarafından şemaya göre ÖNCEDEN doğrulanır; işleyici yalnız işi yapar ve JSON'a çevrilebilir bir değer döndürür.
5
+ import { defineAgentTools, requireUser, type OneDatabase } from '@devoplus/one-sdk'
6
+
7
+ export interface AgentEnv {
8
+ DB: OneDatabase
9
+ }
10
+
11
+ export const agentTools = defineAgentTools<AgentEnv>({
12
+ 'list-notes': {
13
+ // Salt okunur: yalnız kullanıcının KENDİ notları (`sub` ile) — ekrandaki "Notlarım" listesiyle aynı kural.
14
+ handler: async ({ input, ctx, env }) => {
15
+ const user = requireUser(ctx)
16
+ const limit = typeof input.limit === 'number' ? input.limit : 10
17
+ const rows = await env.DB.prepare('SELECT id, text, created_at AS createdAt FROM notes WHERE sub = ? ORDER BY id DESC LIMIT ?')
18
+ .bind(user.sub, limit)
19
+ .all<{ id: number; text: string; createdAt: string }>()
20
+ return { notes: rows.results }
21
+ },
22
+ },
23
+ })
@@ -0,0 +1,40 @@
1
+ ## Bu uygulamanın şablonu: bakım takvimi (`bakim-takvimi`)
2
+
3
+ Uygulama `create-devoplus-one-app --template bakim-takvimi` ile üretildi. Kullanıcının ihtiyacını bu şablona göre DEĞİŞTİRİN;
4
+ baştan yazmayın. Dosya haritası:
5
+
6
+ | Dosya | Ne |
7
+ |---|---|
8
+ | `one.data.json` | Veri modeli: `equipment` (ekipman) + `maintenance_records` (bakım kaydı) + `reminder_runs` (günlük hatırlatma kontrolü). Değişince `pnpm data:generate` |
9
+ | `src/app.ts` | Uçlar: ekipman tanımı, bakım kaydı (tek toplu yazım), gecikenler (süzgeci sunucu koyar), ek dosya, özet |
10
+ | `src/schedule.ts` | Saf kurallar: sıradaki bakım, aralık ve tarih doğrulama, durum, hatırlatma penceresi, gruplama + `schedule.test.ts` |
11
+ | `src/queries.ts` | Okumalar ve sıradaki bakımın SQL karşılığı (`NEXT_DUE_SQL`) |
12
+ | `src/jobs.ts` | Sabah işi `daily-maintenance-reminders` (sorumluya e-posta; kişi başına tek e-posta; damga ile tekrar yok) |
13
+ | `src/names.ts` | Uygulama adı ve bakım sorumlusu izni |
14
+ | `src/shell.ts` | Sayfa: takvim, gecikenler, bakım kayıtları, ekipmanlar |
15
+ | `src/agent.ts` · `AGENT_GUIDE.md` | Devoplus yapay zekâsının araçları ve rehberi |
16
+ | `src/lib/*` | Şablonların ortak parçaları — gerekmedikçe değiştirmeyin |
17
+
18
+ Değişmezler (bozmayın):
19
+
20
+ - **Sıradaki bakım tarihi SUNUCUDA hesaplanır:** son bakım + aralık. Ekipman uçları `next_due_date`,
21
+ `reminder_sent_for` ve `reminder_failed_on` alanlarını istemciden ALMAZ (400); `last_maintenance_date` yalnız ilk tanımda girilir, sonra yalnız
22
+ bakım kaydıyla değişir. Kuralı değiştirirseniz `schedule.ts` → `nextDueDate` ile `queries.ts` → `NEXT_DUE_SQL`'i
23
+ birlikte değiştirin.
24
+ - **Bakım kaydı ve ekipman tarihleri TEK toplu yazımdır** (`env.DB.batch([kayıt, ekipman])`): kayıt yalnız ekipman
25
+ kullanımdaysa eklenir, ekipmanın tarihleri yalnız o kayıt eklendiyse değişir. Eski tarihli bir kayıt son bakım tarihini
26
+ geri götürmez (`max`). İki ayrı yazıma ya da "önce oku, sonra yaz"a çevirmeyin.
27
+ - **Bakımı kim kaydeder:** bakım sorumlusu izni olan kişi ya da ekipmanın sorumlusu. Sorumluluk kişi kimliğiyle
28
+ (`responsible_org_id`, Organization Schema) sunucuda karşılaştırılır; kişi bilgisi okunamazsa kapı kapalı kalır. İzni
29
+ olmayan kişi için "hâlâ sorumlusu" koşulu bakım kaydının ekleme ifadesindedir (denetimden sonra sorumlu değişirse 403).
30
+ - **Gecikenler listesinin süzgecini SUNUCU koyar** (`next_due_date.to` = dün, `active = true`); istemcinin gönderdiği
31
+ tarih süzgeci yok sayılır.
32
+ - **Hatırlatma bir kez gider:** gönderimden önce koşullu damga (`reminder_sent_for` = hatırlatılan tarih); gönderilemezse
33
+ damga geri alınır. Zamanlanmış iş kullanıcısızdır: e-posta uygulamanın kimliğiyle gider ve manifest
34
+ `jobs.services: ["mail.execute"]` + kurulumda yönetici onayı ister. Organizasyon şeması işte kullanılamaz; alıcı
35
+ ekipman kaydındaki kişi kimliğidir. Hatırlatması ulaşmayan ekipman (`reminder_failed_on`) sıranın sonuna geçer: hiç
36
+ ulaşamayan bir sorumlu öteki sorumluları hatırlatmasız bırakmaz.
37
+ - **Bakım kaydı değiştirilmez ve silinmez** (405); düzeltme yeni bir kayıtla yapılır.
38
+ - **Bir koşumda en çok `MAILS_PER_RUN` (5) kişiye** e-posta gider (platformun istek başına dış çağrı sınırı 10); sayıyı
39
+ artırmayın.
40
+ - Mobil bildirim bugün hiçbir ortamda açık değildir; "telefonuma bildirim gelsin" isteğinde bunu söyleyin.
@@ -0,0 +1,45 @@
1
+ # {{nameTr}}
2
+
3
+ Cihaz ve ekipmanların periyodik bakımını izleyen bir bakım takvimi uygulaması. Devoplus Portal'ın içinde, Portal
4
+ menüsünden açılır. Bu uygulama Devoplus One'ın **bakım takvimi** şablonundan üretildi; ihtiyacınıza göre değiştirilecek
5
+ bir başlangıç noktasıdır.
6
+
7
+ ## Ne yapar
8
+
9
+ 1. **Bakım takvimi:** kullanımdaki bütün ekipmanlar sıradaki bakım tarihine göre sıralı listelenir; geciken ve yaklaşan
10
+ bakımlar işaretlenir. Ekranın üstünde kaç bakımın geciktiği ve kaçının yaklaştığı yazar. Liste Excel'e ya da CSV'ye
11
+ aktarılabilir.
12
+ 2. **Sıradaki bakım tarihi kendiliğinden hesaplanır:** son bakım tarihi + bakım aralığı (gün). Bu tarih elle girilmez;
13
+ aralık değişince ya da bakım kaydedilince yeniden hesaplanır.
14
+ 3. **Gecikenler:** bakım tarihi geçmiş ekipmanların ayrı listesi; Excel'e ya da CSV'ye aktarılabilir.
15
+ 4. **Bakım kayıtları:** ekipmanın sorumlusu (ya da bakım sorumlusu) yapılan bakımı tarih, açıklama ve isteğe bağlı
16
+ fotoğraf ya da belgeyle kaydeder. Kayıt girilince ekipmanın son ve sıradaki bakım tarihi birlikte güncellenir.
17
+ Kayıtlar sonradan değiştirilmez.
18
+ 5. **Hatırlatma:** her sabah 08:00'de, bakımı 7 gün içinde olan ya da geciken ekipmanların sorumlusuna tek bir e-posta
19
+ gider. Aynı bakım her gün yeniden hatırlatılmaz; bakım kaydedilince bir sonraki bakım yine hatırlatılır.
20
+ 6. **Ekipmanlar:** bakım sorumlusu ekipman tanımlar (kod, ad, tür, konum, bakım aralığı, sorumlu kişi).
21
+ 7. **Devoplus asistanı:** "Bu hafta hangi bakımlar var?" gibi sorular Devoplus'un yapay zekâ asistanına sorulduğunda
22
+ cevap bu uygulamadan gelir.
23
+
24
+ ## Kimler ne görür
25
+
26
+ | Kim | Ne yapabilir |
27
+ |---|---|
28
+ | Uygulamayı kullanma izni olan herkes ({{permissionCode}}) | Takvimi, gecikenleri ve bakım kayıtlarını görür, Excel'e aktarır |
29
+ | Bir ekipmanın sorumlusu | O ekipmanın bakımını kaydeder ve hatırlatma e-postasını alır |
30
+ | Bakım sorumlusu ({{permissionPrefix}}Manage) | Ekipman tanımlar ve her ekipmanın bakımını kaydeder; Ekipmanlar sekmesini görür |
31
+
32
+ İzinleri portal yöneticisi **Yetki Yönetimi** ekranından verir.
33
+
34
+ ## Neleri değiştirebilirsiniz
35
+
36
+ Yapay zekâ aracınıza iş diliyle söylemeniz yeterlidir:
37
+
38
+ - "Hatırlatma 7 değil 14 gün önce gitsin."
39
+ - "Bakım kaydında yapılan işin türü de seçilsin (periyodik, arıza, kalibrasyon)" — yeni alan.
40
+ - "Ekipmanın tedarikçisi ve garanti bitiş tarihi de tutulsun" — yeni alan.
41
+ - "Geciken bakımlar sorumlunun yöneticisine de bildirilsin."
42
+
43
+ Bilinmesi gerekenler: sabah hatırlatması uygulamanın kendi adına gönderilir; bunun için portal yöneticisinin uygulamayı
44
+ kurarken **arka plan e-postasını onaylaması** gerekir. Onay yoksa hatırlatma gönderilmez, uygulamada "onay bekleniyor"
45
+ yazar ve onay verilince ertesi sabah gider. Bir sabahta en çok 5 kişiye e-posta gider; kalanlar ertesi sabaha kalır.