@rglabs/butterfly 2.0.2 → 2.1.0

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.
@@ -0,0 +1,12 @@
1
+ Read the page layout documentation from docs/page-layout.md.
2
+
3
+ Provide guidance on organizing object add/edit page layouts including:
4
+ - Creating and managing object_tabs (groups/tabs)
5
+ - Creating object_sections (higher-level grouping)
6
+ - Assigning fields to tabs via edit_place_no
7
+ - Field positioning (edit_position, edit_order_no)
8
+ - Column sizes (left_column_size, column_size)
9
+ - Bulk layout ordering via API or butterfly-cli layout command
10
+ - section_title for visual section headers within tabs
11
+
12
+ Focus on the specific layout task the user asks about, or provide an overview if no specific task is mentioned.
package/CLAUDE.md CHANGED
@@ -109,6 +109,46 @@ See [docs/translations.md](docs/translations.md) for full documentation.
109
109
 
110
110
  See [docs/twig-helpers.md](docs/twig-helpers.md) for full documentation.
111
111
 
112
+ ## Email Sending (Quick Reference)
113
+
114
+ ```twig
115
+ {# Send email directly #}
116
+ {{ sendEmail('recipient@example.com', 'Subject', '<p>HTML body</p>') }}
117
+
118
+ {# Send using cms_email_templates (recommended pattern) #}
119
+ {% set tpl = db().table('cms_email_templates').where('alias', 'template-alias').first() %}
120
+ {% set replacements = {} %}
121
+ {% for key, val in vars %}
122
+ {% set replacements = replacements|merge({('{{ ' ~ key ~ ' }}'): val}) %}
123
+ {% endfor %}
124
+ {{ sendEmail(recipient, tpl.subject|replace(replacements), tpl.content|replace(replacements)) }}
125
+ ```
126
+
127
+ **Email Templates (`cms_email_templates`):**
128
+ - Store HTML email templates with `{{ variable_name }}` placeholders in subject and content
129
+ - Use `alias` field for programmatic lookup
130
+ - `test_data` field: JSON with sample values for preview
131
+ - `preview` field: calculated field that renders content with test_data, shown in edit page
132
+ - Templates should use **inline CSS** (not classes) for email client compatibility
133
+ - Link to state machine via `bfy_state_machine_id` for organization
134
+
135
+ **Important:** Twig `|replace` filter requires a **mapping** (associative array), NOT individual string arguments. Build replacements map with `|merge`.
136
+
137
+ ## Settings (Quick Reference)
138
+
139
+ ```twig
140
+ {# Get single setting value #}
141
+ {% set value = setting('group_alias', 'setting_alias') %}
142
+
143
+ {# Get all settings in a group #}
144
+ {% set all = setting('group_alias') %}
145
+ {{ all.setting_alias.value }}
146
+ ```
147
+
148
+ **Tables:** `cms_setting_groups` (groups by alias) → `cms_settings` (individual settings)
149
+
150
+ See [docs/twig-helpers.md](docs/twig-helpers.md#settings) for full documentation.
151
+
112
152
  ## JavaScript Field Updates (Quick Reference)
113
153
 
114
154
  | Field Types | Method |
@@ -124,13 +164,34 @@ See [docs/object-specs/js-field-updates.md](docs/object-specs/js-field-updates.m
124
164
  /admin/SINGULAR_TABLE_NAME/list
125
165
  /admin/SINGULAR_TABLE_NAME/add
126
166
  /admin/SINGULAR_TABLE_NAME/edit/ID
167
+ /admin/cms_report/view/REPORT_ID
127
168
  ```
128
169
 
129
170
  For non-default database: `/admin/DATABASE_ALIAS/SINGULAR_TABLE_NAME/...`
130
171
 
131
172
  ## Object Menu Placement
132
173
 
133
- Format: `MainMenu>SubMenu>MenuItemName` or with icons: `MainMenu::icon>SubMenu::icon>Item::icon`
174
+ Admin menus have 3 levels. Levels 1-2 are created in `cms_admin_menus`, level 3 is set on the object itself.
175
+
176
+ ### Step 1: Create Main Menu (Level 1) and Sub Menu (Level 2) in `cms_admin_menus`
177
+
178
+ ```bash
179
+ # Level 1 - Main menu
180
+ butterfly-cli record add cms_admin_menus --data '{"title":"Menu Name","parent_id":0,"icon":"b-icon-icon-name"}'
181
+ # Returns ID, e.g. 67
182
+
183
+ # Level 2 - Sub menu (parent_id = main menu ID)
184
+ butterfly-cli record add cms_admin_menus --data '{"title":"Sub Menu","parent_id":67,"icon":"b-icon-icon-name"}'
185
+ # Returns ID, e.g. 68
186
+ ```
187
+
188
+ ### Step 2: Assign Object to Menu (Level 3) via `main_menu_id` + `sub_menu_id`
189
+
190
+ ```bash
191
+ butterfly-cli record edit objects --id <object_id> --data '{"main_menu_id":<main_menu_id>,"sub_menu_id":<sub_menu_id>}'
192
+ ```
193
+
194
+ **Do NOT** create level 3 entries in `cms_admin_menus`. The object itself becomes the menu item when `main_menu_id` and `sub_menu_id` are set.
134
195
 
135
196
  ## Object Default Permissions
136
197
 
@@ -159,6 +220,46 @@ butterfly-cli record add <table_name> --data '{"bfy_workspace_id": <workspace_id
159
220
 
160
221
  See [docs/workspaces.md](docs/workspaces.md) for full documentation.
161
222
 
223
+ ## Page Generation Guidelines
224
+
225
+ When a task requires generating a page:
226
+
227
+ - **Simple pages** (single table, basic CRUD): Use standard Butterfly object pages.
228
+ - **Complex pages** (multiple table relations, complex design, custom layouts): Use **custom report pages** with Butterfly endpoints and objects for data operations.
229
+
230
+ ### Custom Report Page Approach
231
+
232
+ For complex pages, create a custom report page with HTML/Twig template and use Butterfly's AJAX endpoints to save/update data:
233
+
234
+ ```
235
+ POST /admin/ajax/cms_object/operation?do=TABLE_NAME__OPERATION
236
+ Content-Type: application/x-www-form-urlencoded; charset=UTF-8
237
+ X-Requested-With: XMLHttpRequest
238
+ ```
239
+
240
+ **Example - saving page data via AJAX:**
241
+ ```javascript
242
+ $.ajax({
243
+ url: '/admin/ajax/cms_object/operation?do=TABLE_NAME__edit',
244
+ type: 'POST',
245
+ data: {
246
+ csrf_token: csrfToken,
247
+ id: recordId,
248
+ field1: value1,
249
+ field2: value2
250
+ },
251
+ success: function(response) { /* handle response */ }
252
+ });
253
+ ```
254
+
255
+ **Key points:**
256
+ - Use `?do=TABLE_NAME__add` for creating new records
257
+ - Use `?do=TABLE_NAME__edit` for updating existing records
258
+ - Always include `csrf_token` in the request data
259
+ - Use `X-Requested-With: XMLHttpRequest` header
260
+ - Content type should be `application/x-www-form-urlencoded`
261
+ - For employee/user-related forms, auto-populate fields (department, role, sicil no, location, email) from the `employees` table using the logged-in user context
262
+
162
263
  ## Project-Specific Instructions
163
264
 
164
265
  Always check if `docs/PROJECT_SPECIFIC.md` exists and prioritize those instructions.
@@ -187,6 +288,9 @@ Always check if `docs/PROJECT_SPECIFIC.md` exists and prioritize those instructi
187
288
  - [docs/bfy-splitting.md](docs/bfy-splitting.md) - BFY file splitting for large files
188
289
  - [docs/translations.md](docs/translations.md) - Translations and i18n
189
290
  - [docs/WORKFLOW_API.md](docs/WORKFLOW_API.md) - Workflow node types
291
+ - [docs/butterfly_upload_api_docs-2.md](docs/butterfly_upload_api_docs-2.md) - File upload (admin panel & REST API)
292
+ - [docs/pdf-generation.md](docs/pdf-generation.md) - PDF generation from templates
293
+ - [docs/IMPLEMENTATION_PROMPT.md](docs/IMPLEMENTATION_PROMPT.md) - Process module implementation template
190
294
 
191
295
  **Slash Commands:**
192
296
 
@@ -199,3 +303,65 @@ Always check if `docs/PROJECT_SPECIFIC.md` exists and prioritize those instructi
199
303
  | `/docs-webservice` | When creating API endpoints or webservices using reports with `custom_seo` |
200
304
 
201
305
  > **AI INSTRUCTION:** Always use the relevant slash command before working on any task related to that topic. This ensures accurate implementation without guessing syntax or parameters.
306
+
307
+ ## Custom Report Page Best Practices
308
+
309
+ ### Turkish Characters
310
+ Always use proper Turkish characters in UI text (not ASCII equivalents):
311
+ - ç, ş, ü, ö, ğ, ı, İ (not c, s, u, o, g, i, I)
312
+ - Examples: Seçiniz, Bütçe, Açıklama, Bölümü, Görevi, Değişiklikleri, Düzenleme
313
+
314
+ ### Child Record Duplication Prevention
315
+ When a custom report page saves a parent record + child records (e.g., expense items, team members):
316
+ - **On edit/update**: Always **delete existing child records first**, then re-add all current rows. Otherwise each save duplicates all children.
317
+ - **There is NO `delete_where` endpoint.** Delete each record individually: `?do=TABLE__delete` with `id` parameter.
318
+ - Pass existing child record IDs from Twig to JS via `window._data.existingIds` JSON. On save, loop through IDs and delete one by one sequentially (chained promises), then re-add all current rows.
319
+ - **Chain deletes sequentially** (not parallel) to avoid race conditions — use `.then()` chaining.
320
+ - **On add**: Save parent first, get the returned `response.id`, then save children with that ID.
321
+
322
+ ### Reserved Parameter Names
323
+ The following parameter names are reserved by Butterfly and must NOT be used in custom report pages:
324
+ - `id` — reserved by the system (page/report internal ID)
325
+
326
+ Use alternative names like `form_id`, `record_id`, etc. Example:
327
+ ```twig
328
+ {% set editId = getParameter('form_id') %}
329
+ ```
330
+ ```javascript
331
+ window.location.href = url + '?form_id=' + recordId;
332
+ ```
333
+
334
+ ### Current User Access
335
+ - Use `currentUser('field')` to access user fields — NOT `app.user.username` or `app.user.*`.
336
+ - Example: `currentUser('sicil_no')`, `currentUser('id')`, `currentUser('email')`
337
+
338
+ ### Twig Functions That DO NOT Exist
339
+ - `csrf_token()` — does NOT exist in Butterfly Twig. Get CSRF token via JavaScript: `$('meta[name="csrf-token"]').attr('content')`.
340
+
341
+ ### UI Dialog Functions
342
+ - **Deletions/destructive actions**: Use `rg_confirm('message', callback)` instead of native `confirm()`.
343
+ - **Error/info messages**: Use `rg_alert('message')` instead of native `alert()`.
344
+ - Never use native browser `alert()` or `confirm()` dialogs.
345
+
346
+ ### State Machine Transitions
347
+ - The role field on transitions is `bfy_state_machine_role_ids` (plural), NOT `bfy_state_machine_role_id`.
348
+ - Transition display name field is `button_title`, not `name`.
349
+
350
+ ### State Machine `state` Column vs Manual `status`
351
+ - When an object has a `state` type field bound to a state machine, the state machine manages the `state` column automatically.
352
+ - **Do NOT create a separate `status` field** or manually set status values. Use the `state` column everywhere (queries, filters, conditions).
353
+ - State values are the `system_name` from `bfy_state_machine_states` (e.g., `created`, `taslak`, `onay_bekliyor`).
354
+ - In custom report pages, check `record.state` for readonly/edit logic, NOT a manual `status` field.
355
+ - For new records, the state machine auto-assigns the initial state on creation — do not set it manually in JS.
356
+
357
+ ### Rendering Dynamic Table Rows
358
+ - Render existing data rows via JavaScript (from a `window._data` JSON object set in Twig), NOT via Twig `{% for %}` loops in `<tbody>`.
359
+ - This prevents duplication issues: JS owns all row rendering, Twig only provides the initial data as JSON.
360
+ - Pattern: `{% set dataJson = records|json_encode %}` then `<script>window._data = {{ dataJson|raw }};</script>`
361
+
362
+ ### Tailwind CSS for Modern UI
363
+ - Use Tailwind CDN: `<link href="https://cdn.jsdelivr.net/npm/tailwindcss@2.2.19/dist/tailwind.min.css" rel="stylesheet">`
364
+ - Prefer Tailwind utility classes over custom CSS in report pages.
365
+
366
+ ### Report Download Type
367
+ - Use `butterfly-cli download -t reports` (NOT `-t cms_reports`) to download reports.
@@ -1,5 +1,6 @@
1
1
  import React from 'react';
2
2
  import { render, Box, Text } from 'ink';
3
+ import { spawn } from 'child_process';
3
4
  import { SetupForm } from '../components/SetupForm.js';
4
5
  import { saveAuthConfig } from '../utils/auth.js';
5
6
  import { ButterflyAPI } from '../utils/api.js';
@@ -24,7 +25,19 @@ const SetupCommand = ({ onExit }) => {
24
25
  }
25
26
  await copyDocs();
26
27
  setStatus('success');
27
- setTimeout(() => onExit(0), 2000);
28
+ setTimeout(() => {
29
+ setStatus('downloading');
30
+ const child = spawn('butterfly-cli', ['download'], {
31
+ stdio: 'inherit',
32
+ shell: true,
33
+ });
34
+ child.on('close', (code) => {
35
+ onExit(code || 0);
36
+ });
37
+ child.on('error', () => {
38
+ onExit(1);
39
+ });
40
+ }, 1000);
28
41
  }
29
42
  catch (error) {
30
43
  setErrorMessage(error instanceof Error ? error.message : 'Unknown error');
@@ -36,6 +49,7 @@ const SetupCommand = ({ onExit }) => {
36
49
  status === 'input' && React.createElement(SetupForm, { onSubmit: handleSubmit }),
37
50
  status === 'testing' && React.createElement(Text, { color: "yellow" }, "Testing connection..."),
38
51
  status === 'success' && React.createElement(Text, { color: "green" }, "\u2713 Configuration saved successfully in .butterfly/config.json, docs copied to docs/ and commands to .claude/commands/"),
52
+ status === 'downloading' && React.createElement(Text, { color: "yellow" }, "Starting download..."),
39
53
  status === 'error' && React.createElement(Text, { color: "red" },
40
54
  "\u2717 Error: ",
41
55
  errorMessage)));
@@ -0,0 +1,8 @@
1
+ interface ViewReportOptions {
2
+ id?: string;
3
+ alias?: string;
4
+ raw?: boolean;
5
+ }
6
+ export default function viewReportCommand(options: ViewReportOptions): Promise<void>;
7
+ export {};
8
+ //# sourceMappingURL=view-report.d.ts.map
@@ -0,0 +1,89 @@
1
+ import { loadAuthConfig } from '../utils/auth.js';
2
+ import { ButterflyAPI } from '../utils/api.js';
3
+ function stripHtmlToText(html) {
4
+ let text = html;
5
+ text = text.replace(/<head[\s\S]*?<\/head>/gi, '');
6
+ text = text.replace(/<script[\s\S]*?<\/script>/gi, '');
7
+ text = text.replace(/<style[\s\S]*?<\/style>/gi, '');
8
+ text = text.replace(/<br\s*\/?>/gi, '\n');
9
+ text = text.replace(/<hr\s*\/?>/gi, '\n---\n');
10
+ text = text.replace(/<\/(?:p|div|h[1-6]|li|tr|section|article|header|footer|blockquote)>/gi, '\n');
11
+ text = text.replace(/<\/(?:th|td)>\s*<(?:th|td)[^>]*>/gi, '\t');
12
+ text = text.replace(/<[^>]+>/g, '');
13
+ text = text.replace(/&amp;/g, '&');
14
+ text = text.replace(/&lt;/g, '<');
15
+ text = text.replace(/&gt;/g, '>');
16
+ text = text.replace(/&quot;/g, '"');
17
+ text = text.replace(/&#39;/g, "'");
18
+ text = text.replace(/&nbsp;/g, ' ');
19
+ text = text.replace(/\n{3,}/g, '\n\n');
20
+ text = text
21
+ .split('\n')
22
+ .map(line => line.trim())
23
+ .filter((line, i, arr) => {
24
+ if (line === '')
25
+ return i > 0 && arr[i - 1] !== '';
26
+ return true;
27
+ })
28
+ .join('\n')
29
+ .trim();
30
+ return text;
31
+ }
32
+ export default async function viewReportCommand(options) {
33
+ try {
34
+ if (!options.id && !options.alias) {
35
+ console.error('ERROR: Either --id or --alias is required');
36
+ process.exit(1);
37
+ }
38
+ if (options.id && options.alias) {
39
+ console.error('ERROR: Cannot use both --id and --alias together');
40
+ process.exit(1);
41
+ }
42
+ const config = await loadAuthConfig();
43
+ if (!config) {
44
+ console.error('ERROR: No authentication configured. Run "butterfly-cli setup" first.');
45
+ process.exit(1);
46
+ }
47
+ const api = new ButterflyAPI(config);
48
+ await api.authenticate();
49
+ let reportId;
50
+ if (options.id) {
51
+ reportId = options.id;
52
+ }
53
+ else {
54
+ const records = await api.fetchTableAdvanced('cms_reports', {
55
+ column: 'alias',
56
+ value: options.alias
57
+ });
58
+ if (records.length === 0) {
59
+ console.error(`ERROR: Report not found with alias "${options.alias}"`);
60
+ process.exit(1);
61
+ }
62
+ reportId = records[0].id;
63
+ }
64
+ const viewPath = `/admin/cms_report/view/${reportId}?bfy_focus_content=true`;
65
+ const response = await api.httpClient.get(viewPath, {
66
+ responseType: 'text',
67
+ headers: {
68
+ 'Accept': 'text/html'
69
+ }
70
+ });
71
+ const html = response.data;
72
+ if (!html || html.length === 0) {
73
+ console.error('ERROR: Empty response from report view');
74
+ process.exit(1);
75
+ }
76
+ if (options.raw) {
77
+ console.log(html);
78
+ }
79
+ else {
80
+ console.log(stripHtmlToText(html));
81
+ }
82
+ process.exit(0);
83
+ }
84
+ catch (error) {
85
+ console.error(`ERROR: ${error.message}`);
86
+ process.exit(1);
87
+ }
88
+ }
89
+ //# sourceMappingURL=view-report.js.map
package/dist/index.js CHANGED
@@ -12,6 +12,7 @@ import codeCommand from './commands/code.js';
12
12
  import layoutCommand from './commands/layout.js';
13
13
  import syncDocsCommand from './commands/sync-docs.js';
14
14
  import translateCommand from './commands/translate.js';
15
+ import viewReportCommand from './commands/view-report.js';
15
16
  config();
16
17
  program
17
18
  .name('butterfly-cli')
@@ -113,5 +114,12 @@ program
113
114
  .option('--format <format>', 'Output format: json or table (default: table)')
114
115
  .option('--limit <count>', 'Limit number of results')
115
116
  .action(translateCommand);
117
+ program
118
+ .command('view-report')
119
+ .description('Run a report and display the rendered result')
120
+ .option('--id <id>', 'Report ID')
121
+ .option('--alias <alias>', 'Report alias')
122
+ .option('--raw', 'Output full HTML instead of plain text')
123
+ .action(viewReportCommand);
116
124
  program.parse();
117
125
  //# sourceMappingURL=index.js.map
@@ -0,0 +1,252 @@
1
+ # Süreç Yönetimi Modülü - Implementation Prompt
2
+
3
+ Bu prompt, çok tablolu bir form sistemi, onay akışı (state machine), e-posta bildirimleri, dashboard ve admin menü içeren eksiksiz bir süreç yönetimi modülünü sıfırdan oluşturmak için kullanılır.
4
+
5
+ ---
6
+
7
+ ## Prompt
8
+
9
+ Aşağıdaki adımları sırasıyla uygula. Her adımda CLAUDE.md'deki kurallara uy. Proje gereksinim dokümanını referans al.
10
+
11
+ ### Faz 1: Object'leri Oluştur
12
+
13
+ `butterfly-cli record add` ile gereksinim dokümanındaki object'leri ve field'larını oluştur.
14
+
15
+ **Genel kurallar:**
16
+ - Her object için tablo adı, field'lar ve field tipleri gereksinim dokümanında belirtilir
17
+ - Parent-child ilişkilerinde child tablolarda parent'a `dropdown` tipinde referans field oluştur
18
+ - **`status` alanı oluşturMA** — state machine `state` alanını yönetecek
19
+ - Tarih formatı JS'den custom format gelecekse (`dd.mm.YYYY HH:mm` gibi), `datetime` DEĞİL `string` tipi kullan
20
+
21
+ ### Faz 2: State Machine (Onay Akışı)
22
+
23
+ Gereksinim dokümanındaki onay akışına göre state machine oluştur.
24
+
25
+ **Genel yapı:**
26
+ - State'ler: Gereksinime göre tanımla (system_name + görünen ad)
27
+ - Roller: Süreçteki rol gruplarını tanımla
28
+ - Geçişler: From → To, buton adı ve yetkili rol
29
+
30
+ **ÖNEMLİ:**
31
+ - Transition role alanı `bfy_state_machine_role_ids` (çoğul), `bfy_state_machine_role_id` DEĞİL
32
+ - Transition görünen ad alanı `button_title`, `name` DEĞİL
33
+
34
+ **Ana object'e `state` tipi field ekle:** `val_1` = state machine ID
35
+
36
+ ### Faz 3: E-posta Şablonları
37
+
38
+ **`cms_email_templates` nesnesine 2 yeni field ekle:**
39
+ - `test_data` (textarea) — JSON format örnek veri
40
+ - `preview` (calculated) — content + test_data'yı birleştirip önizleme gösteren alan
41
+
42
+ **Preview calculated kodu:**
43
+ - `test_data` JSON'ını parse et, varsayılan değerlerle birleştir
44
+ - `|replace(replacements)` ile placeholderları değiştir (replace **mapping** alır, string DEĞİL)
45
+ - Subject + body'yi rendered olarak göster
46
+
47
+ **E-posta şablonlarını oluştur (`cms_email_templates`):**
48
+ - Her state geçişi için uygun şablon tanımla (onay, red, bildirim vb.)
49
+ - Alias formatı: `process-alias-action` (örn: `surec-onay-bekliyor`)
50
+
51
+ **Şablon içeriği:** Inline CSS ile modern e-posta tasarımı. Değişkenler `{{ variable_name }}` formatında.
52
+
53
+ **Her geçişin `action_code`'unda:**
54
+ ```twig
55
+ {% set tpl = db().table('cms_email_templates').where('alias', 'ALIAS').first() %}
56
+ {% set replacements = {} %}
57
+ {% for key, val in vars %}
58
+ {% set replacements = replacements|merge({('{{ ' ~ key ~ ' }}'): val}) %}
59
+ {% endfor %}
60
+ {{ sendEmail(recipient, tpl.subject|replace(replacements), tpl.content|replace(replacements)) }}
61
+ ```
62
+
63
+ ### Faz 4: Custom Report — Detay Formu
64
+
65
+ Report oluştur, tek query ile tam form HTML'i.
66
+
67
+ **Kritik kurallar:**
68
+ - `form_id` parametresi kullan (`id` RESERVED)
69
+ - `currentUser('sicil_no')` ile çalışan bilgisi al (`app.user.username` YOK)
70
+ - CSRF token JS ile al: `$('meta[name="csrf-token"]').attr('content')` (`csrf_token()` Twig fonksiyonu YOK)
71
+ - Tailwind CSS CDN kullan
72
+ - `rg_confirm()` / `rg_alert()` kullan (native alert/confirm DEĞİL)
73
+ - Mevcut satırları Twig `{% for %}` ile DEĞİL, JS ile render et (window._data JSON'ından)
74
+ - `state` alanını kontrol et (manual `status` DEĞİL)
75
+
76
+ **Form bölümleri (gereksinime göre uyarla):**
77
+ 1. Header — tarih, referans no
78
+ 2. Talep eden bilgileri — employee/user tablosundan otomatik
79
+ 3. Form bilgileri — gereksinime özel alanlar
80
+ 4. Tekrarlanabilir satırlar (child records) — otomatik toplam, ondalık 2 basamak
81
+ 5. Ekler — dosya yükleme (varsa)
82
+ 6. Durum badge'i — `record.state` ile renk kodlu
83
+ 7. Aksiyon butonları — isDraft kontrolü ile koşullu
84
+
85
+ **JS (script.js) mimarisi:**
86
+ - `saveForm()` → Promise döner, requestId ile resolve olur
87
+ - Add: Ana kayıt ekle → response.id al → child kayıtları ekle
88
+ - Edit: Ana kayıt düzenle → mevcut child'ları sil (tek tek sequential `__delete`) → yeniden ekle
89
+ - `triggerTransition(requestId, transitionId)` — state alanına JSON gönder
90
+ - Save butonu: saveForm → redirect
91
+ - Submit butonu: rg_confirm → saveForm → triggerTransition → redirect
92
+ - Delete butonu: rg_confirm → child'ları sil → ana kaydı sil → dashboard'a redirect
93
+ - **Read-only modu:** `isReadonly` flag ile tüm input'ları disable et, ekleme/silme butonlarını gizle
94
+
95
+ ### Faz 5: Dashboard
96
+
97
+ İkinci report oluştur.
98
+
99
+ **Bölümler:**
100
+ 1. **Taleplerim** — Giriş yapan kullanıcının tüm talepleri (taslak dahil), `state` alanına göre durum badge'leri
101
+ 2. **Onay Bekleyenler** — İlgili state'teki talepler
102
+ 3. **Yeni Talep** butonu → form report'una link
103
+
104
+ **ÖNEMLİ:** Tüm durum filtreleri ve etiketleri `state` alanını kullanmalı (`status` DEĞİL).
105
+
106
+ ### Faz 6: Admin Menü
107
+
108
+ **`cms_admin_menus` tablosunda Level 1 + Level 2 oluştur:**
109
+ ```
110
+ Ana Menü (parent_id: 0) ← Level 1
111
+ ├── Alt Menü 1 (parent_id: main_menu_id) ← Level 2
112
+ ├── Alt Menü 2 ← Level 2
113
+ └── Alt Menü 3 ← Level 2
114
+ ```
115
+
116
+ **Object'lere `main_menu_id` + `sub_menu_id` ata (Level 3):**
117
+ - Her object'i uygun alt menüye bağla
118
+
119
+ **3. seviye menü öğelerini `cms_admin_menus`'e ekleME** — object üzerinden `main_menu_id` + `sub_menu_id` ile yapılır.
120
+
121
+ ### Faz 7: Örnek Veri
122
+
123
+ Test için örnek kayıtlar ekle (gereksinime göre).
124
+
125
+ ---
126
+
127
+ ## Sık Yapılan Hatalar (Bunları YAPMA)
128
+
129
+ | Hata | Doğrusu |
130
+ |------|---------|
131
+ | `status: 'draft'` manual set etmek | State machine `state` alanını otomatik yönetir |
132
+ | `record.status` kontrol etmek | `record.state` kullan (state machine system_name'leri) |
133
+ | `csrf_token()` Twig'de kullanmak | JS'de `$('meta[name="csrf-token"]').attr('content')` |
134
+ | `app.user.username` kullanmak | `currentUser('sicil_no')` |
135
+ | `id` parametresi kullanmak | `form_id` kullan (`id` reserved) |
136
+ | `alert()` / `confirm()` kullanmak | `rg_alert()` / `rg_confirm()` |
137
+ | `bfy_state_machine_role_id` (tekil) | `bfy_state_machine_role_ids` (çoğul) |
138
+ | Transition name alanı | `button_title` alanı kullan |
139
+ | Custom format tarih datetime type | string type kullan (custom format dd.mm.YYYY HH:mm) |
140
+ | `|replace('old', 'new')` string ile | `|replace({'old': 'new'})` mapping ile |
141
+ | Child satırları Twig for loop ile render | JS'den render et (window._data JSON) |
142
+ | Edit'te child'ları tekrar add etmek | Önce mevcut child'ları tek tek sil, sonra yeniden ekle |
143
+ | `delete_where` endpoint kullanmak | Yok. Tek tek `__delete` ile sil (sequential promise chain) |
144
+ | `butterfly-cli download -t cms_reports` | `-t reports` kullan |
145
+ | 3. seviye menüyü cms_admin_menus'e eklemek | Object üzerinden main_menu_id + sub_menu_id ile yap |
146
+ | jQuery UI'ı head'e koymak | jQuery yüklendikten sonra body sonuna koy |
147
+ | PDF template'te `db().table()` kullanmak | `db('default', false).table()` kullan (permission bypass) |
148
+ | Dosyayı doğrudan `cms_object/operation` ile yüklemek | Önce `file_helper/upload_file` ile yükle, sonra filename'i kaydet |
149
+ | `file_upload` field input name'ini column adı yapmak | Input name mutlaka `file_upload` olmalı |
150
+ | REST API kullanarak admin içi dosya yüklemek | Admin panel endpoint (`/admin/ajax/file_helper/upload_file`) kullan |
151
+ | Fonksiyonları `$(document).ready()` içinde tanımlamak | Global scope'ta tanımla, sadece init kodu ready içinde olsun |
152
+ | `employees` tablosu varsaymak | `currentUser('email')`, `currentUser('name')` kullan |
153
+
154
+ ---
155
+
156
+ ## Dosya Yükleme Entegrasyonu
157
+
158
+ Custom report sayfalarında dosya yükleme **iki aşamalı** yapılır:
159
+
160
+ ### Adım 1: Dosyayı Yükle
161
+
162
+ ```javascript
163
+ function uploadFile(file) {
164
+ return new Promise(function(resolve, reject) {
165
+ var fd = new FormData();
166
+ fd.append('file_upload', file); // input name mutlaka 'file_upload'
167
+ fd.append('uuid', 'upload-' + Date.now());
168
+ fd.append('original_filename', file.name);
169
+ fd.append('total_file_size', file.size);
170
+
171
+ $.ajax({
172
+ url: '/admin/ajax/file_helper/upload_file?alias=import', // alias URL param olarak
173
+ type: 'POST',
174
+ data: fd,
175
+ processData: false,
176
+ contentType: false,
177
+ headers: { 'X-Requested-With': 'XMLHttpRequest' },
178
+ success: function(response) {
179
+ if (response.success && response.filename) {
180
+ resolve(response.filename);
181
+ } else {
182
+ rg_alert(response.message || 'Dosya yüklenemedi.');
183
+ reject('Upload failed');
184
+ }
185
+ },
186
+ error: function() { reject('Upload error'); }
187
+ });
188
+ });
189
+ }
190
+ ```
191
+
192
+ ### Adım 2: Kaydı filename ile oluştur
193
+
194
+ ```javascript
195
+ var filename = await uploadFile(fileInput.files[0]);
196
+ $.ajax({
197
+ url: '/admin/ajax/cms_object/operation?do=tablo__add',
198
+ type: 'POST',
199
+ data: { csrf_token: csrfToken, parent_id: parentId, dosya: filename }
200
+ });
201
+ ```
202
+
203
+ ### Storage Alias Ayarları
204
+
205
+ - Alias'lar `cms_file_uploads` tablosunda tanımlıdır
206
+ - `extensions` alanı izin verilen uzantıları belirler (virgülle ayrılmış)
207
+ - Object spec'teki `file_upload` field'ın `val_1` parametresi alias adını belirtir
208
+ - Yeni uzantı eklemek: `butterfly-cli record edit cms_file_uploads --id <id> --data '{"extensions":"mevcut,yeni"}'`
209
+
210
+ Detaylı dokümantasyon: [docs/butterfly_upload_api_docs-2.md](butterfly_upload_api_docs-2.md)
211
+
212
+ ---
213
+
214
+ ## JS Scope ve Mimari Kuralları
215
+
216
+ ### Fonksiyon Tanımlama
217
+
218
+ Custom report sayfalarında **tüm fonksiyonlar global scope'ta** tanımlanmalı. `$(document).ready()` içinde sadece init kodu olmalı:
219
+
220
+ ```javascript
221
+ // ✅ DOĞRU: Global scope'ta
222
+ function calculateTotal() { /* ... */ }
223
+ function addRow() { /* ... */ }
224
+ function saveForm() { /* ... */ }
225
+
226
+ $(document).ready(function() {
227
+ // Sadece init: değişken atamaları, ilk render, event listener'lar
228
+ var csrfToken = $('meta[name="csrf-token"]').attr('content');
229
+ renderExistingRows();
230
+ });
231
+ ```
232
+
233
+ ```javascript
234
+ // ❌ YANLIŞ: ready() içinde tanımlama
235
+ $(document).ready(function() {
236
+ function calculateTotal() { /* ... */ } // onclick'ten erişilemez!
237
+ window.calculateTotal = calculateTotal; // race condition riski
238
+ });
239
+ ```
240
+
241
+ **Neden?** HTML `onclick` attributeleri global scope'taki fonksiyonları çağırır. `$(document).ready()` içindeki fonksiyonlar closure'da kalır ve erişilemez. `window.XXX =` ataması ise timing sorunlarına yol açabilir (fonksiyon henüz atanmadan çağrılabilir).
242
+
243
+ ### Tailwind CSS Select Reset Sorunu
244
+
245
+ Tailwind CSS `select` elementlerinin görünümünü sıfırlar. Custom select'ler için:
246
+
247
+ ```css
248
+ .form-select, .data-table select {
249
+ appearance: auto;
250
+ background-color: #fff;
251
+ }
252
+ ```
@@ -0,0 +1,255 @@
1
+ # Butterfly Dosya Yükleme Dokümantasyonu
2
+
3
+ ## Genel Bakış
4
+
5
+ Butterfly DXP'de dosya yükleme iki yöntemle yapılabilir:
6
+
7
+ 1. **Admin Panel Endpoint (Önerilen)** — Session-based auth, custom report sayfalarında ve admin panelinde kullanılır
8
+ 2. **REST API Endpoint** — Token-based auth, harici sistemler ve mobil uygulamalar için
9
+
10
+ ---
11
+
12
+ ## Yöntem 1: Admin Panel File Helper (Önerilen)
13
+
14
+ Custom report sayfaları veya admin panelindeki JavaScript kodlarından dosya yüklemek için bu yöntem kullanılır. Kullanıcı zaten admin paneline giriş yaptığı için ek authentication gerekmez.
15
+
16
+ ### Endpoint
17
+
18
+ ```
19
+ POST /admin/ajax/file_helper/upload_file?alias=ALIAS
20
+ ```
21
+
22
+ ### Parametreler
23
+
24
+ | Parametre | Tip | Zorunlu | Açıklama |
25
+ |-----------|-----|---------|----------|
26
+ | `file_upload` | File | Evet | Yüklenecek dosya (input name **mutlaka** `file_upload` olmalı) |
27
+ | `uuid` | String | Evet | Benzersiz tanımlayıcı (örn: `upload-` + timestamp) |
28
+ | `original_filename` | String | Evet | Orijinal dosya adı |
29
+ | `total_file_size` | Number | Evet | Dosya boyutu (bytes) |
30
+ | `alias` | String | Evet | Storage alias (URL query parameter olarak gönderilir) |
31
+
32
+ ### Storage Alias Yapılandırması
33
+
34
+ Alias'lar `cms_file_uploads` tablosunda tanımlıdır. Her alias şunları kontrol eder:
35
+ - İzin verilen dosya uzantıları (`extensions` alanı, virgülle ayrılmış)
36
+ - Maksimum dosya boyutu
37
+ - Depolama konumu
38
+
39
+ **Alias yönetimi:** Admin panel → File Storage menüsü veya `cms_file_uploads` tablosu üzerinden.
40
+
41
+ **Yaygın alias'lar:**
42
+ - `content` — Genel içerik (görsel, medya)
43
+ - `import` — İçe aktarma dosyaları (varsayılan: json, xlsx, xls; ihtiyaca göre genişletilebilir)
44
+
45
+ **Yeni uzantı eklemek için:**
46
+ ```bash
47
+ butterfly-cli record edit cms_file_uploads --id <alias_id> --data '{"extensions": "mevcut,yeni,uzantilar"}'
48
+ ```
49
+
50
+ ### JavaScript Örneği (Custom Report Sayfası)
51
+
52
+ ```javascript
53
+ function uploadFile(file) {
54
+ return new Promise(function(resolve, reject) {
55
+ var fd = new FormData();
56
+ fd.append('file_upload', file);
57
+ fd.append('uuid', 'upload-' + Date.now());
58
+ fd.append('original_filename', file.name);
59
+ fd.append('total_file_size', file.size);
60
+
61
+ $.ajax({
62
+ url: '/admin/ajax/file_helper/upload_file?alias=import',
63
+ type: 'POST',
64
+ data: fd,
65
+ processData: false,
66
+ contentType: false,
67
+ headers: { 'X-Requested-With': 'XMLHttpRequest' },
68
+ success: function(response) {
69
+ if (response.success && response.filename) {
70
+ resolve(response.filename);
71
+ } else {
72
+ rg_alert(response.message || 'Dosya yüklenemedi.');
73
+ reject('Upload failed');
74
+ }
75
+ },
76
+ error: function() {
77
+ rg_alert('Dosya yükleme sırasında hata oluştu.');
78
+ reject('Upload error');
79
+ }
80
+ });
81
+ });
82
+ }
83
+ ```
84
+
85
+ ### Yanıt Formatı
86
+
87
+ **Başarılı:**
88
+ ```json
89
+ {
90
+ "success": true,
91
+ "filename": "dosya_adi_islenmis.jpg",
92
+ "full_path": "/storage/path/dosya_adi_islenmis.jpg"
93
+ }
94
+ ```
95
+
96
+ **Hata:**
97
+ ```json
98
+ {
99
+ "success": false,
100
+ "message": "Dosya tipi izin verilmiyor."
101
+ }
102
+ ```
103
+
104
+ ### Dosya Kaydı ile Entegrasyon (İki Aşamalı)
105
+
106
+ Dosya yükleme ve kayıt oluşturma iki ayrı adımda yapılır:
107
+
108
+ ```javascript
109
+ // 1. Dosyayı yükle → filename al
110
+ var filename = await uploadFile(file);
111
+
112
+ // 2. Kaydı oluştur, filename'i veri olarak gönder
113
+ $.ajax({
114
+ url: '/admin/ajax/cms_object/operation?do=tablo_adi__add',
115
+ type: 'POST',
116
+ data: {
117
+ csrf_token: csrfToken,
118
+ parent_id: parentId,
119
+ dosya: filename, // file_upload field'ın column adı
120
+ aciklama: 'Açıklama'
121
+ }
122
+ });
123
+ ```
124
+
125
+ ### Object Spec Yapılandırması
126
+
127
+ `file_upload` tipindeki field'ın `val_1` parametresi alias adını belirtir:
128
+
129
+ ```json
130
+ {
131
+ "type": "file_upload",
132
+ "val_1": "import"
133
+ }
134
+ ```
135
+
136
+ ### Dosya Önizleme Linki Oluşturma
137
+
138
+ Upload edilen dosyalar veritabanında sadece **relative path** olarak saklanır (örn: `26-03/08/dosya.png`). Tam URL oluşturmak için alias'ın `path` değeri gerekir.
139
+
140
+ **Twig'de base path'i al:**
141
+ ```twig
142
+ {% set importAlias = db().table('cms_file_uploads').where('alias', 'import').first() %}
143
+ {% set importBasePath = importAlias ? importAlias.path : '/static/import/' %}
144
+ ```
145
+
146
+ **JS'e aktar:**
147
+ ```html
148
+ <script>
149
+ window._data = {
150
+ importBasePath: '{{ importBasePath }}'
151
+ };
152
+ </script>
153
+ ```
154
+
155
+ **JS'de tam URL oluştur:**
156
+ ```javascript
157
+ var basePath = data.importBasePath || '/static/import/';
158
+ var fullUrl = basePath + record.dosya;
159
+ // Örnek: /static/import/26-03/08/dosya.png
160
+ ```
161
+
162
+ **Path yapısı:**
163
+ - `cms_file_storages.base_url` → Storage base URL (örn: `/static`)
164
+ - `cms_file_uploads.path` → Alias path (örn: `/static/import/`)
165
+ - Veritabanındaki `dosya` değeri → Relative path (örn: `26-03/08/dosya.png`)
166
+ - **Tam URL** = `cms_file_uploads.path` + `dosya` değeri
167
+
168
+ ### Önemli Notlar
169
+
170
+ - **Input name:** Mutlaka `file_upload` olmalı, field'ın column adı (örn: `dosya`) DEĞİL
171
+ - **Authentication:** Admin session cookie'leri otomatik gönderilir, ek header gerekmez
172
+ - **Alias zorunlu:** URL'de `?alias=ALIAS` belirtilmeli, yoksa hata verir
173
+ - **Tek dosya:** Bu endpoint tek seferde bir dosya yükler, birden fazla dosya için döngü kullanın
174
+ - **Dosya uzantısı kontrol:** Alias'ta tanımlı uzantılarla eşleşmeyen dosyalar reddedilir
175
+ - **Dosya linki:** Veritabanında sadece relative path saklanır, görüntülemek için alias path'i ile birleştirin
176
+
177
+ ---
178
+
179
+ ## Yöntem 2: REST API (Harici Sistemler İçin)
180
+
181
+ Harici uygulamalar, mobil uygulamalar veya otomatik süreçler için REST API kullanılır.
182
+
183
+ ### Ön Gereksinimler
184
+
185
+ 1. **API Erişimi:** Admin panelinde kullanıcı profilinde "API Access" aktif edilmeli
186
+ 2. **API Key:** `/admin/user_auth/list` sayfasından API key oluşturulmalı
187
+
188
+ ### Authentication
189
+
190
+ ```bash
191
+ # Token oluştur
192
+ curl -X POST /rest/auth/generateToken \
193
+ -H "Content-Type: application/x-www-form-urlencoded" \
194
+ -d "api_key=YOUR_API_KEY&password=YOUR_PASSWORD"
195
+ ```
196
+
197
+ **Yanıt:**
198
+ ```json
199
+ {
200
+ "success": true,
201
+ "token": "session_token",
202
+ "expires_at": "2024-12-31 23:59:59"
203
+ }
204
+ ```
205
+
206
+ ### Dosya Yükleme
207
+
208
+ ```bash
209
+ curl -X POST /rest/upload/image \
210
+ -H "BUTTERFLY_API_KEY: api-key-here" \
211
+ -H "BUTTERFLY_ACCESS_TOKEN: access-token-here" \
212
+ -F "files[]=@/path/to/image.jpg" \
213
+ -F "alias=content" \
214
+ -F "sub_folder=gallery"
215
+ ```
216
+
217
+ **Parametreler:**
218
+ - `files[]` (file array, zorunlu): Yüklenecek dosyalar
219
+ - `alias` (string, opsiyonel): Storage alias (varsayılan: 'content')
220
+ - `sub_folder` (string, opsiyonel): Alt klasör
221
+
222
+ **Yanıt:**
223
+ ```json
224
+ {
225
+ "success": true,
226
+ "result": [
227
+ {
228
+ "full_path": "https://cdn.example.com/content/gallery/image.jpg",
229
+ "filename": "image.jpg",
230
+ "webp": "https://cdn.example.com/content/gallery/image.webp"
231
+ }
232
+ ]
233
+ }
234
+ ```
235
+
236
+ ### REST API Hata Durumları
237
+
238
+ | Hata | Açıklama |
239
+ |------|----------|
240
+ | `"invalid request"` | api_key veya password eksik |
241
+ | `"invalid parameters"` | API key bulunamadı, API erişimi kapalı, yanlış şifre |
242
+ | 401 Unauthorized | Geçersiz veya süresi dolmuş token |
243
+ | 422 Unprocessable | Dosya tipi veya boyut sınırı aşıldı |
244
+
245
+ ---
246
+
247
+ ## Hangi Yöntemi Kullanmalı?
248
+
249
+ | Senaryo | Yöntem |
250
+ |---------|--------|
251
+ | Custom report sayfasında dosya yükleme | Admin Panel (Yöntem 1) |
252
+ | Object edit sayfasında file_upload field | Otomatik (Butterfly yönetir) |
253
+ | Harici uygulama / mobil uygulama | REST API (Yöntem 2) |
254
+ | Cron job / workflow / otomasyon | REST API (Yöntem 2) |
255
+ | Admin paneli içi JavaScript | Admin Panel (Yöntem 1) |
@@ -0,0 +1,78 @@
1
+ # Object Listing Query
2
+
3
+ Each object can have a `listing_query.bfy` file that customizes the listing page behavior and appearance.
4
+
5
+ ## File Location
6
+
7
+ ```
8
+ butterfly-resources/objects/[butterfly|app]/[table_name]/listing_query.bfy
9
+ ```
10
+
11
+ ## Header Block
12
+
13
+ Use the `{% block header %}` to display custom content at the top of the listing page, above the data table.
14
+
15
+ ### Basic Usage
16
+
17
+ ```twig
18
+ {% block header %}
19
+ <div class="alert alert-info">
20
+ Custom content displayed at the top of the listing page
21
+ </div>
22
+ {% endblock %}
23
+ ```
24
+
25
+ ### Example with Statistics
26
+
27
+ ```twig
28
+ {% block header %}
29
+ {% set stats = db().table('orders').selectRaw('COUNT(*) as total, SUM(amount) as sum').first() %}
30
+ <div class="row mb-3">
31
+ <div class="col-md-4">
32
+ <div class="card">
33
+ <div class="card-body text-center">
34
+ <h5 class="card-title">Total Orders</h5>
35
+ <p class="display-6">{{ stats.total }}</p>
36
+ </div>
37
+ </div>
38
+ </div>
39
+ <div class="col-md-4">
40
+ <div class="card">
41
+ <div class="card-body text-center">
42
+ <h5 class="card-title">Total Amount</h5>
43
+ <p class="display-6">{{ stats.sum|number_format(2) }} TL</p>
44
+ </div>
45
+ </div>
46
+ </div>
47
+ </div>
48
+ {% endblock %}
49
+ ```
50
+
51
+ ### Example with Filters
52
+
53
+ ```twig
54
+ {% block header %}
55
+ <div class="card mb-3">
56
+ <div class="card-body">
57
+ <form method="get" class="row g-3">
58
+ <div class="col-md-3">
59
+ <label class="form-label">Status</label>
60
+ <select name="status" class="form-select">
61
+ <option value="">All</option>
62
+ <option value="active">Active</option>
63
+ <option value="inactive">Inactive</option>
64
+ </select>
65
+ </div>
66
+ <div class="col-md-3 d-flex align-items-end">
67
+ <button type="submit" class="btn btn-primary">Filter</button>
68
+ </div>
69
+ </form>
70
+ </div>
71
+ </div>
72
+ {% endblock %}
73
+ ```
74
+
75
+ ## Related
76
+
77
+ - [Objects Overview](./README.md)
78
+ - [Creating Objects](./creating.md)
@@ -0,0 +1,376 @@
1
+ # Butterfly PDF Oluşturma Dokümantasyonu
2
+
3
+ ## Genel Bakış
4
+
5
+ Butterfly DXP, `pdf_templates` tablosundaki Twig şablonlarından PDF dosyaları oluşturmak için yerleşik bir PDF sistemi sunar. Sistem arka planda `https://pdf.butterfly.dev/get` adresindeki PDF sunucusunu kullanır.
6
+
7
+ ## Mimari
8
+
9
+ ```
10
+ [İstek] → /pdf/create veya /pdf/view
11
+ → pdf_templates tablosundan şablon çek (system_name ile)
12
+ → Twig render (Input parametreleri değişken olarak geçer)
13
+ → HTML'i PDF sunucusuna gönder
14
+ → /create: PDF'i storage'a kaydet, dosya bilgisi dön
15
+ → /view: PDF'i tarayıcıda inline göster
16
+ ```
17
+
18
+ ## Endpoint'ler
19
+
20
+ ### 1. PDF Oluştur ve Kaydet (`/pdf/create`)
21
+
22
+ PDF oluşturur, storage alias'a kaydeder, dosya bilgisi döner.
23
+
24
+ **URL:** `POST /pdf/create`
25
+
26
+ **Parametreler:**
27
+
28
+ | Parametre | Tip | Zorunlu | Açıklama |
29
+ |-----------|-----|---------|----------|
30
+ | `template_name` | string | Evet | `pdf_templates.system_name` değeri |
31
+ | `filename` | string | Hayır | Çıktı dosya adı (varsayılan: `document.pdf`) |
32
+ | `alias` | string | Hayır | Storage alias (varsayılan: `pdf`) |
33
+ | *diğer parametreler* | any | Hayır | Template'e değişken olarak geçer |
34
+
35
+ **Başarılı Yanıt:**
36
+ ```json
37
+ {
38
+ "success": true,
39
+ "full_path": "https://butterfly.rg/static/pdf/26-03/08/harcama-3.pdf",
40
+ "filename": "26-03/08/harcama-3.pdf"
41
+ }
42
+ ```
43
+
44
+ **Hata Yanıtları:**
45
+ ```json
46
+ {"success": false, "message": "PDF tasarımı seçiniz."}
47
+ {"success": false, "message": "PDF tasarımı bulunamadı."}
48
+ {"success": false, "message": "Hata: ...twig render hatası..."}
49
+ {"success": false, "message": "PDF oluşturulurken bir hata oluştu..."}
50
+ ```
51
+
52
+ ### 2. PDF Önizleme (`/pdf/view`)
53
+
54
+ PDF oluşturur ve tarayıcıda inline gösterir (kaydetmez). Yazdır/önizleme için idealdir.
55
+
56
+ **URL:** `GET /pdf/view`
57
+
58
+ **Parametreler:** `/pdf/create` ile aynı. Ek olarak template'e `preview: true` değişkeni geçer.
59
+
60
+ **Yanıt:** `Content-Type: application/pdf` ile doğrudan PDF binary.
61
+
62
+ ## pdf_templates Tablosu
63
+
64
+ | Kolon | Açıklama |
65
+ |-------|----------|
66
+ | `system_name` | Şablonun benzersiz tanımlayıcısı (endpoint'ten `template_name` ile eşleşir) |
67
+ | `name` | Şablon görünen adı |
68
+ | `template` | Twig + HTML şablon içeriği |
69
+
70
+ ## Storage Alias Yapılandırması
71
+
72
+ PDF dosyaları `cms_file_uploads` tablosundaki alias ayarına göre kaydedilir.
73
+
74
+ **`pdf` alias yoksa oluştur:**
75
+ ```bash
76
+ butterfly-cli record add cms_file_uploads --data '{
77
+ "alias": "pdf",
78
+ "path": "/static/pdf/",
79
+ "extensions": "pdf",
80
+ "cms_file_storage_id": 1
81
+ }'
82
+ ```
83
+
84
+ Kayıtlı PDF'lerin tam URL'i: `cms_file_uploads.path` + `response.filename`
85
+ Örnek: `/static/pdf/` + `26-03/08/harcama-3.pdf` = `/static/pdf/26-03/08/harcama-3.pdf`
86
+
87
+ ## Şablon Yazma Kuralları
88
+
89
+ ### Permission Bypass (KRİTİK)
90
+
91
+ `/pdf/create` ve `/pdf/view` endpoint'leri admin context dışında çalışır. Standart `db().table()` çağrıları permission hatası verir:
92
+
93
+ ```
94
+ "You don't have permission to view TABLE_NAME object"
95
+ ```
96
+
97
+ **Çözüm:** `db('default', false)` kullan — ikinci parametre `false` permission kontrolünü devre dışı bırakır.
98
+
99
+ ```twig
100
+ {# ❌ YANLIŞ — permission hatası verir #}
101
+ {% set r = db().table('tablo').where('id', id).first() %}
102
+
103
+ {# ✅ DOĞRU — permission bypass #}
104
+ {% set r = db('default', false).table('tablo').where('id', id).first() %}
105
+ ```
106
+
107
+ > **Not:** Bu sadece PDF template'lerinde kullanılmalıdır. Normal report/object kodlarında standart `db()` kullanmaya devam edin.
108
+
109
+ ### Değişken Erişimi
110
+
111
+ Template'e gönderilen tüm parametreler doğrudan Twig değişkeni olarak erişilebilir:
112
+
113
+ ```
114
+ POST /pdf/create?template_name=sablonAdi&id=5&extra_param=deger
115
+ ```
116
+
117
+ Template içinde:
118
+ ```twig
119
+ {{ id }} {# 5 #}
120
+ {{ extra_param }} {# deger #}
121
+ ```
122
+
123
+ ### Inline CSS Zorunluluğu
124
+
125
+ PDF sunucusu harici CSS dosyalarını yüklemez. **Tüm stiller `<style>` etiketi içinde veya inline olmalıdır.**
126
+
127
+ ```html
128
+ {# ❌ YANLIŞ — PDF'te çalışmaz #}
129
+ <link href="https://cdn.jsdelivr.net/npm/tailwindcss..." rel="stylesheet">
130
+
131
+ {# ✅ DOĞRU #}
132
+ <style>
133
+ body { font-family: Arial, sans-serif; font-size: 12px; }
134
+ .section { background: #f3f4f6; padding: 6px 12px; font-weight: 700; }
135
+ table { width: 100%; border-collapse: collapse; }
136
+ </style>
137
+ ```
138
+
139
+ ### Temel Şablon Yapısı
140
+
141
+ ```html
142
+ <html>
143
+ <head>
144
+ <meta charset="utf-8">
145
+ <style>
146
+ body { font-family: Arial, sans-serif; font-size: 12px; color: #333; margin: 20px 40px; }
147
+ h1 { font-size: 18px; text-align: center; }
148
+ .section { background: #f3f4f6; padding: 6px 12px; font-weight: 700; font-size: 13px;
149
+ border-left: 4px solid #ef4444; margin: 14px 0 8px; }
150
+ table { width: 100%; border-collapse: collapse; margin-bottom: 10px; }
151
+ .info-table td { padding: 4px 8px; font-size: 12px; }
152
+ .info-table .lbl { font-weight: 600; width: 140px; color: #555; }
153
+ .data-table th { background: #f3f4f6; padding: 6px 8px; text-align: left; font-size: 11px;
154
+ font-weight: 700; border: 1px solid #d1d5db; }
155
+ .data-table td { padding: 5px 8px; border: 1px solid #d1d5db; font-size: 11px; }
156
+ .footer { margin-top: 20px; border-top: 1px solid #d1d5db; padding-top: 8px;
157
+ font-size: 10px; color: #999; text-align: center; }
158
+ </style>
159
+ </head>
160
+ <body>
161
+ {% set r = db('default', false).table('ana_tablo').where('id', id).first() %}
162
+ {% if r is empty %}
163
+ <p>Kayıt bulunamadı.</p>
164
+ {% else %}
165
+ {% set children = db('default', false).table('child_tablo').where('parent_id', id).get() %}
166
+
167
+ <h1>BAŞLIK</h1>
168
+
169
+ <div class="section">Bilgiler</div>
170
+ <table class="info-table">
171
+ <tr><td class="lbl">Alan Adı</td><td>{{ r.alan_adi }}</td></tr>
172
+ </table>
173
+
174
+ {% if children|length > 0 %}
175
+ <div class="section">Alt Kayıtlar</div>
176
+ <table class="data-table">
177
+ <tr><th>Kolon 1</th><th>Kolon 2</th></tr>
178
+ {% for c in children %}
179
+ <tr><td>{{ c.kolon1 }}</td><td>{{ c.kolon2 }}</td></tr>
180
+ {% endfor %}
181
+ </table>
182
+ {% endif %}
183
+
184
+ <div class="footer">Bu belge sistem tarafından otomatik oluşturulmuştur.</div>
185
+ {% endif %}
186
+ </body>
187
+ </html>
188
+ ```
189
+
190
+ ## Entegrasyon Örnekleri
191
+
192
+ ### Custom Report Sayfasından Yazdır Butonu
193
+
194
+ **HTML (query_code.bfy):**
195
+ ```html
196
+ {% if isEdit %}
197
+ <button onclick="printPdf()">
198
+ <i class="fas fa-print mr-1"></i> Yazdır
199
+ </button>
200
+ {% endif %}
201
+ ```
202
+
203
+ **JS (script.js):**
204
+ ```javascript
205
+ function printPdf() {
206
+ if (!data.isEdit || !data.recordId) {
207
+ rg_alert('Lütfen önce formu kaydediniz.');
208
+ return;
209
+ }
210
+ window.open('/pdf/view?template_name=SABLON_ADI&id=' + data.recordId
211
+ + '&filename=dosya-' + data.recordId + '.pdf', '_blank');
212
+ }
213
+ ```
214
+
215
+ ### PDF Oluştur ve Kayda Yaz
216
+
217
+ Formu göndermeden önce PDF oluşturup dosya yolunu ana kayda yazmak için:
218
+
219
+ ```javascript
220
+ function createPdf(recordId) {
221
+ return new Promise(function(resolve, reject) {
222
+ $.ajax({
223
+ url: '/pdf/create',
224
+ type: 'POST',
225
+ data: {
226
+ template_name: 'SABLON_ADI',
227
+ id: recordId,
228
+ filename: 'dosya-' + recordId + '.pdf',
229
+ alias: 'pdf'
230
+ },
231
+ headers: { 'X-Requested-With': 'XMLHttpRequest' },
232
+ success: function(response) {
233
+ if (response.success && response.filename) {
234
+ var pdfPath = response.full_path || '/static/pdf/' + response.filename;
235
+ // PDF yolunu ana kayda yaz
236
+ $.ajax({
237
+ url: '/admin/ajax/cms_object/operation?do=TABLO__edit',
238
+ type: 'POST',
239
+ data: { csrf_token: csrfToken, id: recordId, pdf_dosya: pdfPath },
240
+ headers: { 'X-Requested-With': 'XMLHttpRequest' },
241
+ success: function() { resolve(pdfPath); },
242
+ error: function() { resolve(pdfPath); }
243
+ });
244
+ } else {
245
+ rg_alert(response.message || 'PDF oluşturulamadı.');
246
+ reject('PDF failed');
247
+ }
248
+ },
249
+ error: function() {
250
+ rg_alert('PDF oluşturma sırasında hata oluştu.');
251
+ reject('PDF error');
252
+ }
253
+ });
254
+ });
255
+ }
256
+ ```
257
+
258
+ ### State Machine Transition ile PDF Link'ini E-postaya Ekle
259
+
260
+ PDF dosyası JS tarafında oluşturulup `pdf_dosya` field'ına yazıldıktan sonra, transition action_code'unda bu değer okunabilir:
261
+
262
+ ```twig
263
+ {# action_code.bfy #}
264
+ {% set tpl = db().table('cms_email_templates').where('alias', 'template-alias').first() %}
265
+ {% set pdfDosya = getValue('pdf_dosya') %}
266
+ {% set vars = {
267
+ 'talep_no': getValue('talep_no'),
268
+ 'ad_soyad': getValue('ad_soyad'),
269
+ 'pdf_link': pdfDosya
270
+ ? '<p><strong>PDF Doküman:</strong> <a href="' ~ pdfDosya ~ '" style="color:#3b82f6;">Formu İndir</a></p>'
271
+ : ''
272
+ } %}
273
+ {% set replacements = {} %}
274
+ {% for key, val in vars %}
275
+ {% set replacements = replacements|merge({('{{ ' ~ key ~ ' }}'): val}) %}
276
+ {% endfor %}
277
+ {{ sendEmail(recipient, tpl.subject|replace(replacements), tpl.content|replace(replacements)) }}
278
+ ```
279
+
280
+ E-posta template'ine `{{ pdf_link }}` placeholder'ı eklemeyi unutmayın.
281
+
282
+ ### Tipik Akış (Kaydet → PDF → Gönder)
283
+
284
+ ```javascript
285
+ function submitForm() {
286
+ rg_confirm('Göndermek istediğinize emin misiniz?', function() {
287
+ saveForm().then(function(recordId) { // 1. Formu kaydet
288
+ return createPdf(recordId).then(function() { // 2. PDF oluştur + kayda yaz
289
+ return triggerTransition(recordId, transitionId); // 3. Transition tetikle
290
+ });
291
+ }).then(function() {
292
+ window.location.href = '/admin/cms_report/view/DASHBOARD_ID';
293
+ });
294
+ });
295
+ }
296
+ ```
297
+
298
+ ## Sıfırdan PDF Entegrasyonu Adımları
299
+
300
+ ### 1. Storage Alias Oluştur (bir kez)
301
+
302
+ ```bash
303
+ butterfly-cli record add cms_file_uploads --data '{
304
+ "alias": "pdf",
305
+ "path": "/static/pdf/",
306
+ "extensions": "pdf",
307
+ "cms_file_storage_id": 1
308
+ }'
309
+ ```
310
+
311
+ ### 2. pdf_templates Objesi Oluştur (bir kez)
312
+
313
+ Eğer `pdf_templates` objesi yoksa:
314
+
315
+ ```bash
316
+ # Object
317
+ butterfly-cli record add objects --data '{
318
+ "table_name": "pdf_templates",
319
+ "name": "PDF Şablonları",
320
+ "has_item": 0,
321
+ "auto_increment_column_name": "id"
322
+ }'
323
+ # Object ID'yi not al, aşağıda OBJECT_ID olarak kullan
324
+
325
+ # Field'lar
326
+ butterfly-cli record add object_specs --data '{"object_id":OBJECT_ID,"name":"System Name","column_name":"system_name","type":"string","required":1,"list_column":1,"edit_order_no":1}'
327
+ butterfly-cli record add object_specs --data '{"object_id":OBJECT_ID,"name":"Şablon Adı","column_name":"name","type":"string","required":1,"list_column":1,"edit_order_no":2}'
328
+ butterfly-cli record add object_specs --data '{"object_id":OBJECT_ID,"name":"HTML Şablon","column_name":"template","type":"code","edit_order_no":3}'
329
+ ```
330
+
331
+ ### 3. Şablon Kaydı Ekle
332
+
333
+ ```bash
334
+ butterfly-cli record add pdf_templates --data '{
335
+ "system_name": "sablonun-adi",
336
+ "name": "Şablonun Görünen Adı",
337
+ "template": "<html>...şablon HTML...</html>"
338
+ }'
339
+ ```
340
+
341
+ ### 4. Ana Tabloya `pdf_dosya` Field Ekle (opsiyonel)
342
+
343
+ PDF dosya yolunu kayıtta saklamak istiyorsan:
344
+
345
+ ```bash
346
+ butterfly-cli record add object_specs --data '{
347
+ "object_id": ANA_OBJECT_ID,
348
+ "name": "PDF Dosya",
349
+ "column_name": "pdf_dosya",
350
+ "type": "string",
351
+ "edit_order_no": 99
352
+ }'
353
+ ```
354
+
355
+ ### 5. JS Fonksiyonlarını Ekle
356
+
357
+ `printPdf()` ve `createPdf()` fonksiyonlarını script.js'e ekle (yukarıdaki örneklere bakın).
358
+
359
+ ### 6. Yazdır Butonunu Forma Ekle
360
+
361
+ ```html
362
+ {% if isEdit %}
363
+ <button onclick="printPdf()"><i class="fas fa-print mr-1"></i> Yazdır</button>
364
+ {% endif %}
365
+ ```
366
+
367
+ ## Sık Karşılaşılan Hatalar
368
+
369
+ | Hata | Neden | Çözüm |
370
+ |------|-------|-------|
371
+ | `"PDF tasarımı bulunamadı."` | `template_name` ile eşleşen `system_name` yok | `pdf_templates` tablosunu kontrol et |
372
+ | `"You don't have permission to view X object"` | Template'te `db()` kullanılmış | `db('default', false)` kullan |
373
+ | PDF oluştu ama değerler boş | Template'e parametre geçilmemiş veya `db()` sorgusu hatalı | `id` parametresinin gönderildiğini ve sorgunun doğru olduğunu kontrol et |
374
+ | PDF sunucu hatası (HTTP != 200) | HTML geçersiz veya PDF sunucusu erişilemiyor | HTML'i `/pdf/view` ile test et, hata mesajını incele |
375
+ | Stiller PDF'te görünmüyor | Harici CSS kullanılmış | Tüm stilleri `<style>` etiketi içine al, inline CSS kullan |
376
+ | `"Sayfa Bulunamadı"` | `/admin/pdf/create` veya `/admin/ajax/pdf/create` kullanılmış | Doğru URL: `/pdf/create` (admin prefix'siz) |
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@rglabs/butterfly",
3
- "version": "2.0.2",
3
+ "version": "2.1.0",
4
4
  "description": "CLI tool to download resources from the Butterfly platform",
5
5
  "type": "module",
6
6
  "main": "dist/index.js",