diff --git a/AGENTS.md b/AGENTS.md
index 41a384ae..52347b9c 100644
--- a/AGENTS.md
+++ b/AGENTS.md
@@ -206,7 +206,7 @@ hand-written HTML file behind at its old address — a canonical link, a
browser does not follow it. `docs/public/configuration/troubleshooting.html`
is the worked example.
-There are seventy-nine of them. Eight are `/technical/*`: that whole section became
+There are eighty-five of them. Eight are `/technical/*`: that whole section became
the Insights series under `/advanced/insights/` in one commit, and eight
addresses that had been public for years stopped existing with it — one of
them, `/technical/concept`, is linked from the framework's README, which is
@@ -242,6 +242,11 @@ decision makers) and the `/technical/*` ones point at the new site directly,
so no old address takes two hops. The essays, their SVGs and their sidebar
group are gone from this repository; the sidebar keeps one row, `Insights`,
that leads there.
+The newest six are `/everyday/*`: the Everyday Work guides started on
+2026-11-02 as a section of their own in the bar and moved under the Tutorial
+the same day (`/tutorials/everyday/`), so the bar keeps its four sections.
+The five guides and the index that had been published for a few hours keep
+their old address, with the `#fragment`.
Two things make this safe rather than a second set of pages to maintain:
diff --git a/docs/.vitepress/config.mjs b/docs/.vitepress/config.mjs
index 0c9c8ed4..4dfe945c 100644
--- a/docs/.vitepress/config.mjs
+++ b/docs/.vitepress/config.mjs
@@ -167,7 +167,6 @@ export default defineConfig({
nav: [
{ text: "Guide", link: "/get_started/about", activeMatch: "^/(get_started|configuration|advanced)/" },
{ text: "Tutorial", link: "/tutorials/", activeMatch: "^/tutorials/" },
- { text: "Everyday Work", link: "/everyday/", activeMatch: "^/everyday/" },
{ text: "Cookbook", link: "/cookbook/", activeMatch: "^/cookbook/" },
{ text: "API", link: "/resources/api", activeMatch: "^/resources/" },
{
@@ -226,23 +225,29 @@ export default defineConfig({
{ text: "10. App Structure", link: "/tutorials/step-10" },
{ text: "11. To Production", link: "/tutorials/step-11" },
{ text: "12. Unit Tests", link: "/tutorials/step-12" },
- ],
- },
- {
- // Between the Tutorial and the Cookbook: a tutorial teaches one
- // concept per step, a cookbook chapter answers one question - a guide
- // here is a whole job from a real project, built from several
- // chapters and backed by an installable sample. The row opens the
- // index, which says what the section is and lists the guides.
- text: "Everyday Work",
- link: "/everyday/",
- collapsed: true,
- items: [
- { text: "Excel Import into RAP", link: "/everyday/excel_import_rap" },
- { text: "Mass Change", link: "/everyday/mass_change" },
- { text: "Approval Worklist", link: "/everyday/approval_worklist" },
- { text: "Report and Export", link: "/everyday/report_export" },
- { text: "Concurrent Editing", link: "/everyday/concurrent_editing" },
+ {
+ // The second half of the section: the steps teach one concept
+ // each, a guide here is a whole job from a real project, built
+ // from several cookbook chapters and backed by an installable
+ // sample. A group of its own under the Tutorial rather than a
+ // section in the bar - it is learning by building, like the
+ // steps, and the bar keeps its four sections. The row opens the
+ // index, which says what the guides are and lists them.
+ text: "Everyday Work",
+ link: "/tutorials/everyday/",
+ collapsed: true,
+ items: [
+ { text: "Excel Import into RAP", link: "/tutorials/everyday/excel_import_rap" },
+ { text: "Mass Change", link: "/tutorials/everyday/mass_change" },
+ { text: "Approval Worklist", link: "/tutorials/everyday/approval_worklist" },
+ { text: "Report and Export", link: "/tutorials/everyday/report_export" },
+ { text: "Concurrent Editing", link: "/tutorials/everyday/concurrent_editing" },
+ { text: "File Attachments", link: "/tutorials/everyday/file_attachments" },
+ { text: "Send by Mail", link: "/tutorials/everyday/send_by_mail" },
+ { text: "Print Forms as PDF", link: "/tutorials/everyday/print_forms" },
+ { text: "React to Changes", link: "/tutorials/everyday/react_to_changes" },
+ ],
+ },
],
},
{
diff --git a/docs/cookbook/device_capabilities/spreadsheet.md b/docs/cookbook/device_capabilities/spreadsheet.md
index 4fa70e37..0e52542d 100644
--- a/docs/cookbook/device_capabilities/spreadsheet.md
+++ b/docs/cookbook/device_capabilities/spreadsheet.md
@@ -209,7 +209,7 @@ ENDCLASS.
:::
::: tip **Into a business object**
-To create the uploaded rows in a RAP business object - with a preview, one `MODIFY ENTITIES` for all rows and every refusal shown on its row - see the guide [Excel Import into RAP](/everyday/excel_import_rap).
+To create the uploaded rows in a RAP business object - with a preview, one `MODIFY ENTITIES` for all rows and every refusal shown on its row - see the guide [Excel Import into RAP](/tutorials/everyday/excel_import_rap).
:::
::: tip **ABAP Cloud**
diff --git a/docs/everyday/index.md b/docs/everyday/index.md
deleted file mode 100644
index 82e06357..00000000
--- a/docs/everyday/index.md
+++ /dev/null
@@ -1,35 +0,0 @@
----
-outline: [2, 4]
-description: Guides for the tasks that come up in real projects - each one a whole job from the first click to the saved data, built from the cookbook's pieces and backed by a sample you can install.
----
-# Everyday Work
-
-The [Tutorial](/tutorials/) teaches the framework one concept at a time, and
-the [Cookbook](/cookbook/) answers one question per chapter: how to bind a
-table, how to open a popup, how to read a file. Real work is rarely one of
-those on its own. An import, a mass change or an approval screen needs half a
-dozen of them, in the right order, against a business object that has its own
-rules.
-
-The guides here take one such job and walk through it from start to finish:
-what the user sees, which roundtrip does what, where the business logic stays,
-and what goes wrong in practice. Each guide links the cookbook chapters it is
-built from and ends with a sample you can pull with abapGit and run.
-
-## Guides
-
-| Guide | The job | Needs |
-|---|---|---|
-| [Excel Import into RAP](/everyday/excel_import_rap) | Upload an `.xlsx` file, check every row, create the rows in a RAP business object and show each refusal on its row | XCO XLSX (ABAP Cloud, or on-premise 2023 and later), a RAP business object |
-| [Mass Change](/everyday/mass_change) | Select many rows, describe the change once, preview old and new values, save all with one `UPDATE` | a RAP business object (1909 and later) |
-| [Approval Worklist](/everyday/approval_worklist) | Show what waits for a decision, accept or reject several items at once through the business object's actions | a RAP business object with actions |
-| [Report and Export](/everyday/report_export) | A selection as range tables, one `SELECT`, a total, and the rows on the screen as an `.xlsx` download | XCO XLSX for the export |
-| [Concurrent Editing](/everyday/concurrent_editing) | Let two users edit the same instance without a lock while typing, and refuse a save over a change the user has not seen | a RAP business object with an ETag field |
-
-## What makes a good guide
-
-A guide belongs here when the job is common, takes more than one cookbook
-chapter, and has a decision in it that a reader would otherwise get wrong the
-first time - all or nothing versus row by row, a draft versus an active
-instance, where a check belongs. Suggestions are welcome in the
-[discussions](https://github.com/abap2UI5/abap2UI5/discussions).
diff --git a/docs/public/everyday/approval_worklist.html b/docs/public/everyday/approval_worklist.html
new file mode 100644
index 00000000..92dc652d
--- /dev/null
+++ b/docs/public/everyday/approval_worklist.html
@@ -0,0 +1,30 @@
+
+
+
+
+
+
diff --git a/docs/resources/controls.md b/docs/resources/controls.md
index fc1b9773..2fc0ea1e 100644
--- a/docs/resources/controls.md
+++ b/docs/resources/controls.md
@@ -121,7 +121,7 @@ Used on [Microsoft Excel](/integration/excel).
Module `z2ui5.cc.FileUploader` ([source](https://github.com/abap2UI5/abap2UI5/blob/main/app/webapp/cc/FileUploader.js)). Extends `sap.ui.core.Control`.
-Used on [Spreadsheet](/cookbook/device_capabilities/spreadsheet), [Upload, Download](/cookbook/device_capabilities/upload_download), [Excel Import into RAP](/everyday/excel_import_rap).
+Used on [Spreadsheet](/cookbook/device_capabilities/spreadsheet), [Upload, Download](/cookbook/device_capabilities/upload_download), [Excel Import into RAP](/tutorials/everyday/excel_import_rap), [File Attachments](/tutorials/everyday/file_attachments).
| Property | Type | Default |
|---|---|---|
diff --git a/docs/everyday/approval_worklist.md b/docs/tutorials/everyday/approval_worklist.md
similarity index 100%
rename from docs/everyday/approval_worklist.md
rename to docs/tutorials/everyday/approval_worklist.md
diff --git a/docs/everyday/concurrent_editing.md b/docs/tutorials/everyday/concurrent_editing.md
similarity index 100%
rename from docs/everyday/concurrent_editing.md
rename to docs/tutorials/everyday/concurrent_editing.md
diff --git a/docs/everyday/excel_import_rap.md b/docs/tutorials/everyday/excel_import_rap.md
similarity index 100%
rename from docs/everyday/excel_import_rap.md
rename to docs/tutorials/everyday/excel_import_rap.md
diff --git a/docs/tutorials/everyday/file_attachments.md b/docs/tutorials/everyday/file_attachments.md
new file mode 100644
index 00000000..cbc8b2d0
--- /dev/null
+++ b/docs/tutorials/everyday/file_attachments.md
@@ -0,0 +1,166 @@
+---
+outline: [2, 4]
+samples:
+ - z2ui5_cl_smps_app_016
+description: Store files in a RAP business object and give them back - upload with the FileUploader into a static action with a deep parameter, a list that never loads the content, download by key and delete through the business object.
+---
+# File Attachments
+
+A contract as PDF, a photo of the damage, the signed delivery note: users
+want to keep a file with the record it belongs to, and get it back later. The
+job has four parts - upload, list, download, delete - and one decision that
+matters more than the others: the business object decides what it stores,
+not the app.
+
+The worked example is `Z2UI5_CL_SMPS_APP_016` in
+[abap2UI5/samples-stack](https://github.com/abap2UI5/samples-stack) (package
+`11`). Its business object `Z2UI5_R_SMPS_FIL` keeps one row per file and
+takes new files only through its static action `uploadFile`.
+
+## The Shape of the App
+
+| Part | How |
+|---|---|
+| **Upload** | `z2ui5:FileUploader` brings the file as a data URL; the app decodes it and calls the action |
+| **List** | A `SELECT` on the CDS view with name, type, size and date - never the content |
+| **Download** | `READ ENTITIES` by key with the content, back to a data URL, `download_b64_file` |
+| **Delete** | `MODIFY ENTITIES ... DELETE`, then `COMMIT ENTITIES` |
+
+## Step 1: Let the Business Object Own the Upload
+
+The behavior definition declares `create` as internal and offers a static
+action instead:
+
+```abap
+static action uploadFile deep parameter z2ui5_a_smps_file_upload result [1] $self;
+```
+
+The action is the only way in, so every file goes through the same checks -
+the behavior pool refuses an empty file with a message - no matter whether
+it comes from this app, a Fiori elements UI or another program. The
+parameter is deep: a description on the first level, and the file one level
+below, as content, MIME type and name.
+
+## Step 2: Upload
+
+The FileUploader delivers `data:;base64,`. Split it, take the
+MIME type from the header - it is the browser's guess from the extension and
+may be missing - and decode the payload:
+
+```abap
+SPLIT file AT `,` INTO DATA(header) DATA(base64).
+DATA(mime_type) = substring_before( val = substring_after( val = header sub = `data:` ) sub = `;` ).
+IF mime_type IS INITIAL.
+ mime_type = `application/octet-stream`.
+ENDIF.
+DATA(content) = cl_web_http_utility=>decode_x_base64( base64 ).
+```
+
+Then call the action. A static action has no key, so the `%cid` is the
+caller's own name for the call:
+
+```abap
+MODIFY ENTITIES OF z2ui5_r_smps_fil
+ ENTITY file
+ EXECUTE uploadfile
+ FROM VALUE #( ( %cid = `UPLOAD_1`
+ %param = VALUE #( description = description
+ _streamproperties = VALUE #( filecontent = content
+ mimetype = mime_type
+ filename = file_name ) ) ) )
+ FAILED DATA(s_failed)
+ REPORTED DATA(s_reported).
+```
+
+`COMMIT ENTITIES RESPONSE OF` saves it, and a refusal from either statement
+goes into a message box. Afterwards clear the attribute that held the data
+URL: the app instance is kept between roundtrips, and the file would travel
+along with every one of them.
+
+## Step 3: List Without the Content
+
+EML reads by key, so the list is a `SELECT` on the CDS view. Leave the
+content column out. The list needs the size of a file, not its bytes, and a
+bound table goes to the browser in full:
+
+```abap
+SELECT FROM z2ui5_r_smps_fil
+ FIELDS FileUuid, FileName, MimeType, FileSize, Description, CreatedAt
+ ORDER BY CreatedAt DESCENDING
+ INTO TABLE @DATA(t_result) UP TO 50 ROWS.
+```
+
+## Step 4: Download by Key
+
+The row's button sends the key with the event,
+`` client->_event( val = `DOWNLOAD` arg = `${FILE_UUID}` ) ``, and
+`client->get_event_arg( )` reads it. The handler reads that one file with its
+content and hands it back the way it came:
+
+```abap
+READ ENTITIES OF z2ui5_r_smps_fil
+ ENTITY file
+ FIELDS ( filename mimetype content )
+ WITH VALUE #( ( fileuuid = file_uuid ) )
+ RESULT DATA(t_result).
+
+DATA(s_file) = t_result[ 1 ].
+client->follow_up_action(
+ val = client->cs_event-download_b64_file
+ t_arg = VALUE #( ( |data:{ s_file-mimetype };base64,{ cl_web_http_utility=>encode_x_base64( s_file-content ) }| )
+ ( |{ s_file-filename }| ) ) ).
+```
+
+Check `t_result` before the table expression: a file deleted in another tab
+is not an exception, the result is simply empty. The frontend refuses data
+URLs of an active type such as HTML or SVG, so a stored file cannot run as a
+page in the user's browser - see
+[Upload, Download](/cookbook/device_capabilities/upload_download#download).
+
+## Step 5: Delete
+
+`MODIFY ENTITIES ... DELETE FROM` by key and a COMMIT. The business object
+decides whether a file may be deleted; the app only shows its answer.
+
+## Limits and Variants
+
+- **File size.** The file travels base64-encoded inside one roundtrip and is
+ held in one work process, decoded and copied into the business object.
+ Request limits of the ICF and the memory of the session end this long
+ before the sample does. Files of tens of megabytes belong in a document
+ store - SAP Document Management, an object store on BTP - with the
+ business object keeping a reference.
+- **Attachments of an existing record.** Here the file is its own business
+ object. To attach files to, say, a travel, model the file as a child
+ entity of the travel and create it by association, or keep the travel's
+ key in the file's table.
+- **Several files at once.** `UploadSet` takes many files and drag and drop -
+ `Z2UI5_CL_SMP_APP_517` in [abap2UI5/samples](https://github.com/abap2UI5/samples)
+ shows it.
+- **Release.** A deep action parameter with `@Semantics.largeObject` needs
+ ABAP Platform 2025 or a current ABAP Cloud release. On older systems, pass
+ the file to an action with a flat parameter, or create the instance with
+ the content field directly.
+
+## Related
+
+- [Upload, Download](/cookbook/device_capabilities/upload_download) - the
+ FileUploader and the download action.
+- [EML](/cookbook/eml_cds_sql/eml) - actions, reads and deletes.
+- [PDF](/cookbook/device_capabilities/pdf) - showing a stored PDF in the view
+ instead of downloading it.
+
+
+
+## Working Samples
+
+Complete apps from the [sample catalog](https://abap2ui5.github.io/samples-site/)
+that use what this page describes. Each is a single class in [abap2UI5/samples](https://github.com/abap2UI5/samples)
+unless its row names another of the three sample repositories — pull that repository with
+[abapGit](https://abapgit.org) and start the class with `?app_start=`.
+
+| Sample | Class |
+|---|---|
+| Deep Action Parameter — EXECUTE uploadFile with a deep parameter - the file travels as a stream property, download and delete included | [`Z2UI5_CL_SMPS_APP_016`](https://github.com/abap2UI5/samples-stack/blob/main/src/11/z2ui5_cl_smps_app_016.clas.abap) · [samples-stack](https://github.com/abap2UI5/samples-stack), branch `11-rap-file-upload` |
+
+
diff --git a/docs/tutorials/everyday/index.md b/docs/tutorials/everyday/index.md
new file mode 100644
index 00000000..f4205d08
--- /dev/null
+++ b/docs/tutorials/everyday/index.md
@@ -0,0 +1,39 @@
+---
+outline: [2, 4]
+description: Guides for the tasks that come up in real projects - each one a whole job from the first click to the saved data, built from the cookbook's pieces and backed by a sample you can install.
+---
+# Everyday Work
+
+The [steps of the Tutorial](/tutorials/) teach the framework one concept at a
+time, and the [Cookbook](/cookbook/) answers one question per chapter: how to
+bind a table, how to open a popup, how to read a file. Real work is rarely one
+of those on its own. An import, a mass change or an approval screen needs half a
+dozen of them, in the right order, against a business object that has its own
+rules.
+
+The guides here take one such job and walk through it from start to finish:
+what the user sees, which roundtrip does what, where the business logic stays,
+and what goes wrong in practice. Each guide links the cookbook chapters it is
+built from and ends with a sample you can pull with abapGit and run.
+
+## Guides
+
+| Guide | The job | Needs |
+|---|---|---|
+| [Excel Import into RAP](/tutorials/everyday/excel_import_rap) | Upload an `.xlsx` file, check every row, create the rows in a RAP business object and show each refusal on its row | XCO XLSX (ABAP Cloud, or on-premise 2023 and later), a RAP business object |
+| [Mass Change](/tutorials/everyday/mass_change) | Select many rows, describe the change once, preview old and new values, save all with one `UPDATE` | a RAP business object (1909 and later) |
+| [Approval Worklist](/tutorials/everyday/approval_worklist) | Show what waits for a decision, accept or reject several items at once through the business object's actions | a RAP business object with actions |
+| [Report and Export](/tutorials/everyday/report_export) | A selection as range tables, one `SELECT`, a total, and the rows on the screen as an `.xlsx` download | XCO XLSX for the export |
+| [Concurrent Editing](/tutorials/everyday/concurrent_editing) | Let two users edit the same instance without a lock while typing, and refuse a save over a change the user has not seen | a RAP business object with an ETag field |
+| [File Attachments](/tutorials/everyday/file_attachments) | Store files in a business object, list them without their content, download and delete by key | a deep action parameter (ABAP Platform 2025 or ABAP Cloud) |
+| [Send by Mail](/tutorials/everyday/send_by_mail) | A mail form next to the data, the attachment written when sending, a clear answer whether the mail left | `cl_bcs_mail_message` and outbound mail configured |
+| [Print Forms as PDF](/tutorials/everyday/print_forms) | Render an existing Adobe Form into a PDF instead of a printer, one file or a ZIP of several | Adobe Document Services (on-premise) |
+| [React to Changes](/tutorials/everyday/react_to_changes) | Log, notify or follow up whenever a business object changes, whoever changed it | RAP business events (ABAP Platform 2021 or ABAP Cloud) |
+
+## What makes a good guide
+
+A guide belongs here when the job is common, takes more than one cookbook
+chapter, and has a decision in it that a reader would otherwise get wrong the
+first time - all or nothing versus row by row, a draft versus an active
+instance, where a check belongs. Suggestions are welcome in the
+[discussions](https://github.com/abap2UI5/abap2UI5/discussions).
diff --git a/docs/everyday/mass_change.md b/docs/tutorials/everyday/mass_change.md
similarity index 97%
rename from docs/everyday/mass_change.md
rename to docs/tutorials/everyday/mass_change.md
index fe51c608..e1f39df2 100644
--- a/docs/everyday/mass_change.md
+++ b/docs/tutorials/everyday/mass_change.md
@@ -136,7 +136,7 @@ deselect the refused rows, or change the description of the change, and press
Preview again.
If the rows are independent and the user would rather save what can be saved,
-use the retry from [Excel Import into RAP](/everyday/excel_import_rap#all-or-nothing-or-row-by-row):
+use the retry from [Excel Import into RAP](/tutorials/everyday/excel_import_rap#all-or-nothing-or-row-by-row):
roll back, drop the refused rows, send the rest again.
## Limits and Variants
@@ -156,7 +156,7 @@ roll back, drop the refused rows, send the rest again.
- [Tables](/cookbook/model/tables) - selection, columns and cells.
- [EML](/cookbook/eml_cds_sql/eml) - `MODIFY ... UPDATE` and the responses.
-- [Excel Import into RAP](/everyday/excel_import_rap) - the same pattern for
+- [Excel Import into RAP](/tutorials/everyday/excel_import_rap) - the same pattern for
rows that come from a file.
diff --git a/docs/tutorials/everyday/print_forms.md b/docs/tutorials/everyday/print_forms.md
new file mode 100644
index 00000000..311f9eba
--- /dev/null
+++ b/docs/tutorials/everyday/print_forms.md
@@ -0,0 +1,148 @@
+---
+outline: [2, 4]
+samples:
+ - z2ui5_cl_smps_app_024
+description: Turn an Adobe Form into a PDF from an abap2UI5 app - the generated function module between FP_JOB_OPEN and FP_JOB_CLOSE with the PDF returned instead of printed, one PDF as a download or several as a ZIP, and what ADS needs.
+---
+# Print Forms as PDF
+
+The invoice, the delivery note, the certificate: most systems already have
+the form, built as an Adobe Form years ago and printed from a SAP GUI
+transaction. A browser app has no spool dialog and usually no printer - what
+the user wants is the PDF, to look at, download, mail or print from the
+browser. The form itself stays as it is; only the output changes.
+
+The worked example is `Z2UI5_CL_SMPS_APP_024` in
+[abap2UI5/samples-stack](https://github.com/abap2UI5/samples-stack) (package
+`14`). It renders any form whose name is typed in, and downloads one PDF or a
+ZIP of several. It needs Standard ABAP and Adobe Document Services (ADS).
+
+## The Shape of the App
+
+| Step | What happens |
+|---|---|
+| **Find the function module** | `FP_FUNCTION_MODULE_NAME` returns the name the form generated on this system |
+| **Open the job** | `FP_JOB_OPEN` with `getpdf` and `nodialog` - the PDF comes back instead of going to a printer |
+| **Render** | The generated function module, with the form's data, returns the PDF in `/1BCDWB/FORMOUTPUT` |
+| **Close the job** | `FP_JOB_CLOSE`, in every case |
+| **Hand it over** | `download_b64_file`, or a ZIP with `cl_abap_zip` for several forms |
+
+## Step 1: Ask for the Function Module
+
+An Adobe Form generates a function module, and its name differs from system
+to system. Never write it down; ask for it:
+
+```abap
+TRY.
+ CALL FUNCTION 'FP_FUNCTION_MODULE_NAME'
+ EXPORTING
+ i_name = CONV fpname( form )
+ IMPORTING
+ e_funcname = function.
+ CATCH cx_fp_api INTO DATA(api_error).
+ error = api_error->get_text( ).
+ RETURN.
+ENDTRY.
+```
+
+## Step 2: Render Into a PDF, Not a Printer
+
+Two output parameters make the difference between printing and returning
+the file: `getpdf` hands the PDF back, `nodialog` suppresses the print
+dialog, which a browser app has no SAP GUI to show in:
+
+```abap
+outputpar-getpdf = abap_true.
+outputpar-nodialog = abap_true.
+
+CALL FUNCTION 'FP_JOB_OPEN'
+ CHANGING
+ ie_outputparams = outputpar
+ EXCEPTIONS
+ OTHERS = 1.
+```
+
+Then the generated function module. In a real app you call it with the
+parameters of your form's interface - the order, the customer, the items -
+plus `/1BCDWB/DOCPARAMS` in and `/1BCDWB/FORMOUTPUT` out. The sample does not
+know the form in advance, so it calls it with a parameter table that holds
+only those two; a form whose interface wants more says so in the result list.
+
+```abap
+CALL FUNCTION function
+ EXPORTING
+ /1bcdwb/docparams = docparams
+ is_order = s_order " your form's own parameters
+ IMPORTING
+ /1bcdwb/formoutput = output.
+```
+
+`output-pdf` is the file.
+
+## Step 3: Close the Job, Always
+
+Call `FP_JOB_CLOSE` whether the rendering worked or not. An open job makes
+the next `FP_JOB_OPEN` of the same session fail, and the user sees an error
+for a form that would render fine.
+
+## Step 4: Hand the PDF Over
+
+One file goes to the browser as a download:
+
+```abap
+client->follow_up_action(
+ val = client->cs_event-download_b64_file
+ t_arg = VALUE #( ( |data:application/pdf;base64,{ cl_http_utility=>encode_x_base64( pdf ) }| )
+ ( |{ to_lower( form ) }.pdf| ) ) ).
+```
+
+Several go into one ZIP, so the browser asks once instead of once per file:
+
+```abap
+DATA(zip) = NEW cl_abap_zip( ).
+LOOP AT t_pdf INTO DATA(s_pdf).
+ zip->add( name = |{ to_lower( s_pdf-form ) }.pdf|
+ content = s_pdf-pdf ).
+ENDLOOP.
+DATA(zip_file) = zip->save( ).
+```
+
+To show the PDF inside the app rather than download it, put it into a
+`PDFViewer` - see [PDF](/cookbook/device_capabilities/pdf). To send it, it is
+the attachment of [Send by Mail](/tutorials/everyday/send_by_mail).
+
+## What the System Needs
+
+- **ADS.** Adobe Document Services do the rendering; the system reaches them
+ through the RFC destination `ADS` in `SM59`. When `FP_JOB_OPEN` fails right
+ away, this is the first thing to check - the sample says so in its error.
+- **ABAP Cloud.** The classic runtime with `FP_JOB_OPEN` is not released
+ there. Forms are rendered by the Forms Service by Adobe through
+ `cl_fp_ads_util`, with the form template and the data as XML; the shape of
+ the app - render in the handler, hand the PDF to the browser - stays the
+ same.
+- **Many forms.** Rendering takes a noticeable moment per form. A few at a
+ time are fine in a roundtrip; for a whole month of invoices, render in a
+ background job and offer the result for download when it is done.
+
+## Related
+
+- [PDF](/cookbook/device_capabilities/pdf) - showing a PDF in the view.
+- [Upload, Download](/cookbook/device_capabilities/upload_download) - the
+ download action and the data URL types it refuses.
+- [Send by Mail](/tutorials/everyday/send_by_mail) - the PDF as an attachment.
+
+
+
+## Working Samples
+
+Complete apps from the [sample catalog](https://abap2ui5.github.io/samples-site/)
+that use what this page describes. Each is a single class in [abap2UI5/samples](https://github.com/abap2UI5/samples)
+unless its row names another of the three sample repositories — pull that repository with
+[abapGit](https://abapgit.org) and start the class with `?app_start=`.
+
+| Sample | Class |
+|---|---|
+| Render to PDF, Download One or a ZIP — Renders Adobe Forms of the system to PDF with FP_JOB_OPEN and the generated function module - one form as a PDF download, several at once as one ZIP file | [`Z2UI5_CL_SMPS_APP_024`](https://github.com/abap2UI5/samples-stack/blob/main/src/14/z2ui5_cl_smps_app_024.clas.abap) · [samples-stack](https://github.com/abap2UI5/samples-stack), branch `14-adobe-forms` |
+
+
diff --git a/docs/tutorials/everyday/react_to_changes.md b/docs/tutorials/everyday/react_to_changes.md
new file mode 100644
index 00000000..656f7075
--- /dev/null
+++ b/docs/tutorials/everyday/react_to_changes.md
@@ -0,0 +1,147 @@
+---
+outline: [2, 4]
+samples:
+ - z2ui5_cl_smps_app_011
+ - z2ui5_cl_smps_app_012
+description: Do something whenever a RAP business object changes - a log, a mail, a follow-up document - with business events raised in the save sequence and consumed by a handler class, and how the result reaches an abap2UI5 screen.
+---
+# React to Changes
+
+Every new ticket should be logged, a manager informed when a status changes,
+a follow-up document created when an order is released. The tempting place
+for that is the app's Save handler: after the COMMIT, write the log, send the
+mail. It works until the second app, the Fiori elements UI or the background
+job changes the same data and nothing happens.
+
+The reaction belongs to the business object, not to one of its screens. RAP
+business events are the mechanism: the business object announces what
+happened, a handler reacts, and neither knows the other.
+
+The worked example is package `05` of
+[abap2UI5/samples-stack](https://github.com/abap2UI5/samples-stack):
+`Z2UI5_CL_SMPS_APP_011` creates and changes tickets, `Z2UI5_CL_SMPS_APP_012`
+shows the log the handler writes. Business events need ABAP Platform 2021 or
+ABAP Cloud.
+
+## The Shape of the Solution
+
+| Part | Where it lives | What it does |
+|---|---|---|
+| **The event** | The behavior definition | Declares what the business object announces |
+| **Raising** | The additional save of the behavior pool | Raises the event for what this transaction created or changed |
+| **Consuming** | A handler class of its own | Reacts - here, writes a log row |
+| **The screens** | Two abap2UI5 apps | One changes data, one shows what the handler did |
+
+## Step 1: Declare What Is Announced
+
+Two kinds of event, and the choice between them is the main decision:
+
+```abap
+" notification event: the key, nothing else
+event TicketCreated;
+
+" data event: the state at the time of the change, typed by an abstract entity
+event StatusChanged parameter Z2UI5_R_SMPS_TCK_STAT;
+```
+
+A **notification** event says "this instance changed" and leaves it to the
+handler to read the current state. A **data** event carries the state along,
+so the handler needs no read - at the price of data that may be outdated by
+the time the handler runs. Take the notification when the handler needs the
+latest state anyway; take the data event when it needs to know what the
+state was at the moment of the change, as an audit log does.
+
+## Step 2: Raise in the Save Sequence
+
+Events are raised where the business object knows the transaction went
+through - in the additional save of the behavior pool, which sees what was
+created and updated:
+
+```abap
+METHOD save_modified.
+ IF create-ticket IS NOT INITIAL.
+ RAISE ENTITY EVENT z2ui5_r_smps_tck~TicketCreated
+ FROM VALUE #( FOR c IN create-ticket ( %key = VALUE #( TicketUUID = c-TicketUUID ) ) ).
+ ENDIF.
+ ...
+ENDMETHOD.
+```
+
+Raised here, an event is raised only for data that is actually saved. A
+validation that refuses the save also prevents the event, which a log
+written in the app's handler would not get right.
+
+## Step 3: Consume in a Handler Class
+
+The handler is a class of its own that inherits from
+`cl_abap_behavior_event_handler` and names the events it wants:
+
+```abap
+CLASS lhe_ticket DEFINITION INHERITING FROM cl_abap_behavior_event_handler.
+ PRIVATE SECTION.
+ METHODS on_ticket_created FOR ENTITY EVENT
+ ticketcreated FOR ticket~TicketCreated.
+ENDCLASS.
+```
+
+That declaration is all it takes to listen; nothing is registered anywhere else.
+A second handler for the same event runs as well, and deleting one leaves
+the business object unchanged. The handler receives all instances of the
+transaction as one table, so it writes its log rows with one `INSERT`.
+
+The handler runs **after** the transaction that raised the event, in a
+session of its own. Two consequences:
+
+- An error in the handler does not undo the change. Handle failures in the
+ handler - log them, retry them - instead of expecting the save to fail.
+- The app that saved does not see the result in the same roundtrip. The log
+ row exists a moment later.
+
+## Step 4: Show the Result on Screen
+
+The abap2UI5 apps take no part in the event mechanism: one changes tickets
+through EML as any app would, the other reads the log table. Because the
+handler runs after the save, the log screen shows a new row on its next
+read - the sample has a refresh button for it.
+
+For a screen that should update by itself:
+
+- **Polling.** A timer that reads again every few seconds - simple, and fine
+ for a monitor page. See [Timer](/cookbook/browser_interaction/timer).
+- **Push.** The handler publishes a message through ABAP Messaging Channels,
+ and the open screens receive it over a WebSocket without asking - see
+ [WebSocket](/cookbook/expert_more/websocket). On-premise only.
+
+## What Fits Here, and What Does Not
+
+| Reaction | Business event? |
+|---|---|
+| A log or audit trail of changes | Yes - a data event and an `INSERT` |
+| A mail or a notification to someone | Yes - the handler sends it, see [Send by Mail](/tutorials/everyday/send_by_mail) |
+| A follow-up document in another business object | Yes - the handler calls that business object with EML |
+| A check that may refuse the save | No - that is a validation of the business object |
+| A value that must be correct right after the save | No - that is a determination |
+
+## Related
+
+- [RAP](/cookbook/eml_cds_sql/rap) and [EML](/cookbook/eml_cds_sql/eml) -
+ the business object and how the apps call it.
+- [Timer](/cookbook/browser_interaction/timer) and
+ [WebSocket](/cookbook/expert_more/websocket) - getting the result onto the
+ screen without a click.
+
+
+
+## Working Samples
+
+Complete apps from the [sample catalog](https://abap2ui5.github.io/samples-site/)
+that use what this page describes. Each is a single class in [abap2UI5/samples](https://github.com/abap2UI5/samples)
+unless its row names another of the three sample repositories — pull that repository with
+[abapGit](https://abapgit.org) and start the class with `?app_start=`.
+
+| Sample | Class |
+|---|---|
+| Ticket App — every create and update raises an entity event | [`Z2UI5_CL_SMPS_APP_011`](https://github.com/abap2UI5/samples-stack/blob/main/src/05/z2ui5_cl_smps_app_011.clas.abap) · [samples-stack](https://github.com/abap2UI5/samples-stack), branch `05-business-events` |
+| Event Log App — what the handler wrote, newest first | [`Z2UI5_CL_SMPS_APP_012`](https://github.com/abap2UI5/samples-stack/blob/main/src/05/z2ui5_cl_smps_app_012.clas.abap) · [samples-stack](https://github.com/abap2UI5/samples-stack), branch `05-business-events` |
+
+
diff --git a/docs/everyday/report_export.md b/docs/tutorials/everyday/report_export.md
similarity index 100%
rename from docs/everyday/report_export.md
rename to docs/tutorials/everyday/report_export.md
diff --git a/docs/tutorials/everyday/send_by_mail.md b/docs/tutorials/everyday/send_by_mail.md
new file mode 100644
index 00000000..ced8150f
--- /dev/null
+++ b/docs/tutorials/everyday/send_by_mail.md
@@ -0,0 +1,152 @@
+---
+outline: [2, 4]
+samples:
+ - z2ui5_cl_smps_app_023
+description: Send what the app shows by mail - the attachment written in the backend, cl_bcs_mail_message on ABAP Cloud and current releases, the outbound configuration a mail needs, the status after the send, and how to test it.
+---
+# Send by Mail
+
+"Can you send me that list?" is the most common follow-up to any report. The
+job: a button next to the data, a small form for the recipient, the data as
+an attachment, and a clear answer whether the mail left the system. Nothing
+in it is specific to abap2UI5 except the form - the rest is the mail API of
+the system and its configuration, which is where most of the trouble is.
+
+The worked example is `Z2UI5_CL_SMPS_APP_023` in
+[abap2UI5/samples-stack](https://github.com/abap2UI5/samples-stack) (package
+`13`). It sends a table as an `.xlsx` attachment with `cl_bcs_mail_message`.
+
+## The Shape of the App
+
+| Part | What it does |
+|---|---|
+| **Form** | Sender, recipient, subject, text - bound attributes, filled in where the app knows the answer |
+| **Attachment** | Written in the backend at the moment of sending, from the data the app holds |
+| **Send** | `cl_bcs_mail_message`: sender, recipient, subject, body, attachment, `send( )` |
+| **Answer** | The status the mail system returns, or the text of the exception |
+
+## Step 1: Fill In What the App Knows
+
+A mail form that starts empty is a form the user abandons. Fill the subject
+and the text when the app starts, and the recipient where the record knows
+one - the customer's contact, the creator of a request:
+
+```abap
+IF client->check_on_init( ).
+ subject = `Products as XLSX`.
+ text = |Hello,\n\nthe current product list is attached.\n\nSent from an abap2UI5 app.|.
+ view_display( ).
+```
+
+The sender is a configuration question, not a user's choice. Most mail
+setups accept only addresses of the own domain, or one fixed sender per
+system. Fill it from a customizing table or a constant, and show it read
+only, rather than letting each user type one the mail server then refuses.
+
+Use `type` `Email` on the address inputs, so a phone shows the keyboard with
+the `@`.
+
+## Step 2: Build the Attachment When Sending
+
+Write the file in the handler of the Send button, from the rows the app has,
+not from a file the browser kept. The attachment then shows exactly the data
+the user saw, and nothing large travels through the roundtrips before it is
+needed:
+
+```abap
+DATA(xlsx) = z2ui5_cl_smps_xlsx=>write( t_cells ).
+```
+
+The same idea works for any format: a PDF from a form (see
+[Print Forms as PDF](/tutorials/everyday/print_forms)), a CSV built with
+`concat_lines_of( )`, a ZIP of several files with `cl_abap_zip`.
+
+## Step 3: Send
+
+`cl_bcs_mail_message` is the mail API of ABAP Cloud, and available on
+on-premise releases from 2023 on:
+
+```abap
+TRY.
+ DATA(mail) = cl_bcs_mail_message=>create_instance( ).
+ mail->set_sender( CONV #( sender ) ).
+ mail->add_recipient( CONV #( recipient ) ).
+ mail->set_subject( CONV #( subject ) ).
+ mail->set_main( cl_bcs_mail_textpart=>create_text_plain( text ) ).
+ mail->add_attachment( cl_bcs_mail_binarypart=>create_instance(
+ iv_content = xlsx
+ iv_content_type = CONV #( z2ui5_cl_smps_xlsx=>mime_type )
+ iv_filename = `products.xlsx` ) ).
+ mail->send( IMPORTING ev_mail_status = DATA(status) ).
+
+ CATCH cx_bcs_mail INTO DATA(error).
+ client->message_box_display( text = error->get_text( ) type = `error` ).
+ENDTRY.
+```
+
+Everything that can go wrong arrives as `cx_bcs_mail`: an address that is
+not one, a sender the configuration does not allow, a system without
+outbound mail. Show its text. It is the one piece of information the user
+can pass on to the person who runs the system.
+
+On older on-premise releases the API is `cl_bcs` with `cl_document_bcs` -
+[E-Mail](/cookbook/expert_more/email) shows that variant, attachment
+included.
+
+## Step 4: Say What Happened
+
+`send( )` hands the mail to the outbound queue of the system. The status it
+returns says whether the mail left already or is waiting there. A mail that
+is queued is not an error, but it is not delivered either, so say which one
+it is instead of a plain "sent":
+
+```abap
+client->message_box_display( text = |Mail handed over to the mail system, status { status }| type = `success` ).
+```
+
+## What the System Needs
+
+The code is the small part. A mail only leaves the system when outbound mail
+is configured:
+
+| Stack | Configuration | Where to look when nothing arrives |
+|---|---|---|
+| ABAP Cloud | A communication arrangement for the scenario `SAP_COM_0548` | The communication arrangement and the mail monitoring app of the launchpad |
+| On-premise | SMTP in transaction `SCOT` | Transaction `SOST` |
+
+Agree on the sender address with whoever runs the mail server before the
+first test. Most failures that look like a bug in the app are a sender the
+server does not accept.
+
+## Testing
+
+- Send to yourself first, and open the attachment: a file that downloads
+ fine can still be refused by a mail client if its MIME type is wrong.
+- On a development system without a real mail server, an SMTP sink such as
+ a local test server shows the mail exactly as it would be sent.
+- Large attachments are the mail server's limit, not the app's. Several
+ megabytes are usually fine; beyond that, send a link to the record
+ instead.
+
+## Related
+
+- [E-Mail](/cookbook/expert_more/email) - the classic BCS API.
+- [Report and Export](/tutorials/everyday/report_export) - the report a mail button
+ usually sits on.
+- [Spreadsheet](/cookbook/device_capabilities/spreadsheet) - writing the
+ attachment.
+
+
+
+## Working Samples
+
+Complete apps from the [sample catalog](https://abap2ui5.github.io/samples-site/)
+that use what this page describes. Each is a single class in [abap2UI5/samples](https://github.com/abap2UI5/samples)
+unless its row names another of the three sample repositories — pull that repository with
+[abapGit](https://abapgit.org) and start the class with `?app_start=`.
+
+| Sample | Class |
+|---|---|
+| E-Mail — Send a Table as XLSX Attachment — A mail with an .xlsx attachment, sent with cl_bcs_mail_message - recipient, subject and text from the screen, the attachment written with the XCO XLSX library | [`Z2UI5_CL_SMPS_APP_023`](https://github.com/abap2UI5/samples-stack/blob/main/src/13/z2ui5_cl_smps_app_023.clas.abap) · [samples-stack](https://github.com/abap2UI5/samples-stack), branch `13-excel-email` |
+
+
diff --git a/docs/tutorials/index.md b/docs/tutorials/index.md
index b39a6c0f..ab8df36e 100644
--- a/docs/tutorials/index.md
+++ b/docs/tutorials/index.md
@@ -45,6 +45,14 @@ no frontend project, no OData service.
- **[Step 12: Unit Tests](/tutorials/step-12)** — test the app class
like any ABAP class; no UI5 runtime involved.
+## Everyday Work
+
+The steps build one app to teach the framework. The
+[Everyday Work](/tutorials/everyday/) guides start where they end: each one
+takes a job from a real project - an Excel import, a mass change, an approval
+worklist, a report with export - and walks through it from the first click to
+the saved data, with a sample you can install.
+
## What You Should Know
No prior abap2UI5 or UI5 experience is needed — the tutorial introduces every
@@ -62,7 +70,8 @@ popup calls.
## After the Tutorial
-The [Cookbook](/cookbook/) covers each topic again as a
+The [Everyday Work](/tutorials/everyday/) guides apply the steps to whole
+jobs, the [Cookbook](/cookbook/) covers each topic again as a
reference chapter, and three catalogs of complete, tested apps continue from
there: