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,272 @@
1
+ // {{nameTr}} — sayfa (üç görünüm: Ziyaretlerim · Ekip ziyaretleri · Tüm ziyaretler). Liste, süzgeç ve form hazır
2
+ // bileşenlerdir; seçenekler veri modelinden gelir. Ekip ve ERP müşteri listesi sunucuda çözülür; sayfa yalnız gösterir.
3
+ import type { OneNavigationItem } from '@devoplus/one-sdk'
4
+ import { dataModel } from './data/types.ts'
5
+ import { renderClientScript } from './lib/client.ts'
6
+ import { choices, label } from './lib/model.ts'
7
+ import { escapeHtml, i18n, renderPage, translator, view, type Lang, type Strings, type Theme } from './lib/page.ts'
8
+
9
+ export const STRINGS: Strings = {
10
+ tr: {
11
+ title: {{nameTrJs}},
12
+ waiting: "Portal'dan oturum bekleniyor…",
13
+ networkError: 'İstek gönderilemedi (ağ hatası).',
14
+ requestFailed: 'İşlem yapılamadı.',
15
+ readOnly: 'Vekalet ya da Yerine Görüntüle oturumundasınız: bu uygulamada yalnız görüntüleme yapabilirsiniz.',
16
+ mineIntro: 'Kaydettiğiniz müşteri görüşmeleri. Bir satırı seçip düzenleyebilir, sonraki adımı tamamlandı olarak işaretleyebilirsiniz.',
17
+ week: 'Bu hafta ({from} - {to}): {n} görüşme.',
18
+ overdue: '{n} sonraki adımın tarihi geçti.',
19
+ upcomingTitle: 'Yaklaşan sonraki adımlar',
20
+ upcomingEmpty: 'Yaklaşan sonraki adım yok.',
21
+ formTitle: 'Ziyaret bilgileri',
22
+ newVisit: 'Yeni ziyaret',
23
+ saved: 'Ziyaret kaydedildi.',
24
+ customerTitle: 'ERP müşteri listesi',
25
+ customerHint: 'Müşteriyi listeden seçin; listede yoksa adını elle yazın.',
26
+ customerSearch: 'Müşteri ara',
27
+ stateNotConfigured: 'ERP müşteri listesi bu uygulamaya bağlanmadı: müşteri adını elle yazın.',
28
+ stateNotPermitted: 'ERP müşteri listesine bu uygulamanın ya da sizin izniniz yok: müşteri adını elle yazın.',
29
+ stateUnavailable: 'ERP müşteri listesine şu anda ulaşılamıyor: müşteri adını elle yazabilir ya da sonra yeniden deneyebilirsiniz.',
30
+ stateMisconfigured: 'ERP müşteri listesi beklenen bilgileri döndürmüyor (uygulamanın sorgu ayarı yanlış): müşteri adını elle yazın.',
31
+ truncated: 'Liste çok uzun olduğu için kesildi; aradığınız müşteri yoksa adını elle yazın.',
32
+ myTable: 'Ziyaretlerim',
33
+ teamIntro: 'Organizasyon şemasında yöneticisi olduğunuz çalışanların müşteri görüşmeleri.',
34
+ teamNone: 'Organizasyon şemasında size bağlı çalışan yok.',
35
+ teamTable: 'Ekip ziyaretleri',
36
+ allIntro: 'Bütün satış ekibinin müşteri görüşmeleri.',
37
+ managersOnly: 'Bu bölüm yalnız bütün ziyaretleri görme izni olan kişilere açıktır.',
38
+ allTable: 'Tüm ziyaretler',
39
+ search: 'Ara',
40
+ dateRange: 'Ziyaret tarihi',
41
+ },
42
+ en: {
43
+ title: {{nameEnJs}},
44
+ waiting: 'Waiting for the session from the Portal…',
45
+ networkError: 'Request could not be sent (network error).',
46
+ requestFailed: 'The action could not be completed.',
47
+ readOnly: 'You are in a Delegation or View as User session: you can only view this app.',
48
+ mineIntro: 'Customer contacts you recorded. Select a row to edit it or mark the next step as done.',
49
+ week: 'This week ({from} - {to}): {n} contacts.',
50
+ overdue: '{n} next steps are past their date.',
51
+ upcomingTitle: 'Upcoming next steps',
52
+ upcomingEmpty: 'No upcoming next steps.',
53
+ formTitle: 'Visit details',
54
+ newVisit: 'New visit',
55
+ saved: 'Visit saved.',
56
+ customerTitle: 'ERP customer list',
57
+ customerHint: 'Pick the customer from the list; if it is not there, type the name.',
58
+ customerSearch: 'Search customers',
59
+ stateNotConfigured: 'The ERP customer list is not connected to this app: type the customer name.',
60
+ stateNotPermitted: 'This app or you have no permission for the ERP customer list: type the customer name.',
61
+ stateUnavailable: 'The ERP customer list cannot be reached right now: type the customer name or try again later.',
62
+ stateMisconfigured: 'The ERP customer list does not return the expected data (the app query setting is wrong): type the customer name.',
63
+ truncated: 'The list was cut because it is too long; if your customer is missing, type the name.',
64
+ myTable: 'My visits',
65
+ teamIntro: 'Customer contacts of the employees you manage in the organization schema.',
66
+ teamNone: 'No employees report to you in the organization schema.',
67
+ teamTable: 'Team visits',
68
+ allIntro: 'Customer contacts of the whole sales team.',
69
+ managersOnly: 'Only people allowed to see all visits can use this section.',
70
+ allTable: 'All visits',
71
+ search: 'Search',
72
+ dateRange: 'Visit date',
73
+ },
74
+ }
75
+
76
+ const MODEL = {
77
+ manage: '{{permissionPrefix}}Manage',
78
+ type: choices(dataModel, 'visits', 'visit_type'),
79
+ outcome: choices(dataModel, 'visits', 'outcome'),
80
+ source: choices(dataModel, 'visits', 'customer_source'),
81
+ labels: {
82
+ code: label(dataModel, 'visits', 'customer_code'),
83
+ customer: label(dataModel, 'visits', 'customer_name'),
84
+ source: label(dataModel, 'visits', 'customer_source'),
85
+ rep: label(dataModel, 'visits', 'rep_name'),
86
+ date: label(dataModel, 'visits', 'visit_date'),
87
+ type: label(dataModel, 'visits', 'visit_type'),
88
+ notes: label(dataModel, 'visits', 'notes'),
89
+ outcome: label(dataModel, 'visits', 'outcome'),
90
+ step: label(dataModel, 'visits', 'next_step'),
91
+ stepDate: label(dataModel, 'visits', 'next_step_date'),
92
+ stepDone: label(dataModel, 'visits', 'next_step_done'),
93
+ },
94
+ }
95
+
96
+ export function renderShell(lang: Lang, theme: Theme, path: string): string {
97
+ const tx = translator(STRINGS, lang)
98
+ const body = [
99
+ view(
100
+ '/',
101
+ path,
102
+ `${i18n(tx, 'mineIntro', 'p', 'class="one-muted"')}
103
+ <p><strong id="weekBanner"></strong> <span id="overdueBanner"></span></p>
104
+ <section class="one-card">
105
+ <h2 data-i18n="upcomingTitle">${escapeHtml(tx('upcomingTitle'))}</h2>
106
+ <one-table id="upcomingTable" no-search></one-table>
107
+ </section>
108
+ <section class="one-card">
109
+ <h2 data-i18n="formTitle">${escapeHtml(tx('formTitle'))}</h2>
110
+ <div class="app-toolbar"><button type="button" class="one-btn" id="newVisit" data-i18n="newVisit">${escapeHtml(tx('newVisit'))}</button></div>
111
+ <div id="customerBox">
112
+ <h3 data-i18n="customerTitle">${escapeHtml(tx('customerTitle'))}</h3>
113
+ <div id="customerState" class="one-message one-message-info" hidden></div>
114
+ <div id="customerPicker" hidden>
115
+ ${i18n(tx, 'customerHint', 'p', 'class="one-muted"')}
116
+ <one-filter id="customerFilter"></one-filter>
117
+ <one-table id="customerTable" clickable no-search></one-table>
118
+ </div>
119
+ </div>
120
+ <one-form id="visitForm" action="/api/visits" method="POST" no-cancel></one-form>
121
+ </section>
122
+ <one-table id="myTable" src="/api/visits" paging="page" export="xlsx csv" clickable caption="${escapeHtml(tx('myTable'))}"></one-table>`,
123
+ ),
124
+ view(
125
+ '/team',
126
+ path,
127
+ `${i18n(tx, 'teamIntro', 'p', 'class="one-muted"')}
128
+ ${i18n(tx, 'teamNone', 'p', 'class="one-message one-message-info" id="teamNone" hidden')}
129
+ <one-filter id="teamFilter" for="teamTable"></one-filter>
130
+ <one-table id="teamTable" src="/api/team/visits" paging="page" export="xlsx csv" caption="${escapeHtml(tx('teamTable'))}"></one-table>`,
131
+ ),
132
+ view(
133
+ '/all',
134
+ path,
135
+ `${i18n(tx, 'managersOnly', 'p', 'class="one-muted" id="allDenied"')}
136
+ <div id="allBox" hidden>
137
+ ${i18n(tx, 'allIntro', 'p', 'class="one-muted"')}
138
+ <one-filter id="allFilter" for="allTable"></one-filter>
139
+ <one-table id="allTable" src="/api/all/visits" paging="page" export="xlsx csv" caption="${escapeHtml(tx('allTable'))}"></one-table>
140
+ </div>`,
141
+ ),
142
+ ].join('\n')
143
+ return renderPage({ lang, theme, title: tx('title'), body })
144
+ }
145
+
146
+ export function renderShellScript(navigation: readonly OneNavigationItem[], options: { actingWrites?: boolean } = {}): string {
147
+ return renderClientScript({ strings: STRINGS, navigation, ...(options.actingWrites !== undefined ? { actingWrites: options.actingWrites } : {}), body: pageScript() })
148
+ }
149
+
150
+ function pageScript(): string {
151
+ return `
152
+ var M = ${JSON.stringify(MODEL).replace(/<\//g, '<\\/')};
153
+ var form = $('visitForm'), myTable = $('myTable'), teamTable = $('teamTable'), allTable = $('allTable'), upcoming = $('upcomingTable');
154
+ var customerTable = $('customerTable'), customerFilter = $('customerFilter');
155
+ var STATE_KEYS = { 'not-configured': 'stateNotConfigured', 'not-permitted': 'stateNotPermitted', unavailable: 'stateUnavailable', misconfigured: 'stateMisconfigured' };
156
+ var customerState = null;
157
+
158
+ var listColumns = [
159
+ { key: 'visit_date', label: M.labels.date, type: 'date' },
160
+ { key: 'customer_name', label: M.labels.customer },
161
+ { key: 'visit_type', label: M.labels.type, type: 'choice', options: M.type },
162
+ { key: 'outcome', label: M.labels.outcome, type: 'choice', options: M.outcome },
163
+ { key: 'next_step', label: M.labels.step },
164
+ { key: 'next_step_date', label: M.labels.stepDate, type: 'date' },
165
+ { key: 'next_step_done', label: M.labels.stepDone, type: 'boolean' }
166
+ ];
167
+ myTable.columns = listColumns.concat([{ key: 'customer_source', label: M.labels.source, type: 'choice', options: M.source }]);
168
+ teamTable.columns = [{ key: 'rep_name', label: M.labels.rep }].concat(listColumns);
169
+ allTable.columns = [{ key: 'rep_name', label: M.labels.rep }].concat(listColumns);
170
+ upcoming.columns = [
171
+ { key: 'next_step_date', label: M.labels.stepDate, type: 'date' },
172
+ { key: 'customer_name', label: M.labels.customer },
173
+ { key: 'next_step', label: M.labels.step }
174
+ ];
175
+ upcoming.emptyText = { tr: S.tr.upcomingEmpty, en: S.en.upcomingEmpty };
176
+ customerTable.columns = [{ key: 'code', label: M.labels.code }, { key: 'name', label: M.labels.customer }];
177
+ customerFilter.fields = [{ key: 'q', label: { tr: S.tr.customerSearch, en: S.en.customerSearch }, type: 'text' }];
178
+ var filterFields = [
179
+ { key: 'q', label: { tr: S.tr.search, en: S.en.search }, type: 'text' },
180
+ { key: 'visit_type', label: M.labels.type, type: 'choice', options: M.type },
181
+ { key: 'outcome', label: M.labels.outcome, type: 'choice', options: M.outcome },
182
+ { key: 'visit_date', label: { tr: S.tr.dateRange, en: S.en.dateRange }, type: 'dateRange' }
183
+ ];
184
+ $('teamFilter').fields = filterFields;
185
+ $('allFilter').fields = filterFields;
186
+
187
+ function fields(editing) {
188
+ var list = [
189
+ { key: 'customer_code', label: M.labels.code, maxLength: 40 },
190
+ { key: 'customer_name', label: M.labels.customer, required: true, maxLength: 160 },
191
+ { key: 'visit_date', label: M.labels.date, type: 'date', required: true },
192
+ { key: 'visit_type', label: M.labels.type, type: 'choice', options: M.type, required: true },
193
+ { key: 'outcome', label: M.labels.outcome, type: 'choice', options: M.outcome, required: true },
194
+ { key: 'notes', label: M.labels.notes, type: 'longText', maxLength: 4000 },
195
+ { key: 'next_step', label: M.labels.step, maxLength: 200 },
196
+ { key: 'next_step_date', label: M.labels.stepDate, type: 'date' }
197
+ ];
198
+ if (editing) list.push({ key: 'next_step_done', label: M.labels.stepDone, type: 'boolean' });
199
+ form.fields = list;
200
+ }
201
+ function today() { var d = new Date(); return d.getFullYear() + '-' + (d.getMonth() < 9 ? '0' : '') + (d.getMonth() + 1) + '-' + (d.getDate() < 10 ? '0' : '') + d.getDate(); }
202
+ function newVisit() {
203
+ fields(false);
204
+ form.setAttribute('action', '/api/visits'); form.setAttribute('method', 'POST');
205
+ form.value = { visit_date: today(), visit_type: 'visit' };
206
+ one.setTitle(null);
207
+ }
208
+ newVisit();
209
+
210
+ function showCustomerState(state, truncated) {
211
+ customerState = state;
212
+ var box = $('customerState');
213
+ if (state === 'ok') {
214
+ $('customerPicker').removeAttribute('hidden');
215
+ if (truncated) { box.textContent = t('truncated'); box.removeAttribute('hidden'); } else box.setAttribute('hidden', '');
216
+ } else {
217
+ $('customerPicker').setAttribute('hidden', '');
218
+ box.textContent = t(STATE_KEYS[state] || 'stateUnavailable'); box.removeAttribute('hidden');
219
+ }
220
+ }
221
+ var customerTruncated = false;
222
+ function loadCustomers(q) {
223
+ return api('GET', '/api/customers' + (q ? '?q=' + encodeURIComponent(q) : '')).then(function (r) {
224
+ if (!r.ok) { showCustomerState('unavailable', false); return; }
225
+ customerTruncated = r.body.truncated === true;
226
+ showCustomerState(r.body.state, customerTruncated);
227
+ customerTable.rows = r.body.items;
228
+ });
229
+ }
230
+ customerFilter.addEventListener('change', function (e) { loadCustomers((e.detail.params && e.detail.params.q) || ''); });
231
+ customerTable.addEventListener('rowclick', function (e) {
232
+ var c = e.detail.row;
233
+ form.value = Object.assign({}, form.value, { customer_code: c.code, customer_name: c.name });
234
+ });
235
+
236
+ function loadSummary() {
237
+ return api('GET', '/api/summary').then(function (r) {
238
+ if (!r.ok) return;
239
+ var s = r.body;
240
+ $('weekBanner').textContent = t('week', { from: fmtDate(s.week.from), to: fmtDate(s.week.to), n: fmtNumber(s.week.total) });
241
+ $('overdueBanner').textContent = s.overdue > 0 ? t('overdue', { n: fmtNumber(s.overdue) }) : '';
242
+ upcoming.rows = s.upcoming;
243
+ });
244
+ }
245
+ myTable.addEventListener('rowclick', function (e) {
246
+ if (!canWrite()) return;
247
+ var r = e.detail.row;
248
+ fields(true);
249
+ form.setAttribute('action', '/api/visits/' + encodeURIComponent(r.id)); form.setAttribute('method', 'PATCH');
250
+ form.value = r;
251
+ one.setTitle(r.customer_name);
252
+ });
253
+ $('newVisit').addEventListener('click', newVisit);
254
+ form.addEventListener('saved', function (e) {
255
+ e.preventDefault();
256
+ one.toast({ severity: 'success', summary: t('saved') });
257
+ newVisit(); myTable.reload(); loadSummary();
258
+ });
259
+ function loadTeamNote() {
260
+ return api('GET', '/api/team/visits?limit=1').then(function (r) {
261
+ if (r.ok) $('teamNone').toggleAttribute('hidden', r.body.teamSize !== 0);
262
+ });
263
+ }
264
+ onSession(function () {
265
+ var manager = can(M.manage);
266
+ $('allBox').toggleAttribute('hidden', !manager); $('allDenied').toggleAttribute('hidden', manager);
267
+ if (manager) allTable.reload();
268
+ });
269
+ onLang(function () { loadSummary(); if (customerState) showCustomerState(customerState, customerTruncated); });
270
+ onReady(function () { loadSummary(); loadCustomers(''); loadTeamNote(); });
271
+ `
272
+ }
@@ -0,0 +1,65 @@
1
+ import type { OrgUserWithManagers } from '@devoplus/one-sdk'
2
+ import { describe, expect, it } from 'vitest'
3
+ import { CUSTOMER_QUERY, isConfigured, searchCustomers } from './customers.ts'
4
+ import { BACKDATE_MAX_DAYS, checkVisit, istanbulDay, teamOf, weekRange } from './visit.ts'
5
+
6
+ const NOW = new Date('2026-10-21T09:00:00Z') // çarşamba
7
+
8
+ function user(id: string, functionalManager: string): OrgUserWithManagers {
9
+ return {
10
+ id,
11
+ userCode: null,
12
+ fullName: id,
13
+ companyId: '',
14
+ workplaceId: '',
15
+ departmentId: '',
16
+ employeeTypeId: '',
17
+ titleId: '',
18
+ positionId: '',
19
+ functionalManager,
20
+ operationalManager: '',
21
+ isEmployee: true,
22
+ isActive: true,
23
+ }
24
+ }
25
+
26
+ describe('ziyaret kuralları', () => {
27
+ it('hafta İstanbul saatiyle pazartesi - pazar', () => {
28
+ expect(weekRange(NOW)).toEqual({ from: '2026-10-19', to: '2026-10-25' })
29
+ expect(weekRange(new Date('2026-10-25T20:59:00Z'))).toEqual({ from: '2026-10-19', to: '2026-10-25' })
30
+ expect(weekRange(new Date('2026-10-25T21:00:00Z'))).toEqual({ from: '2026-10-26', to: '2026-11-01' })
31
+ expect(istanbulDay(NOW)).toBe('2026-10-21')
32
+ })
33
+
34
+ it('ziyaret bugünden ileri ve bir yıldan eski olamaz; sonraki adım tarihiyle birlikte ve ziyaretten sonra', () => {
35
+ expect(checkVisit({ visit_date: '2026-10-21', next_step: null, next_step_date: null }, NOW)).toBeNull()
36
+ expect(checkVisit({ visit_date: '2026-10-22', next_step: null, next_step_date: null }, NOW)).toMatchObject({ field: 'visit_date' })
37
+ expect(checkVisit({ visit_date: '2025-01-01', next_step: null, next_step_date: null }, NOW)).toMatchObject({ field: 'visit_date' })
38
+ expect(BACKDATE_MAX_DAYS).toBeGreaterThan(0)
39
+ expect(checkVisit({ visit_date: '2026-10-20', next_step: 'Teklif gönder', next_step_date: null }, NOW)).toMatchObject({ field: 'next_step_date' })
40
+ expect(checkVisit({ visit_date: '2026-10-20', next_step: '', next_step_date: '2026-10-30' }, NOW)).toMatchObject({ field: 'next_step' })
41
+ expect(checkVisit({ visit_date: '2026-10-20', next_step: 'Teklif', next_step_date: '2026-10-19' }, NOW)).toMatchObject({ field: 'next_step_date' })
42
+ expect(checkVisit({ visit_date: '2026-10-20', next_step: 'Teklif', next_step_date: '2026-10-30' }, NOW)).toBeNull()
43
+ })
44
+
45
+ it('ekip: yöneticisi ben olanlar; derinlik sınırlı, döngü ve kendim hariç', () => {
46
+ const users = [user('A', 'M'), user('B', 'm'), user('C', 'A'), user('M', 'C'), user('X', '')]
47
+ expect(teamOf(users, 'M').sort()).toEqual(['a', 'b'])
48
+ expect(teamOf(users, 'm', 2).sort()).toEqual(['a', 'b', 'c'])
49
+ expect(teamOf(users, 'm', 5).sort()).toEqual(['a', 'b', 'c'])
50
+ expect(teamOf(users, 'X')).toEqual([])
51
+ })
52
+
53
+ it('ERP müşteri sorgusu yer tutucu: doldurulmadan bağlı sayılmaz; arama Türkçe harflere duyarsız', () => {
54
+ expect(isConfigured(CUSTOMER_QUERY)).toBe(false)
55
+ expect(isConfigured({ key: 'ERP.Customers', codeColumn: 'Code', nameColumn: '' })).toBe(false)
56
+ expect(isConfigured({ key: 'ERP.Customers', codeColumn: 'Code', nameColumn: 'Name' })).toBe(true)
57
+ const list = [
58
+ { code: 'C1', name: 'Çağlar Ticaret' },
59
+ { code: 'C2', name: 'İstanbul Gıda' },
60
+ ]
61
+ expect(searchCustomers(list, 'caglar', 10).map((c) => c.code)).toEqual(['C1'])
62
+ expect(searchCustomers(list, 'ISTANBUL', 10).map((c) => c.code)).toEqual(['C2'])
63
+ expect(searchCustomers(list, '', 1)).toHaveLength(1)
64
+ })
65
+ })
@@ -0,0 +1,98 @@
1
+ // Müşteri ziyareti kuralları — SAF fonksiyonlar (veritabanı ve ağ yok): tarih kuralları, sonraki adım, hafta aralığı ve
2
+ // "ekibim kim" hesabı. Sunucu her kaydı burada doğrular; kuralı değiştiren bu dosyayı ve `visit.test.ts`'i birlikte değiştirir.
3
+ import type { OrgUserWithManagers } from '@devoplus/one-sdk'
4
+
5
+ /** Bütün ziyaretleri görme izni (manifest `permissions`): satış müdürü, bölge yöneticisi. */
6
+ export const MANAGE_PERMISSION = '{{permissionPrefix}}Manage'
7
+
8
+ /**
9
+ * Ekip derinliği: 1 = yalnız doğrudan bağlı çalışanlar (organizasyon şemasında yöneticisi BEN olanlar); 2 = onların
10
+ * bağlıları da … Ekip her istekte organizasyon şemasından SUNUCUDA hesaplanır; kayıtta yönetici tutulmaz, bu yüzden
11
+ * şema değişince ekip görünümü de kendiliğinden değişir.
12
+ */
13
+ export const TEAM_DEPTH = 1
14
+ /** Geçmişe dönük en çok kaç günlük ziyaret girilebilir. */
15
+ export const BACKDATE_MAX_DAYS = 365
16
+
17
+ const DATE_RE = /^\d{4}-\d{2}-\d{2}$/
18
+ const DAY_MS = 86_400_000
19
+
20
+ /** İstanbul saatiyle gün (YYYY-AA-GG). Türkiye'de yaz saati uygulaması yoktur (UTC+3). */
21
+ export function istanbulDay(at: Date): string {
22
+ return new Date(at.getTime() + 3 * 3_600_000).toISOString().slice(0, 10)
23
+ }
24
+
25
+ function addDays(day: string, days: number): string {
26
+ return new Date(Date.parse(`${day}T00:00:00Z`) + days * DAY_MS).toISOString().slice(0, 10)
27
+ }
28
+
29
+ /** Bu hafta: İstanbul saatiyle pazartesi - pazar (ikisi de dahil). */
30
+ export function weekRange(now: Date): { from: string; to: string } {
31
+ const today = istanbulDay(now)
32
+ const weekday = new Date(`${today}T00:00:00Z`).getUTCDay() // 0 = pazar
33
+ const from = addDays(today, -((weekday + 6) % 7))
34
+ return { from, to: addDays(from, 6) }
35
+ }
36
+
37
+ export interface VisitFields {
38
+ visit_date: unknown
39
+ next_step: unknown
40
+ next_step_date: unknown
41
+ }
42
+
43
+ export type VisitIssue = { field: 'visit_date' | 'next_step' | 'next_step_date'; message: string } | null
44
+
45
+ const text = (v: unknown): string => (typeof v === 'string' ? v.trim() : '')
46
+
47
+ /**
48
+ * Tarih kuralları: ziyaret bugünden ileri olamaz (gerçekleşmiş görüşme kaydedilir) ve bir yıldan eski olamaz; sonraki adım
49
+ * yazıldıysa tarihi de verilir (ya da tersi) ve sonraki adım ziyaretten önce olamaz.
50
+ */
51
+ export function checkVisit(values: VisitFields, now: Date): VisitIssue {
52
+ const today = istanbulDay(now)
53
+ const visitDate = text(values.visit_date)
54
+ if (!DATE_RE.test(visitDate)) return { field: 'visit_date', message: 'Ziyaret tarihini girin.' }
55
+ if (visitDate > today) return { field: 'visit_date', message: 'Ziyaret tarihi bugünden ileri olamaz; gerçekleşen görüşmeyi kaydedin.' }
56
+ if (visitDate < addDays(today, -BACKDATE_MAX_DAYS)) return { field: 'visit_date', message: `Ziyaret tarihi ${BACKDATE_MAX_DAYS} günden eski olamaz.` }
57
+ const step = text(values.next_step)
58
+ const stepDate = text(values.next_step_date)
59
+ if (step && !stepDate) return { field: 'next_step_date', message: 'Sonraki adımın tarihini girin.' }
60
+ if (stepDate && !step) return { field: 'next_step', message: 'Sonraki adımı yazın ya da tarihini silin.' }
61
+ if (stepDate && !DATE_RE.test(stepDate)) return { field: 'next_step_date', message: 'Sonraki adım tarihini girin.' }
62
+ if (stepDate && stepDate < visitDate) return { field: 'next_step_date', message: 'Sonraki adım ziyaretten önce olamaz.' }
63
+ return null
64
+ }
65
+
66
+ /**
67
+ * Ekibim: organizasyon şemasında (fonksiyonel) yöneticisi `managerId` olan kişiler, `depth` basamak aşağıya kadar.
68
+ * Kendisi dahil edilmez; döngülü şemada sonsuza gitmez. Kimlikler küçük harfe indirilir.
69
+ * ⛔ Ekip KESİLMEZ: liste görünürlüğün (kimin ziyaretini görürüm) ve ekip büyüklüğünün kaynağıdır. Bir üst sınır sondaki
70
+ * kişileri sessizce dışarıda bırakır; yönetici o kişilerin ziyaretlerini göremez ve bunu öğrenmezdi. Sorgu ekibi tek bağlı
71
+ * parametreyle (JSON dizisi) alır, kişi sayısı sorgu metnini büyütmez.
72
+ */
73
+ export function teamOf(users: readonly OrgUserWithManagers[], managerId: string, depth = TEAM_DEPTH): string[] {
74
+ const me = managerId.toLowerCase()
75
+ const byManager = new Map<string, string[]>()
76
+ for (const u of users) {
77
+ const m = typeof u.functionalManager === 'string' ? u.functionalManager.toLowerCase() : ''
78
+ if (!m) continue
79
+ const list = byManager.get(m) ?? []
80
+ list.push(u.id.toLowerCase())
81
+ byManager.set(m, list)
82
+ }
83
+ const seen = new Set<string>([me])
84
+ const team: string[] = []
85
+ let level = [me]
86
+ for (let d = 0; d < depth && level.length > 0; d++) {
87
+ const next: string[] = []
88
+ for (const m of level)
89
+ for (const id of byManager.get(m) ?? []) {
90
+ if (seen.has(id)) continue
91
+ seen.add(id)
92
+ team.push(id)
93
+ next.push(id)
94
+ }
95
+ level = next
96
+ }
97
+ return team
98
+ }
@@ -0,0 +1,37 @@
1
+ {
2
+ "scopes": ["orgschema.read"],
3
+ "services": { "database": true, "files": false, "cache": false },
4
+ "menuIcon": "pi pi-briefcase",
5
+ "navigation": [
6
+ { "path": "/", "label": { "tr": "Ziyaretlerim", "en": "My visits" } },
7
+ { "path": "/team", "label": { "tr": "Ekip ziyaretleri", "en": "Team visits" } },
8
+ { "path": "/all", "label": { "tr": "Tüm ziyaretler", "en": "All visits" }, "permission": "{{permissionPrefix}}Manage" }
9
+ ],
10
+ "permissions": [
11
+ { "code": "{{permissionPrefix}}Manage", "name": { "tr": "Bütün müşteri ziyaretlerini gör", "en": "See all customer visits" } }
12
+ ],
13
+ "agent": {
14
+ "summary": {
15
+ "tr": "Müşteri ziyaret raporu: satış ekibinin bu haftaki müşteri görüşmeleri, sonuçları ve yaklaşan sonraki adımlar.",
16
+ "en": "Customer visit report: the sales team's customer contacts this week, their outcomes and upcoming next steps."
17
+ },
18
+ "tools": [
19
+ {
20
+ "name": "weekly-visit-summary",
21
+ "title": { "tr": "Bu haftaki ziyaret özeti", "en": "This week's visit summary" },
22
+ "description": {
23
+ "tr": "Bu haftanın (pazartesi-pazar, Türkiye saati) müşteri görüşmelerini türe ve sonuca göre sayar, en çok görüşülen müşterileri ve yaklaşan sonraki adımları döndürür. Salt okunur.",
24
+ "en": "Counts this week's (Monday-Sunday, Turkey time) customer contacts by type and outcome, returns the most contacted customers and upcoming next steps. Read-only."
25
+ },
26
+ "readOnly": true,
27
+ "permission": "{{permissionCode}}",
28
+ "input": {
29
+ "type": "object",
30
+ "properties": {
31
+ "scope": { "type": "string", "enum": ["mine", "all"], "description": "mine: kullanıcının kendi görüşmeleri (varsayılan) · all: bütün görüşmeler (yalnız tüm ziyaretleri görme izni olana)" }
32
+ }
33
+ }
34
+ }
35
+ ]
36
+ }
37
+ }
@@ -0,0 +1,32 @@
1
+ ## Bu uygulamanın şablonu: saha denetimi (`saha-denetimi`)
2
+
3
+ Uygulama `create-devoplus-one-app --template saha-denetimi` 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: `checklists` + `checklist_items` (tanım) · `audits` (denetim) + `audit_answers` (madde cevabı). Değişince `pnpm data:generate` |
9
+ | `src/app.ts` | Uçlar: kontrol listesi/madde tanımı, denetim başlatma (madde kopyası), taslak kaydı, tamamlama, rapor, fotoğraf, pano |
10
+ | `src/score.ts` | Saf kurallar: ağırlıklı puan, kritik madde tavanı, sonuç bantları, tamamlama eksikleri + `score.test.ts` |
11
+ | `src/queries.ts` | Pano ve yapay zekâ araçlarının ortak sorguları (yalnız tamamlanmış denetimler) |
12
+ | `src/shell.ts` | Sayfa: Denetimlerim · Pano · Kontrol listeleri + denetim paneli (form, sonuç tablosu, yazdırma) |
13
+ | `src/agent.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
+ - **Puan YALNIZ sunucuda hesaplanır** (`computeScore`, tamamlama ucunda). Denetim uçları `score`, `result`, `status`,
19
+ sayılar ve `critical_failed` alanlarını istemciden ALMAZ (400); tamamlama ucunun gövdesi boştur.
20
+ - **Tamamlanan denetim değişmez:** cevap ucu ve silme yalnız taslakta çalışır (409); genel güncelleme yoktur (405).
21
+ Cevap güncellemeleri `EXISTS (… status = 'draft')` koşuluyla aynı toplu yazımda yürür; tamamlama, denetimin
22
+ `updated_at`'i okunduğundan beri değiştiyse durur. Bu koşulları "önce oku, sonra yaz"a çevirmeyin.
23
+ - **Madde kopyası:** denetim başlarken kontrol listesinin maddeleri `audit_answers`'a kopyalanır. Puan kopyadan
24
+ hesaplanır; kontrol listesini düzenlemek geçmiş denetimi değiştirmez. Puanı canlı listeden hesaplamayın.
25
+ - **Görünürlük:** kişi yalnız kendi denetimini görür (`records: 'own'`); yönetici (`…Manage`) hepsini görür. Başkasının
26
+ denetimi "bulunamadı" (404) döner — kimlik yoklanamaz. Denetimi yalnız denetçi doldurur (yönetici bile değiştiremez).
27
+ - **Yer:** `location_id` organizasyon şemasındaki iş yeri kimliğidir ve sunucuda şemaya karşı doğrulanır; ad şemadan
28
+ yazılır. Şema okunamazsa ekran serbest metne döner. Pano yerleri `location_name`'e göre gruplar.
29
+ - **Fotoğraf:** yalnız kullanıcının kendi yükleme ön ekindeki anahtar kayda yazılır (`ownFileKey`); indirme denetimi
30
+ görebilen kişiye açıktır.
31
+ - Telefon numarası alanı EKLEMEYİN; onay akışı (onaylayan zinciri) bu uygulamanın işi değildir — kurumun İş Akışı
32
+ modülü kullanılır.
@@ -0,0 +1,44 @@
1
+ # {{nameTr}}
2
+
3
+ Mağaza ve şubelerde yapılan saha denetimlerini kaydeden bir uygulama. Devoplus Portal'ın içinde, Portal menüsünden
4
+ açılır. Bu uygulama Devoplus One'ın **saha denetimi** şablonundan üretildi; ihtiyacınıza göre değiştirilecek bir
5
+ başlangıç noktasıdır.
6
+
7
+ ## Ne yapar
8
+
9
+ 1. **Kontrol listeleri:** yönetici denetimde bakılacak maddeleri tanımlar. Her maddenin bir bölümü, bir ağırlığı (1-10)
10
+ vardır ve "kritik" olarak işaretlenebilir.
11
+ 2. **Denetimlerim:** denetçi kontrol listesini ve denetlenen yeri seçer. Yer, Portal'daki iş yerleri listesinden
12
+ seçilir; listede yoksa adı elle yazılır. Her madde "uygun", "uygun değil" ya da "uygulanamaz" olarak işaretlenir;
13
+ not ve fotoğraf eklenebilir. Denetim taslak olarak kaydedilip sonra tamamlanabilir.
14
+ 3. **Puan:** denetim tamamlanınca puanı uygulama hesaplar: uygun maddelerin ağırlığının, değerlendirilen maddelerin
15
+ ağırlığına oranı. "Uygulanamaz" maddeler puana girmez. Kritik bir madde uygun değilse denetim her zaman başarısız
16
+ sayılır. 85 ve üstü başarılı, 60 ile 85 arası geliştirilmeli, 60'ın altı başarısızdır.
17
+ 4. **Değişmez kayıt:** tamamlanan denetim değiştirilemez ve silinemez. Kontrol listesi sonradan değişse de geçmiş
18
+ denetimler ve puanları aynı kalır.
19
+ 5. **Rapor:** her denetimin raporu yazdırılabilir (tarayıcının "PDF olarak kaydet" seçeneğiyle PDF yapılabilir).
20
+ Denetim listeleri Excel'e aktarılabilir.
21
+ 6. **Pano:** yönetici son 30 günün özetini, yerlere göre son ve ortalama puanı ve son denetimleri görür.
22
+ 7. **Devoplus asistanı:** "Hangi mağazaların puanı düşük?" gibi sorular Devoplus'un yapay zekâ asistanına
23
+ sorulduğunda cevap bu uygulamadan gelir.
24
+
25
+ ## Kimler ne görür
26
+
27
+ | Kim | Ne yapabilir |
28
+ |---|---|
29
+ | Uygulamayı kullanma izni olan herkes ({{permissionCode}}) | Denetim yapar; yalnız kendi denetimlerini görür |
30
+ | Yönetici ({{permissionPrefix}}Manage) | Kontrol listelerini yönetir, bütün denetimleri ve panoyu görür, Excel'e aktarı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
+ - "Başarılı sayılma sınırı 90 olsun" — puan bandı.
39
+ - "Uygun değil seçilen maddede fotoğraf zorunlu olsun" — yeni kural.
40
+ - "Denetim tamamlanınca mağaza müdürüne e-posta gitsin" — bildirim (e-posta izni gerekir).
41
+ - "Her ay her mağaza en az bir kez denetlensin, eksik olanlar panoda görünsün" — yeni pano bilgisi.
42
+
43
+ Bilinmesi gerekenler: iş yeri listesi Portal'ın organizasyon bilgisinden okunur; okunamadığında yerin adı elle
44
+ yazılır. Fotoğraflar uygulamanın kendi dosya alanında saklanır ve yalnız denetçi ile yöneticiler indirebilir.
@@ -0,0 +1,55 @@
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}}, mağaza ve şubelerde yapılan saha denetimlerini kaydeder. Yönetici kontrol listeleri tanımlar (bölüm, madde,
10
+ ağırlık, kritik madde). Denetçi bir yeri seçip denetimi başlatır, her maddeyi "uygun", "uygun değil" ya da
11
+ "uygulanamaz" olarak işaretler, not ve fotoğraf ekler ve denetimi tamamlar. Puanı uygulama hesaplar: uygun maddelerin
12
+ ağırlığının değerlendirilen maddelerin ağırlığına oranı (0-100). "Uygulanamaz" maddeler puana girmez. Kritik bir madde
13
+ uygun değilse denetim her zaman başarısızdır. Tamamlanan denetim değiştirilemez. Kullanıcı "Kadıköy mağazasının son
14
+ denetimi nasıldı", "hangi mağazaların puanı düşük", "son denetimlerde neler uygun değildi" gibi sorular sorduğunda bu
15
+ uygulamaya başvurun. Denetim açan ya da dolduran araç yoktur.
16
+
17
+ ## Veri
18
+
19
+ - **Kontrol listesi** (`checklists`) ve **maddeleri** (`checklist_items`): bölüm, madde metni, ağırlık (1-10),
20
+ kritik mi.
21
+ - **Denetim** (`audits`): denetlenen yer (iş yeri ya da elle yazılmış ad), kontrol listesi, denetçi, durum (`draft`
22
+ taslak · `completed` tamamlandı), tamamlanma zamanı, puan, sonuç (`passed` başarılı · `needs_improvement`
23
+ geliştirilmeli · `failed` başarısız), kritik madde uygun değil mi.
24
+ - **Denetim cevabı** (`audit_answers`): maddenin denetim anındaki kopyası, sonuç (`compliant` uygun · `noncompliant`
25
+ uygun değil · `not_applicable` uygulanamaz), not, fotoğraf.
26
+ - Sonuç bantları: 85 ve üstü başarılı, 60-85 arası geliştirilmeli, 60'ın altı başarısız. Taslağın puanı yoktur;
27
+ araçlar yalnız tamamlanmış denetimleri döndürür.
28
+
29
+ ## Araçlar
30
+
31
+ ### `recent-audit-results` — son denetim sonuçları (salt okunur)
32
+
33
+ - **Ne zaman:** kullanıcı bir yerin ya da genel olarak son denetimlerin sonucunu, puanını ya da uygun bulunmayan
34
+ maddeleri sorduğunda.
35
+ - **Girdi:** `location` (isteğe bağlı) — yer adında geçen ifade; `limit` (1-30, varsayılan 10).
36
+ - **Çıktı:** `{ "audits": [{ "location", "checklist", "auditor", "completedAt", "score", "result", "criticalFailed",
37
+ "failedItems": ["…"] }] }` — en yenisi önce; `failedItems` en çok 10 madde, kritik olanlar "(kritik)" ekiyle önce.
38
+ - **Dikkat:** yer adında büyük/küçük harf eşleşmesi yalnız Latin harflerde yok sayılır; Türkçe harfli bir aramada
39
+ sonuç çıkmazsa ifadeyi küçük harfle yeniden deneyin. Yalnız yöneticiler kullanabilir.
40
+
41
+ ### `location-scores` — yerlere göre denetim puanları (salt okunur)
42
+
43
+ - **Ne zaman:** kullanıcı mağazaları karşılaştırmak, en düşük ya da en yüksek puanlı yerleri görmek istediğinde.
44
+ - **Girdi:** yok.
45
+ - **Çıktı:** `{ "locations": [{ "location", "audits", "lastScore", "averageScore", "lastAuditAt", "criticalFailures" }] }`
46
+ — son denetimi en yeni olan önce, en çok 50 yer.
47
+
48
+ ## Örnek istekler
49
+
50
+ - "Kadıköy mağazasının son denetimi nasıldı?" → `recent-audit-results` (`location: "kadıköy"`) → `score`, `result`,
51
+ `failedItems`
52
+ - "Hangi mağazaların puanı düşük?" → `location-scores` → `lastScore`'a göre sıralayın
53
+ - "Son denetimlerde en sık uygun bulunmayan maddeler neler?" → `recent-audit-results` (`limit: 30`) → `failedItems`
54
+ - "Yarın Bakırköy mağazasını denetleyeceğim, denetimi aç." → araç yok; kullanıcıyı uygulamanın Denetimlerim sekmesine
55
+ yönlendirin.