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,345 @@
1
+ // {{nameTr}} — sayfa (dört görünüm: Sertifikalarım · Ekibim · Tüm kayıtlar · Sertifika türleri). Liste, süzgeç, form,
2
+ // kişi seçici ve dosya yükleyici hazır bileşenlerdir; alan adları veri modelinden gelir. Geçerlilik sonu, durum ve ekip
3
+ // SUNUCUDA hesaplanır; sayfa yalnız gösterir.
4
+ import type { OneNavigationItem } from '@devoplus/one-sdk'
5
+ import { dataModel } from './data/types.ts'
6
+ import { renderClientScript } from './lib/client.ts'
7
+ import { label } from './lib/model.ts'
8
+ import { escapeHtml, i18n, renderPage, translator, view, type Lang, type Strings, type Theme } from './lib/page.ts'
9
+
10
+ export const STRINGS: Strings = {
11
+ tr: {
12
+ title: {{nameTrJs}},
13
+ waiting: "Portal'dan oturum bekleniyor…",
14
+ networkError: 'İstek gönderilemedi (ağ hatası).',
15
+ requestFailed: 'İşlem yapılamadı.',
16
+ readOnly: 'Vekalet ya da Yerine Görüntüle oturumundasınız: bu uygulamada yalnız görüntüleme yapabilirsiniz.',
17
+ myIntro: 'Size ait eğitim ve sertifika kayıtları. Geçerlilik sonu, tamamlanma tarihi ve türün geçerlilik süresinden hesaplanır. Belgeyi indirmek için satıra tıklayın.',
18
+ myBanner: '{expired} sertifikanızın süresi doldu, {expiring} sertifikanızın süresi 30 gün içinde doluyor.',
19
+ myNone: 'Süresi dolan ya da 30 gün içinde dolacak sertifikanız yok.',
20
+ teamIntro: 'Organizasyon şemasında size bağlı çalışanların kayıtları. Ekip bilgisi her açılışta organizasyon şemasından okunur.',
21
+ teamCount: 'Ekibinizde {n} kişi var.',
22
+ exportXlsx: "Excel'e aktar",
23
+ exportFailed: 'Dosya hazırlanamadı.',
24
+ listTruncated: "Liste çok uzun olduğu için ilk {n} kayıt gösteriliyor; tamamı için Excel'e aktarın.",
25
+ myTable: 'Sertifikalarım',
26
+ teamTable: 'Ekibimin sertifikaları',
27
+ status: 'Durum',
28
+ statusValid: 'Geçerli',
29
+ statusExpiring: 'Süresi doluyor',
30
+ statusExpired: 'Süresi doldu',
31
+ statusRenewed: 'Yenilendi',
32
+ statusNone: 'Süresiz',
33
+ noAttachment: 'Bu kayıtta belge yok.',
34
+ downloadFailed: 'Belge indirilemedi.',
35
+ recordsIntro: 'Bütün kayıtlar. Yeni kayıtta kişinin yöneticisi organizasyon şemasından alınır; yönetici değiştiyse "Yöneticileri yenile" ile güncelleyin.',
36
+ recordsBanner: 'Süresi dolmuş {expired}, 30 gün içinde dolacak {expiring} sertifika var (yenilenmiş olanlar hariç).',
37
+ lastRun: 'Son hatırlatma kontrolü: {day} — {sent} kişi için e-posta gönderildi.',
38
+ lastRunConsent: 'Son hatırlatma kontrolü: {day} — e-postalar gönderilemedi: arka plan e-postası için portal yöneticisinin uygulama kurulumunda onay vermesi gerekiyor.',
39
+ noRun: 'Henüz hatırlatma kontrolü yapılmadı.',
40
+ search: 'Ara',
41
+ recordsTable: 'Eğitim ve sertifika kayıtları',
42
+ recordForm: 'Kayıt bilgileri',
43
+ newRecord: 'Yeni kayıt',
44
+ recordSaved: 'Kayıt kaydedildi.',
45
+ refreshManagers: 'Yöneticileri yenile',
46
+ managersRefreshed: '{n} kayıtta yönetici güncellendi.',
47
+ typesIntro: 'Eğitim ve sertifika türleri. Geçerlilik süresi boş bırakılırsa tür süresizdir. Süre değişikliği yalnız sonraki kayıtlara uygulanır.',
48
+ typesTable: 'Sertifika türleri',
49
+ typeForm: 'Tür bilgileri',
50
+ newType: 'Yeni tür',
51
+ typeSaved: 'Tür kaydedildi.',
52
+ managersOnly: 'Bu işlem yalnız eğitim sorumlusu izni olan kişilere açıktır.',
53
+ },
54
+ en: {
55
+ title: {{nameEnJs}},
56
+ waiting: 'Waiting for the session from the Portal…',
57
+ networkError: 'Request could not be sent (network error).',
58
+ requestFailed: 'The action could not be completed.',
59
+ readOnly: 'You are in a Delegation or View as User session: you can only view this app.',
60
+ myIntro: 'Your training and certificate records. The expiry date is computed from the completion date and the validity of the type. Click a row to download the document.',
61
+ myBanner: '{expired} of your certificates have expired, {expiring} expire within 30 days.',
62
+ myNone: 'None of your certificates has expired or expires within 30 days.',
63
+ teamIntro: 'Records of the employees who report to you in the organization chart. The team is read from the organization chart each time.',
64
+ teamCount: 'There are {n} people in your team.',
65
+ exportXlsx: 'Export to Excel',
66
+ exportFailed: 'The file could not be prepared.',
67
+ listTruncated: 'The list is too long, so only the first {n} records are shown; export to Excel for all of them.',
68
+ myTable: 'My certificates',
69
+ teamTable: 'My team certificates',
70
+ status: 'Status',
71
+ statusValid: 'Valid',
72
+ statusExpiring: 'Expiring',
73
+ statusExpired: 'Expired',
74
+ statusRenewed: 'Renewed',
75
+ statusNone: 'No expiry',
76
+ noAttachment: 'This record has no document.',
77
+ downloadFailed: 'The document could not be downloaded.',
78
+ recordsIntro: 'All records. For a new record the manager is taken from the organization chart; if managers changed, update them with "Refresh managers".',
79
+ recordsBanner: '{expired} certificates have expired, {expiring} expire within 30 days (renewed ones excluded).',
80
+ lastRun: 'Latest reminder check: {day} — e-mail sent for {sent} people.',
81
+ lastRunConsent: 'Latest reminder check: {day} — e-mails could not be sent: the portal administrator must approve background e-mail in the app installation.',
82
+ noRun: 'No reminder check has run yet.',
83
+ search: 'Search',
84
+ recordsTable: 'Training and certificate records',
85
+ recordForm: 'Record details',
86
+ newRecord: 'New record',
87
+ recordSaved: 'Record saved.',
88
+ refreshManagers: 'Refresh managers',
89
+ managersRefreshed: 'Manager updated on {n} records.',
90
+ typesIntro: 'Training and certificate types. Leave the validity empty for a type that never expires. A validity change applies to later records only.',
91
+ typesTable: 'Certificate types',
92
+ typeForm: 'Type details',
93
+ newType: 'New type',
94
+ typeSaved: 'Type saved.',
95
+ managersOnly: 'Only people with the training manager permission can do this.',
96
+ },
97
+ }
98
+
99
+ const MODEL = {
100
+ manage: '{{permissionPrefix}}Manage',
101
+ labels: {
102
+ person: label(dataModel, 'certificates', 'person_name'),
103
+ manager: label(dataModel, 'certificates', 'manager_name'),
104
+ type: label(dataModel, 'certificates', 'type_id'),
105
+ typeName: label(dataModel, 'certificates', 'type_name'),
106
+ completed: label(dataModel, 'certificates', 'completed_on'),
107
+ expires: label(dataModel, 'certificates', 'expires_on'),
108
+ number: label(dataModel, 'certificates', 'certificate_no'),
109
+ attachment: label(dataModel, 'certificates', 'attachment'),
110
+ attachmentName: label(dataModel, 'certificates', 'attachment_name'),
111
+ typeTitle: label(dataModel, 'cert_types', 'name'),
112
+ validity: label(dataModel, 'cert_types', 'validity_months'),
113
+ description: label(dataModel, 'cert_types', 'description'),
114
+ active: label(dataModel, 'cert_types', 'active'),
115
+ },
116
+ }
117
+
118
+ export function renderShell(lang: Lang, theme: Theme, path: string): string {
119
+ const tx = translator(STRINGS, lang)
120
+ const btn = (id: string, key: string, cls = 'one-btn') => `<button type="button" class="${cls}" id="${id}" data-i18n="${key}">${escapeHtml(tx(key))}</button>`
121
+ const body = [
122
+ view(
123
+ '/',
124
+ path,
125
+ `${i18n(tx, 'myIntro', 'p', 'class="one-muted"')}
126
+ <p><strong id="myBanner"></strong></p>
127
+ <div class="app-toolbar">${btn('myExport', 'exportXlsx')}</div>
128
+ <one-table id="myTable" clickable caption="${escapeHtml(tx('myTable'))}"></one-table>`,
129
+ ),
130
+ view(
131
+ '/team',
132
+ path,
133
+ `${i18n(tx, 'teamIntro', 'p', 'class="one-muted"')}
134
+ <p class="one-muted" id="teamNote"></p>
135
+ <div class="app-toolbar" id="teamToolbar" hidden>${btn('teamExport', 'exportXlsx')}</div>
136
+ <one-table id="teamTable" clickable caption="${escapeHtml(tx('teamTable'))}"></one-table>`,
137
+ ),
138
+ view(
139
+ '/records',
140
+ path,
141
+ `${i18n(tx, 'recordsIntro', 'p', 'class="one-muted"')}
142
+ ${i18n(tx, 'managersOnly', 'p', 'class="one-muted" id="recordsDenied"')}
143
+ <div id="recordsBox" hidden>
144
+ <p><strong id="recordsBanner"></strong></p>
145
+ <p class="one-muted" id="runNote"></p>
146
+ <div class="app-toolbar">${btn('newRecord', 'newRecord')} ${btn('refreshManagers', 'refreshManagers')}</div>
147
+ <one-filter id="recordsFilter" for="recordsTable"></one-filter>
148
+ <one-table id="recordsTable" paging="page" export="xlsx csv" clickable caption="${escapeHtml(tx('recordsTable'))}"></one-table>
149
+ <section class="one-card">
150
+ <h2 data-i18n="recordForm">${escapeHtml(tx('recordForm'))}</h2>
151
+ <one-form id="recordForm" action="/api/certificates" method="POST"></one-form>
152
+ </section>
153
+ </div>`,
154
+ ),
155
+ view(
156
+ '/types',
157
+ path,
158
+ `${i18n(tx, 'typesIntro', 'p', 'class="one-muted"')}
159
+ <one-table id="typesTable" src="/api/cert_types" paging="page" export="xlsx csv" clickable caption="${escapeHtml(tx('typesTable'))}"></one-table>
160
+ ${i18n(tx, 'managersOnly', 'p', 'class="one-muted" id="typesDenied"')}
161
+ <section id="typeEditor" class="one-card" hidden>
162
+ <h2 data-i18n="typeForm">${escapeHtml(tx('typeForm'))}</h2>
163
+ <div class="app-toolbar">${btn('newType', 'newType')}</div>
164
+ <one-form id="typeForm" action="/api/cert_types" method="POST"></one-form>
165
+ </section>`,
166
+ ),
167
+ ].join('\n')
168
+ return renderPage({ lang, theme, title: tx('title'), body })
169
+ }
170
+
171
+ export function renderShellScript(navigation: readonly OneNavigationItem[], options: { actingWrites?: boolean } = {}): string {
172
+ return renderClientScript({ strings: STRINGS, navigation, ...(options.actingWrites !== undefined ? { actingWrites: options.actingWrites } : {}), body: pageScript() })
173
+ }
174
+
175
+ function pageScript(): string {
176
+ return `
177
+ var M = ${JSON.stringify(MODEL).replace(/<\//g, '<\\/')};
178
+ var myTable = $('myTable'), teamTable = $('teamTable'), recordsTable = $('recordsTable'), typesTable = $('typesTable');
179
+ var recordForm = $('recordForm'), typeForm = $('typeForm');
180
+ var typeOptions = [], TODAY = null;
181
+ var STATUS = [
182
+ { value: 'valid', label: { tr: S.tr.statusValid, en: S.en.statusValid } },
183
+ { value: 'expiring', label: { tr: S.tr.statusExpiring, en: S.en.statusExpiring } },
184
+ { value: 'expired', label: { tr: S.tr.statusExpired, en: S.en.statusExpired } },
185
+ { value: 'renewed', label: { tr: S.tr.statusRenewed, en: S.en.statusRenewed } },
186
+ { value: 'no-expiry', label: { tr: S.tr.statusNone, en: S.en.statusNone } }
187
+ ];
188
+ var listColumns = [
189
+ { key: 'type_name', label: M.labels.typeName },
190
+ { key: 'status', label: { tr: S.tr.status, en: S.en.status }, type: 'choice', options: STATUS },
191
+ { key: 'completed_on', label: M.labels.completed, type: 'date' },
192
+ { key: 'expires_on', label: M.labels.expires, type: 'date' },
193
+ { key: 'certificate_no', label: M.labels.number },
194
+ { key: 'attachment_name', label: M.labels.attachmentName }
195
+ ];
196
+ myTable.columns = listColumns;
197
+ teamTable.columns = [{ key: 'person_name', label: M.labels.person }].concat(listColumns);
198
+ recordsTable.columns = [
199
+ { key: 'person_name', label: M.labels.person },
200
+ { key: 'type_name', label: M.labels.typeName },
201
+ { key: 'completed_on', label: M.labels.completed, type: 'date' },
202
+ { key: 'expires_on', label: M.labels.expires, type: 'date' },
203
+ { key: 'manager_name', label: M.labels.manager },
204
+ { key: 'certificate_no', label: M.labels.number }
205
+ ];
206
+ $('recordsFilter').fields = [
207
+ { key: 'q', label: { tr: S.tr.search, en: S.en.search }, type: 'text' },
208
+ { key: 'expires_on', label: M.labels.expires, type: 'dateRange' }
209
+ ];
210
+ typesTable.columns = [
211
+ { key: 'name', label: M.labels.typeTitle },
212
+ { key: 'validity_months', label: M.labels.validity, type: 'number' },
213
+ { key: 'description', label: M.labels.description },
214
+ { key: 'active', label: M.labels.active, type: 'boolean' }
215
+ ];
216
+ typeForm.fields = [
217
+ { key: 'name', label: M.labels.typeTitle, required: true, maxLength: 120 },
218
+ { key: 'validity_months', label: M.labels.validity, type: 'number', min: 1, max: 240 },
219
+ { key: 'description', label: M.labels.description, maxLength: 300 },
220
+ { key: 'active', label: M.labels.active, type: 'boolean' }
221
+ ];
222
+ var fileField = { key: 'attachment', label: M.labels.attachment, type: 'file', accept: 'image/*,.pdf', maxSize: 5242880, action: '/api/files' };
223
+ function newType() { typeForm.setAttribute('action', '/api/cert_types'); typeForm.setAttribute('method', 'POST'); typeForm.value = { active: true }; one.setTitle(null); }
224
+ // Yeni kayıtta kişi ve tür seçilir; düzenlemede yalnız tarih, belge numarası ve belge (kişi ve tür değişmez).
225
+ function newRecord() {
226
+ recordForm.fields = [
227
+ { key: 'person', label: M.labels.person, type: 'person', required: true },
228
+ { key: 'type_id', label: M.labels.type, type: 'choice', options: typeOptions, required: true },
229
+ { key: 'completed_on', label: M.labels.completed, type: 'date', required: true },
230
+ { key: 'certificate_no', label: M.labels.number, maxLength: 60 },
231
+ fileField
232
+ ];
233
+ recordForm.setAttribute('action', '/api/certificates'); recordForm.setAttribute('method', 'POST');
234
+ recordForm.value = { completed_on: TODAY };
235
+ one.setTitle(null);
236
+ }
237
+ function editRecord(r) {
238
+ recordForm.fields = [
239
+ { key: 'completed_on', label: M.labels.completed, type: 'date', required: true },
240
+ { key: 'certificate_no', label: M.labels.number, maxLength: 60 },
241
+ fileField
242
+ ];
243
+ recordForm.setAttribute('action', '/api/certificates/' + encodeURIComponent(r.id)); recordForm.setAttribute('method', 'PATCH');
244
+ recordForm.value = { completed_on: r.completed_on, certificate_no: r.certificate_no, attachment: r.attachment ? { key: r.attachment, name: r.attachment_name || '' } : null };
245
+ one.setTitle((r.person_name || '') + ' - ' + (r.type_name || ''));
246
+ }
247
+ function download(r) {
248
+ if (!r.has_attachment && !r.attachment) { one.toast({ severity: 'info', summary: t('noAttachment') }); return; }
249
+ one.fetch('/api/certificates/' + encodeURIComponent(r.id) + '/attachment').then(function (res) {
250
+ if (!res.ok) { one.toast({ severity: 'error', summary: t('downloadFailed') }); return; }
251
+ return res.arrayBuffer().then(function (data) {
252
+ return one.download({ name: r.attachment_name || 'belge', mime: (res.headers.get('content-type') || 'application/octet-stream').split(';')[0], data: data });
253
+ });
254
+ }, function () { one.toast({ severity: 'error', summary: t('downloadFailed') }); });
255
+ }
256
+ function exportList(path, name) {
257
+ one.fetch(path + '/export.xlsx?lang=' + lang).then(function (res) {
258
+ if (!res.ok) {
259
+ return res.text().then(function (text) {
260
+ var body = null;
261
+ try { body = text ? JSON.parse(text) : null; } catch (e) { body = null; }
262
+ one.toast({ severity: 'error', summary: t('exportFailed'), detail: problem({ status: res.status, body: body }) });
263
+ });
264
+ }
265
+ return res.arrayBuffer().then(function (data) {
266
+ return one.download({ name: name + '.xlsx', mime: (res.headers.get('content-type') || '').split(';')[0], data: data });
267
+ });
268
+ }, function () { one.toast({ severity: 'error', summary: t('exportFailed') }); });
269
+ }
270
+ function count(items, status) { return items.filter(function (x) { return x.status === status; }).length; }
271
+ function loadMine() {
272
+ return api('GET', '/api/my/certificates').then(function (r) {
273
+ if (!r.ok) { $('myBanner').textContent = problem(r); myTable.rows = []; return; }
274
+ TODAY = r.body.today;
275
+ myTable.rows = r.body.items;
276
+ var expired = count(r.body.items, 'expired'), expiring = count(r.body.items, 'expiring');
277
+ $('myBanner').textContent = expired + expiring > 0 ? t('myBanner', { expired: fmtNumber(expired), expiring: fmtNumber(expiring) }) : t('myNone');
278
+ if (r.body.truncated) $('myBanner').textContent += ' ' + t('listTruncated', { n: fmtNumber(r.body.items.length) });
279
+ });
280
+ }
281
+ function loadTeam() {
282
+ return api('GET', '/api/team/certificates').then(function (r) {
283
+ if (!r.ok) { $('teamNote').textContent = problem(r); $('teamToolbar').setAttribute('hidden', ''); teamTable.rows = []; return; }
284
+ $('teamNote').textContent = t('teamCount', { n: fmtNumber(r.body.team.length) });
285
+ if (r.body.truncated) $('teamNote').textContent += ' ' + t('listTruncated', { n: fmtNumber(r.body.items.length) });
286
+ $('teamToolbar').removeAttribute('hidden');
287
+ teamTable.rows = r.body.items;
288
+ });
289
+ }
290
+ function loadSummary() {
291
+ return api('GET', '/api/summary').then(function (r) {
292
+ if (!r.ok) return;
293
+ var s = r.body;
294
+ $('recordsBanner').textContent = t('recordsBanner', { expired: fmtNumber(s.expired), expiring: fmtNumber(s.expiring) });
295
+ $('runNote').textContent = !s.lastRun ? t('noRun') : s.lastRun.consent_needed ? t('lastRunConsent', { day: fmtDate(s.lastRun.run_key) }) : t('lastRun', { day: fmtDate(s.lastRun.run_key), sent: fmtNumber(s.lastRun.sent_count) });
296
+ });
297
+ }
298
+ function loadTypes() {
299
+ return loadAll('/api/cert_types?active=true&_sort=name').then(function (r) {
300
+ if (!r.ok) { one.toast({ severity: 'error', summary: problem(r) }); return; }
301
+ typeOptions = r.body.items.map(function (x) { return { value: x.id, label: x.name }; });
302
+ if (recordForm.getAttribute('method') !== 'PATCH') newRecord();
303
+ });
304
+ }
305
+ $('myExport').addEventListener('click', function () { exportList('/api/my/certificates', t('myTable')); });
306
+ $('teamExport').addEventListener('click', function () { exportList('/api/team/certificates', t('teamTable')); });
307
+ myTable.addEventListener('rowclick', function (e) { download(e.detail.row); });
308
+ teamTable.addEventListener('rowclick', function (e) { download(e.detail.row); });
309
+ recordsTable.addEventListener('rowclick', function (e) { if (canWrite()) editRecord(e.detail.row); });
310
+ typesTable.addEventListener('rowclick', function (e) {
311
+ if (!can(M.manage) || !canWrite()) return;
312
+ var r = e.detail.row;
313
+ typeForm.setAttribute('action', '/api/cert_types/' + encodeURIComponent(r.id)); typeForm.setAttribute('method', 'PATCH');
314
+ typeForm.value = r; one.setTitle(r.name);
315
+ });
316
+ $('newRecord').addEventListener('click', newRecord);
317
+ $('newType').addEventListener('click', newType);
318
+ $('refreshManagers').addEventListener('click', function () {
319
+ api('POST', '/api/certificates/refresh-managers', {}).then(function (r) {
320
+ if (!r.ok) { one.toast({ severity: 'error', summary: problem(r) }); return; }
321
+ one.toast({ severity: 'success', summary: t('managersRefreshed', { n: fmtNumber(r.body.updated) }) });
322
+ recordsTable.reload();
323
+ });
324
+ });
325
+ recordForm.addEventListener('saved', function (e) {
326
+ e.preventDefault();
327
+ one.toast({ severity: 'success', summary: t('recordSaved') });
328
+ newRecord(); recordsTable.reload(); loadSummary(); loadMine();
329
+ });
330
+ typeForm.addEventListener('saved', function (e) {
331
+ e.preventDefault();
332
+ one.toast({ severity: 'success', summary: t('typeSaved') });
333
+ newType(); typesTable.reload(); loadTypes();
334
+ });
335
+ onSession(function () {
336
+ var manager = can(M.manage);
337
+ $('recordsBox').toggleAttribute('hidden', !manager); $('recordsDenied').toggleAttribute('hidden', manager);
338
+ var editor = manager && canWrite();
339
+ $('typeEditor').toggleAttribute('hidden', !editor); $('typesDenied').toggleAttribute('hidden', editor);
340
+ // Bütün kayıtlar yalnız eğitim sorumlusuna yüklenir (uç da izin ister).
341
+ if (manager) { recordsTable.setAttribute('src', '/api/certificates'); loadSummary(); loadTypes(); newType(); }
342
+ });
343
+ onReady(function () { loadMine(); loadTeam(); });
344
+ `
345
+ }
@@ -0,0 +1,57 @@
1
+ {
2
+ "scopes": ["orgschema.read", "mail.execute"],
3
+ "services": { "database": true, "files": true, "cache": false },
4
+ "menuIcon": "pi pi-verified",
5
+ "navigation": [
6
+ { "path": "/", "label": { "tr": "Sertifikalarım", "en": "My certificates" } },
7
+ { "path": "/team", "label": { "tr": "Ekibim", "en": "My team" } },
8
+ { "path": "/records", "label": { "tr": "Tüm kayıtlar", "en": "All records" }, "permission": "{{permissionPrefix}}Manage" },
9
+ { "path": "/types", "label": { "tr": "Sertifika türleri", "en": "Certificate types" }, "permission": "{{permissionPrefix}}Manage" }
10
+ ],
11
+ "permissions": [
12
+ { "code": "{{permissionPrefix}}Manage", "name": { "tr": "Eğitim ve sertifika kayıtlarını yönet", "en": "Manage training and certificate records" } }
13
+ ],
14
+ "agent": {
15
+ "summary": {
16
+ "tr": "Eğitim ve sertifika takibi: çalışanların eğitim ve sertifikaları, geçerlilik süreleri ve süresi dolmak üzere olanlar.",
17
+ "en": "Training and certificate tracking: employee trainings and certificates, validity periods and those about to expire."
18
+ },
19
+ "tools": [
20
+ {
21
+ "name": "expiring-certificates",
22
+ "title": { "tr": "Süresi yaklaşan sertifikalar", "en": "Expiring certificates" },
23
+ "description": {
24
+ "tr": "Geçerlilik süresi verilen gün içinde dolacak ya da dolmuş (yenilenmemiş) sertifikaları kişi, yönetici ve tür bilgisiyle, en acil olan önce döndürür. Yalnız eğitim kayıtlarını yönetme izni olanlara açıktır. Salt okunur.",
25
+ "en": "Returns certificates that expire within the given number of days or have expired (not renewed), with person, manager and type, most urgent first. Only for people allowed to manage training records. Read-only."
26
+ },
27
+ "readOnly": true,
28
+ "permission": "{{permissionPrefix}}Manage",
29
+ "input": {
30
+ "type": "object",
31
+ "properties": {
32
+ "days": { "type": "integer", "minimum": 0, "maximum": 365, "description": "Bugünden itibaren kaç gün (varsayılan 30; dolmuş olanlar her zaman dahil)" },
33
+ "limit": { "type": "integer", "minimum": 1, "maximum": 50, "description": "En çok kaç kayıt (varsayılan 20)" }
34
+ }
35
+ }
36
+ },
37
+ {
38
+ "name": "certificate-types",
39
+ "title": { "tr": "Sertifika türleri", "en": "Certificate types" },
40
+ "description": {
41
+ "tr": "Tanımlı eğitim ve sertifika türlerini geçerlilik süreleriyle (ay) döndürür; kişisel veri içermez. Salt okunur.",
42
+ "en": "Returns the defined training and certificate types with their validity (months); contains no personal data. Read-only."
43
+ },
44
+ "readOnly": true,
45
+ "permission": "{{permissionCode}}",
46
+ "input": { "type": "object", "properties": {} }
47
+ }
48
+ ]
49
+ },
50
+ "jobs": {
51
+ "schedules": [
52
+ { "name": "daily-certificate-reminders", "cron": "0 8 * * *", "timeZone": "Europe/Istanbul", "description": "Süresi 30 gün içinde dolacak sertifikaları kişiye ve yöneticisine e-postayla hatırlatır" }
53
+ ],
54
+ "queues": [],
55
+ "services": ["mail.execute"]
56
+ }
57
+ }
@@ -0,0 +1,28 @@
1
+ ## Bu uygulamanın şablonu: envanter (`envanter`)
2
+
3
+ Uygulama `create-devoplus-one-app --template envanter` 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: `items` (stok kalemi) + `movements` (hareket) + `stock_checks` (günlük kontrol). Değişince `pnpm data:generate` |
9
+ | `src/app.ts` | Uçlar: kalem tanımı, stok hareketi (eksi stok engelli), azalma e-postası, özet |
10
+ | `src/stock.ts` | Saf kurallar: hareket doğrulama (birim, ondalık, tavan), eşik, kontrol günü + `stock.test.ts` |
11
+ | `src/jobs.ts` | Sabah işi `daily-stock-check` (azalan kalemleri günün kaydına yazar; kullanıcısız, e-posta GÖNDERMEZ) |
12
+ | `src/shell.ts` | Sayfa: stok (süzgeç + grafik), hareketler, kalemler |
13
+ | `src/agent.ts` · `src/queries.ts` · `AGENT_GUIDE.md` | Devoplus yapay zekâsının araçları ve rehberi |
14
+ | `src/lib/*` | Şablonların ortak parçaları — gerekmedikçe değiştirmeyin |
15
+
16
+ Değişmezler (bozmayın):
17
+
18
+ - **Miktar YALNIZ hareketle değişir.** Kalem uçları `quantity` ve `low_notified_at` alanlarını istemciden ALMAZ.
19
+ - **Hareket kaydı ve miktar değişikliği TEK toplu yazımdır** (`env.DB.batch([hareket, miktar])` — ikisi birlikte yazılır
20
+ ya da hiçbiri): hareket yalnız kalem kullanımdaysa ve çıkışta eldeki miktar yetiyorsa (`quantity >= ?`) eklenir, miktar
21
+ yalnız o hareket eklendiyse değişir. "Önce oku, sonra yaz"a ya da iki ayrı yazıma çevirmeyin — iki eşzamanlı çıkış stoku
22
+ eksiye düşürürdü, ikinci yazım düşerse miktar değişmiş ama hareket eksik kalırdı.
23
+ - **Azalma e-postası bir kez gider:** damga koşullu güncellemeyle konur (`low_notified_at IS NULL`); gönderim
24
+ başarısızsa damga geri alınır. Miktar eşiğe çıkınca ya da eşik değişince damga silinir.
25
+ - **Hareket değiştirilmez ve silinmez** (405); düzeltme yeni bir hareket ya da sayımla yapılır.
26
+ - **Kişi kimlikleri:** kalem sorumlusu `responsible_org_id` (Organization Schema kimliği → `toOrgUserIds`).
27
+ - **Zamanlanmış iş kullanıcısızdır ve e-posta GÖNDEREMEZ.** "Her sabah azalanları e-postayla gönder" isteğinde bunun
28
+ bugün mümkün olmadığını söyleyin; sonuç uygulamada ve Devoplus yapay zekâsında görünür.
@@ -0,0 +1,40 @@
1
+ # {{nameTr}}
2
+
3
+ Kurumun stok kalemlerini, eldeki miktarlarını ve konumlarını izleyen bir envanter uygulaması. Devoplus Portal'ın
4
+ içinde, Portal menüsünden açılır. Bu uygulama Devoplus One'ın **envanter** şablonundan üretildi; ihtiyacınıza göre
5
+ değiştirilecek bir başlangıç noktasıdır.
6
+
7
+ ## Ne yapar
8
+
9
+ 1. **Stok:** bütün kalemlerin eldeki miktarı, birimi, konumu ve en az miktarı listelenir; azalan kalemler işaretlenir.
10
+ Ekranın üstünde kaç kalemin azaldığı ve son günlük kontrolün sonucu yazar; grafik kalemlerin konumlara dağılımını
11
+ gösterir. Liste Excel'e ya da CSV'ye aktarılabilir.
12
+ 2. **Hareketler:** stok sorumlusu giriş, çıkış ya da sayım girer. Çıkış eldeki miktardan fazla olamaz; sayım eldeki
13
+ miktarı sayılan değere eşitler. Her hareket kimin yaptığıyla birlikte kayıtta kalır ve değiştirilmez.
14
+ 3. **Kalemler:** stok sorumlusu kalem tanımlar (stok kodu, ad, birim, konum, en az miktar, sorumlu kişi).
15
+ 4. **Azalma uyarısı:** miktar en az miktarın altına düştüğünde kalemin sorumlusuna bir kez e-posta gider; miktar
16
+ yeniden yeterli seviyeye çıkınca uyarı yeniden kurulur. Her sabah 07:00'de yapılan kontrol azalan kalemleri kaydeder.
17
+ 5. **Devoplus asistanı:** "Hangi malzemeler azaldı?" gibi sorular Devoplus'un yapay zekâ asistanına sorulduğunda
18
+ cevap bu uygulamadan gelir.
19
+
20
+ ## Kimler ne görür
21
+
22
+ | Kim | Ne yapabilir |
23
+ |---|---|
24
+ | Uygulamayı kullanma izni olan herkes ({{permissionCode}}) | Stoku, hareketleri ve grafiği görür, Excel'e aktarır |
25
+ | Stok sorumlusu ({{permissionPrefix}}Manage) | Kalem tanımlar, giriş, çıkış ve sayım girer |
26
+
27
+ İzinleri portal yöneticisi **Yetki Yönetimi** ekranından verir.
28
+
29
+ ## Neleri değiştirebilirsiniz
30
+
31
+ Yapay zekâ aracınıza iş diliyle söylemeniz yeterlidir:
32
+
33
+ - "Birimlere *paket* ekle" (ilk yayından önce netleştirin).
34
+ - "Çıkışta hangi departmana verildiği de sorulsun" — yeni alan.
35
+ - "Konumlar arasında transfer yapılabilsin" — yeni hareket türü.
36
+ - "Stok değeri (birim fiyat × miktar) de görünsün" — yeni hesap.
37
+
38
+ Bilinmesi gerekenler: azalma e-postası, miktarı eşiğin altına düşüren hareketi giren kişinin Portal'daki
39
+ **"İzinleri incele"** düğmesiyle e-posta izni vermiş olmasını gerektirir; izin yoksa hareket yine kaydedilir ve uyarı
40
+ bir sonraki harekette yeniden denenir. Sabah kontrolü e-posta göndermez, sonucu uygulamada gösterir.
@@ -0,0 +1,47 @@
1
+ # {{nameTr}} — Devoplus yapay zekâsı için rehber
2
+
3
+ Bu dosyayı **Devoplus yapay zekâsı** okur (uygulama adresinde `/.well-known/one-agent.md`). Bir aracı eklerken,
4
+ değiştirirken ya da kaldırırken bu dosyayı AYNI değişiklikte güncelleyin; yayın kapısı dört başlığı ve her aracın adını
5
+ arar.
6
+
7
+ ## Uygulama ne yapar
8
+
9
+ {{nameTr}}, kurumun stok kalemlerini, eldeki miktarlarını ve nerede durduklarını izleyen bir envanter uygulamasıdır.
10
+ Miktar yalnız stok hareketiyle değişir (giriş, çıkış, sayım) ve eksiye düşmez. Her kalemin bir "en az miktarı"
11
+ vardır; miktar bunun altına düşünce kalem "azaldı" sayılır, kalemin sorumlusuna bir kez e-posta gider ve her sabah
12
+ yapılan kontrol azalan kalemleri kaydeder. Kullanıcı "A4 kâğıt kaldı mı", "depoda kaç toner var", "hangi malzemeler
13
+ azaldı" gibi şeyler sorduğunda bu uygulamaya başvurun. Stok hareketi girmek için araç yoktur.
14
+
15
+ ## Veri
16
+
17
+ - **Stok kalemi** (`items`): stok kodu (tekil), ad, kategori, birim (`piece` adet · `box` kutu · `kg` · `litre` ·
18
+ `metre`), konum, eldeki miktar, en az miktar, sorumlu kişinin adı, kullanımda mı.
19
+ - **Stok hareketi** (`movements`): kalem, tür (`in` giriş · `out` çıkış · `count` sayım — sayım eldeki miktarı
20
+ sayılan değere eşitler), miktar, açıklama, işlemi yapan. Hareket değiştirilmez.
21
+ - **Günlük stok kontrolü** (`stock_checks`): her sabah 07:00'de (Türkiye saati) en az miktarın altındaki kalemler.
22
+ - En az miktarı 0 olan kalem hiçbir zaman "azaldı" sayılmaz.
23
+
24
+ ## Araçlar
25
+
26
+ ### `search-items` — stok kalemi ara (salt okunur)
27
+
28
+ - **Ne zaman:** kullanıcı bir malzemenin stokta olup olmadığını, ne kadar kaldığını ya da nerede durduğunu sorduğunda.
29
+ - **Girdi:** `query` — stok kodunda, adda ya da konumda geçen ifade; `limit` (1-50, varsayılan 10).
30
+ - **Çıktı:** `{ "items": [{ "sku", "name", "quantity", "min_quantity", "unit", "location", "low" }] }`.
31
+ - **Dikkat:** büyük/küçük harf eşleşmesi yalnız Latin harflerde yok sayılır; Türkçe harfli bir aramada sonuç
32
+ çıkmazsa ifadeyi küçük harfle yeniden deneyin.
33
+
34
+ ### `list-low-stock` — azalan stoku listele (salt okunur)
35
+
36
+ - **Ne zaman:** kullanıcı hangi malzemelerin azaldığını ya da sipariş edilmesi gerekenleri sorduğunda.
37
+ - **Girdi:** yok.
38
+ - **Çıktı:** `{ "count": 3, "items": [{ "sku", "name", "quantity", "min_quantity", "unit", "location" }],
39
+ "lastDailyCheck": { "day": "2026-10-20", "lowCount": 2 } }` — `items` en çok 50 kalem, en kritik olan önce;
40
+ `count` tam sayıdır. Hiç kontrol yapılmadıysa `lastDailyCheck` `null`.
41
+
42
+ ## Örnek istekler
43
+
44
+ - "A4 kâğıt kaldı mı?" → `search-items` (`query: "a4"`) → `quantity` ve `unit`
45
+ - "Hangi malzemeler azaldı?" → `list-low-stock`
46
+ - "Toner nerede duruyor?" → `search-items` → `location`
47
+ - "Depoya 20 kutu kâğıt girişi yap." → araç yok; kullanıcıyı uygulamanın Hareketler sekmesine yönlendirin.
@@ -0,0 +1,62 @@
1
+ {
2
+ "version": 1,
3
+ "entities": {
4
+ "items": {
5
+ "title": { "tr": "Stok kalemleri", "en": "Stock items" },
6
+ "fields": {
7
+ "sku": { "type": "text", "title": { "tr": "Stok kodu", "en": "SKU" }, "required": true, "unique": true, "maxLength": 40 },
8
+ "name": { "type": "text", "title": { "tr": "Ad", "en": "Name" }, "required": true, "maxLength": 120 },
9
+ "category": { "type": "text", "title": { "tr": "Kategori", "en": "Category" }, "maxLength": 60, "index": true },
10
+ "unit": {
11
+ "type": "choice",
12
+ "title": { "tr": "Birim", "en": "Unit" },
13
+ "required": true,
14
+ "default": "piece",
15
+ "choices": [
16
+ { "value": "piece", "title": { "tr": "Adet", "en": "Piece" } },
17
+ { "value": "box", "title": { "tr": "Kutu", "en": "Box" } },
18
+ { "value": "kg", "title": { "tr": "Kilogram", "en": "Kilogram" } },
19
+ { "value": "litre", "title": { "tr": "Litre", "en": "Litre" } },
20
+ { "value": "metre", "title": { "tr": "Metre", "en": "Metre" } }
21
+ ]
22
+ },
23
+ "location": { "type": "text", "title": { "tr": "Konum", "en": "Location" }, "maxLength": 120, "index": true },
24
+ "quantity": { "type": "number", "title": { "tr": "Miktar", "en": "Quantity" }, "required": true, "default": 0, "min": 0 },
25
+ "min_quantity": { "type": "number", "title": { "tr": "En az miktar", "en": "Minimum quantity" }, "required": true, "default": 0, "min": 0 },
26
+ "responsible_org_id": { "type": "text", "title": { "tr": "Sorumlu (kimlik)", "en": "Responsible (id)" }, "maxLength": 24 },
27
+ "responsible_name": { "type": "text", "title": { "tr": "Sorumlu", "en": "Responsible" }, "maxLength": 120 },
28
+ "low_notified_at": { "type": "datetime", "title": { "tr": "Azalma bildirimi", "en": "Low stock notice" } },
29
+ "active": { "type": "boolean", "title": { "tr": "Kullanımda", "en": "In use" }, "required": true, "default": true }
30
+ }
31
+ },
32
+ "movements": {
33
+ "title": { "tr": "Stok hareketleri", "en": "Stock movements" },
34
+ "fields": {
35
+ "item_id": { "type": "text", "title": { "tr": "Kalem", "en": "Item" }, "required": true, "maxLength": 36, "index": true },
36
+ "item_name": { "type": "text", "title": { "tr": "Kalem adı", "en": "Item name" }, "maxLength": 120 },
37
+ "kind": {
38
+ "type": "choice",
39
+ "title": { "tr": "Hareket", "en": "Movement" },
40
+ "required": true,
41
+ "index": true,
42
+ "choices": [
43
+ { "value": "in", "title": { "tr": "Giriş", "en": "In" } },
44
+ { "value": "out", "title": { "tr": "Çıkış", "en": "Out" } },
45
+ { "value": "count", "title": { "tr": "Sayım", "en": "Count" } }
46
+ ]
47
+ },
48
+ "quantity": { "type": "number", "title": { "tr": "Miktar", "en": "Quantity" }, "required": true, "min": 0 },
49
+ "note": { "type": "text", "title": { "tr": "Açıklama", "en": "Note" }, "maxLength": 200 },
50
+ "actor_name": { "type": "text", "title": { "tr": "İşlemi yapan", "en": "Done by" }, "maxLength": 120 }
51
+ }
52
+ },
53
+ "stock_checks": {
54
+ "title": { "tr": "Günlük stok kontrolleri", "en": "Daily stock checks" },
55
+ "fields": {
56
+ "run_key": { "type": "text", "title": { "tr": "Kontrol günü", "en": "Check day" }, "required": true, "unique": true, "maxLength": 20 },
57
+ "low_count": { "type": "number", "title": { "tr": "Eşiğin altındaki kalem", "en": "Items below minimum" }, "required": true, "default": 0, "min": 0 },
58
+ "low_items": { "type": "longText", "title": { "tr": "Eşiğin altındaki kalemler", "en": "Items below minimum" }, "maxLength": 20000 }
59
+ }
60
+ }
61
+ }
62
+ }
@@ -0,0 +1,20 @@
1
+ // Devoplus yapay zekâsının bu uygulamada çağırabileceği araçların İŞLEYİCİLERİ. Adı, açıklaması, girdi şeması,
2
+ // salt-okunurluğu ve izni `one.manifest.json` → `agent.tools`'tadır; rehber `AGENT_GUIDE.md`'dir. Yeni araç = üçü birlikte.
3
+ // Araçlar yalnız uygulamanın KENDİ veritabanını okur. Stok hareketi GİREN araç bilinçli olarak yoktur.
4
+ import { defineAgentTools } from '@devoplus/one-sdk'
5
+ import type { Env } from './app.ts'
6
+ import { lastCheck, lowStock, searchItems } from './queries.ts'
7
+
8
+ export const agentTools = defineAgentTools<Env>({
9
+ 'search-items': {
10
+ handler: async ({ input, env }) => ({
11
+ items: await searchItems(env, String(input.query), typeof input.limit === 'number' ? input.limit : 10),
12
+ }),
13
+ },
14
+ 'list-low-stock': {
15
+ handler: async ({ env }) => {
16
+ const [low, check] = await Promise.all([lowStock(env, 50), lastCheck(env)])
17
+ return { count: low.count, items: low.items, lastDailyCheck: check ? { day: check.run_key, lowCount: check.low_count } : null }
18
+ },
19
+ },
20
+ })