WEB 伝票形式明細行 HTML(PYTHON.400/QHTMLSRC.DENPYO)

最終更新: 2025-11-28
0001.00 <!DOCTYPE html>                                                                  
0002.00 % import ibm_db2                                                                       
0003.00 <html lang="ja">                                                                 
0004.00   <head>                                                                         
0005.00     <meta charset="utf-8">                                                       
0006.00           <meta http-equiv="Content-Language" content="ja">                      
0007.00           <meta http-equiv="Content-Type" content="text/html; charset=utf-8">    
0008.00           <meta http-equiv="Pragma" content="no-cache">                          
0009.00           <meta http-equiv="cache-control" content="no-cache">                   
0010.00           <meta http-equiv="Expires" content="-1">                               
0011.00           <meta http-equiv="X-UA-Compatible" content="IE=edge"/>                 
0012.00           <meta name="viewport" content="width=device-width, initial-scale=1.0"/>
0013.00     <title>$TEXT</title>                                                   
0014.00     <style>                                                                      
0015.00       html {                                                                           
0016.00         font-family: "BIZ UPDGothic", Meiryo, sans-serif;                              
0017.00         accent-color: rgb(26, 115, 232);                                               
0018.00         color: #555;                                                                   
0019.00         margin: 0;                                                                     
0020.00         box-sizing: border-box;                                                        
0021.00         height: 100%;                                                                  
0022.00       }                                                                                
0023.00       body {                                                                           
0024.00         margin: 0;                        
0025.00         padding: 1em 4em 3em;             
0026.00         box-sizing: border-box;           
0027.00         height: 100%;                     
0028.00         display: flex;                    
0029.00         flex-direction: column;           
0030.00       }                                   
0031.00       header {                            
0032.00         flex: 0;                          
0033.00       }                                   
0034.00       footer {                            
0035.00         flex: 0;                          
0036.00       }                                   
0037.00       .content {                          
0038.00         flex: 1;                          
0039.00         min-height: 0;                    
0040.00         max-width: calc(100%);            
0041.00         overflow: auto;                   
0042.00       }                                   
0043.00       main {                              
0044.00         min-height: 0;                    
0045.00         flex: 1;                          
0046.00         display: flex;                    
0047.00         flex-direction: column;           
0048.00         align-items: flex-start;                 
0049.00       }                                          
0050.00       form {                                     
0051.00         display: contents;                       
0052.00       }                                          
0053.00       input {                                    
0054.00         font-family: inherit;                    
0055.00         font-size: inherit;                      
0056.00         color: inherit;                          
0057.00       }                                          
0058.00       input:read-only {                          
0059.00         border-color: transparent;               
0060.00       }                                          
0061.00       h1 {                                       
0062.00         font-size: 2.4em;                        
0063.00         font-weight: normal;                     
0064.00         color: #8888ff;                          
0065.00       }                                          
0066.00       fieldset {                                 
0067.00         display: grid;                           
0068.00         grid-template-columns: repeat(3, 20em);  
0069.00         gap: 0.2em;                              
0070.00         border: 0;                               
0071.00         margin: 1em 0;                           
0072.00       }                                    
0073.00       .span-2 {                            
0074.00         grid-column: span 2;               
0075.00       }                                    
0076.00       .span-3 {                            
0077.00         grid-column: span 3;               
0078.00       }                                    
0079.00       label {                              
0080.00         display: flex;                     
0081.00         align-items: baseline;             
0082.00         gap: 0 0.7em;                      
0083.00       }                                    
0084.00       .name {                              
0085.00         flex: none;                        
0086.00         width: 8em;                        
0087.00       }                                    
0088.00       header .name::after,                 
0089.00       footer .name::after                  
0090.00       {                                    
0091.00         content: ':';                      
0092.00       }                                    
0093.00       #record-table thead tr th {          
0094.00         color: #333;                       
0095.00         font-weight: normal;               
0096.00         text-align: left;                             
0097.00         border-bottom: 2px solid hsl(220, 90%, 56%);  
0098.00       }                                               
0099.00       #record-table {                                 
0100.00         border-collapse: separate;                    
0101.00       }                                               
0102.00       #record-table th {                              
0103.00         padding-bottom: 0.75em;                       
0104.00         background-color: white;                      
0105.00         position: sticky;                             
0106.00         top: 0;                                       
0107.00       }                                               
0108.00       button > img {                               
0109.00         width: 1.2em;                                 
0110.00         height: 1.2em;                                
0111.00         object-fit: contain;                          
0112.00         vertical-align: text-bottom;                  
0113.00         margin-right: 0.3em;                          
0114.00       }                                               
0115.00       .dspmod {                                       
0116.00         position: absolute;                           
0117.00         top: 1em;                                     
0118.00         right: 3em;                                   
0119.00         font-size: 2.5em;                             
0120.00         color: #8888ff;                                                                                              
0121.00       }                                                                                                              
0122.00     </style>                                                                                                   
0123.00   </head>                                                                                                      
0124.00   <body data-dspmod="{{dspmod}}">                                                                              
0125.00     <form action="/DSPDTA" method="post" id="record-form">                                                     
0126.00       <input type="hidden" name="_CHG" value="*NO">                                                            
0127.00       <header>                                                                                                 
0128.00         <h1>$TEXT</h1>                                                                                   
0129.00 % if dspmod == 'DSPPTN':                                                                                             
0130.00         <div class="dspmod">  表示  </div>                                                               
0131.00 % elif dspmod == 'CHGPTN':                                                                                           
0132.00         <div class="dspmod">  変更  </div>                                                               
0133.00 % else:                                                                                                              
0134.00         <div class="dspmod">  入力  </div>                                                               
0135.00 % end                                                                                                                
0136.00         <div class="buttons">                                                                                  
0137.00           <button type="submit" formaction="/end"><img src="/icon/END.SVG">  終了  </button>       
0138.00 % if dspmod == "CHGPTN":                                                                                             
0139.00           <button type="submit" formaction="/update"><img src="/icon/UPDATE.SVG">  更新  </button> 
0140.00           <button type="submit" formaction="/delete"><img src="/icon/DELETE.SVG">  削除  </button> 
0141.00 % elif dspmod == "INPPTN":                                                                                           
0142.00           <button type="submit" formaction="/insert"><img src="/icon/UPDATE.SVG">  更新  </button> 
0143.00 % end                                                                                                                
0144.00           <button type="button" class="default" onclick="location.href='/init';"><img src="/icon/BACK.SVG"> 
0145.00         </div>                                                                                                    
0146.00         <!--  ヘッダー  -->                                                                                       
0147.00         <fieldset>                                                                                                
0148.00           <label$CLASS>                                                                                           
0149.00             <span class="name">$COLHDG</span>                                                               
0150.00             <span class="value">                                                                                  
0151.00               <input type="text" name="$FLD" value="{{values[0].get('$FLD', '')}}"                                   
0152.00                   size="$SIZE" maxlength="$FLDLEN"   {{"readonly" if dspmod != "INPPTN" else ""}}$ALIGN>             
0153.00             </span>                                                                                               
0154.00           </label>                                                                                                
0155.00         </fieldset>                                                                                               
0156.00       </header>                                                                                                   
0157.00       <!--  サブファイル  -->                                                                                     
0158.00       <main>                                                                                                      
0159.00         <div class="content">                                                                                     
0160.00           <table id="record-table">                                                                               
0161.00             <thead>                                                                                               
0162.00               <tr>                                                                                                
0163.00                 <th class="checkbox">                                                                             
0164.00                 </th>                                                                                             
0165.00                 <th>                                                                                              
0166.00                   <span class="name">$COLHDG</span>                                                         
0167.00                 </th>                                                                                             
0168.00               </tr>                                                                                   
0169.00             </thead>                                                                                  
0170.00             <tbody>                                                                                   
0171.00             </tbody>                                                                                  
0172.00           </table>                                                                                    
0173.00         </div>                                                                                        
0174.00       </main>                                                                                         
0175.00       <footer>                                                                                        
0176.00         <!--  フッター  -->                                                                           
0177.00         <fieldset>                                                                                    
0178.00           <label$CLASS>                                                                               
0179.00             <span class="name">$COLHDG</span>                                                   
0180.00             <span class="value">                                                                      
0181.00               <input type="text" name="$FLD" value="{{values[0].get('$FLD', '')}}"                       
0182.00                      size="$SIZE" maxlength="$FLDLEN" {{"readonly" if dspmod == "DSPPTN" else ""}}$ALIGN>
0183.00             </span>                                                                                   
0184.00           </label>                                                                                    
0185.00         </fieldset>                                                                                   
0186.00       </footer>                                                                                       
0187.00       </main>                                                                                         
0188.00       <footer>                                                                                        
0189.00       </footer>                                                                                       
0190.00     </form>                                                                                           
0191.00     <form name="key">                                                                                 
0192.00       <input type="hidden" name="$KEYFLD" value="{{values[0].get('$KEYFLD', '')}}">   
0193.00     </form>                                                                           
0194.00     <template id="empty-record">                                                      
0195.00     </template>                                                                       
0196.00     <script>                                                                          
0197.00       "use strict";                                                                         
0198.00                                                                                             
0199.00       /**                                                                                   
0200.00        *  要素の一番下までスクロールされたかどうか監視する。                                
0201.00        */                                                                                   
0202.00       class ScrollEndObserver {                                                             
0203.00         #callback;                                                                          
0204.00         #threshold = 0;                                                                     
0205.00         #targets = new Map();                                                               
0206.00         constructor(callback, options) {                                                    
0207.00           this.#callback = callback;                                                        
0208.00           const effective_options = {                                                       
0209.00             threshold: 0,                                                                   
0210.00             ...options,                                                                     
0211.00           };                                                                                
0212.00           this.#threshold = effective_options.threshold;                                    
0213.00         }                                                                                   
0214.00         /**                                                                                 
0215.00          * container 要素の監視を開始する。                                                 
0216.00          */                                                               
0217.00         observe(container) {                                              
0218.00           if(!this.#targets.has(container)) {                             
0219.00             const onscroll = this.#onscroll.bind(this);                   
0220.00             container.addEventListener("scroll", onscroll, {              
0221.00               passive: true,                                              
0222.00             });                                                           
0223.00             this.#targets.set(container, {                                
0224.00               onscroll,                                                   
0225.00               scrollHeight: 0,                                            
0226.00               clientHeight: 0,                                            
0227.00             });                                                           
0228.00           }                                                               
0229.00         }                                                                 
0230.00         /**                                                               
0231.00          * container 要素の監視を終了する。                               
0232.00          */                                                               
0233.00         unobserve(container) {                                            
0234.00           const entry = this.#targets.get(container);                     
0235.00           if(entry) {                                                     
0236.00             container.removeEventListener("scroll", entry.onscroll);      
0237.00             this.#targets.delete(container);                              
0238.00           }                                                               
0239.00         }                                                                 
0240.00         /**                                                    
0241.00          *  全ての要素の監視を終了する。                       
0242.00          */                                                    
0243.00         disconnect() {                                         
0244.00           this.#targets.forEach((value, key) => {           
0245.00             this.unobserve(key);                               
0246.00           });                                                  
0247.00         }                                                      
0248.00         /**                                                    
0249.00          * onscroll イベントのリスナー                         
0250.00          */                                                    
0251.00         #onscroll(event) {                                     
0252.00           const target = event.target;                         
0253.00           const entry = this.#targets.get(target);             
0254.00                                                                
0255.00           //  監視対象でない場合は終了。                       
0256.00           if(!entry)                                           
0257.00             return;                                            
0258.00                                                                
0259.00           const scrollHeight = target.scrollHeight;            
0260.00           const clientHeight = target.clientHeight;            
0261.00                                                                
0262.00           //  要素の大きさが以前と変わっていないなら終了。     
0263.00           // ( 不必要な callback が発生するのを避けるため。 )  
0264.00           if(scrollHeight === entry.scrollHeight && clientHeight === entry.clientHeight)                 
0265.00             return;                                                                                      
0266.00                                                                                                          
0267.00           //  データの追加が必要なら callback する。                                                     
0268.00           if(this.needsMoreContent(target)) {                                                            
0269.00             entry.scrollHeight = scrollHeight;                                                           
0270.00             entry.clientHeight = clientHeight;                                                           
0271.00             this.#callback({                                                                             
0272.00               target,                                                                                    
0273.00               scrollHeight,                                                                              
0274.00               clientHeight,                                                                              
0275.00             }, this);                                                                                    
0276.00           }                                                                                              
0277.00         }                                                                                                
0278.00         /**                                                                                              
0279.00          *  指定のコンテナにデータの追加が必要かどうか判定する。                                         
0280.00          */                                                                                              
0281.00         needsMoreContent(target) {                                                                       
0282.00           const scrollHeight = target.scrollHeight;                                                      
0283.00           const clientHeight = target.clientHeight;                                                      
0284.00           const scrollTop = target.scrollTop;                                                            
0285.00                                                                                                          
0286.00           return scrollTop >= scrollHeight - clientHeight - (clientHeight * (1.0 - this.#threshold)); 
0287.00         }                                                                                                
0288.00       }                                                                              
0289.00                                                                                      
0290.00       /**                                                                            
0291.00        *  この画面特有の処理                                                         
0292.00        */                                                                            
0293.00       class App {                                                                    
0294.00         #endOfData = false;  //  最後のページまでデータを読み込んだ。                
0295.00         #scrollEndObserver;                                                          
0296.00         #container = document.querySelector(".content");                             
0297.00         #header = document.querySelector("header");                                  
0298.00         #main = document.querySelector("main");                                      
0299.00         #footer = document.querySelector("footer");                                  
0300.00         #tbody = document.querySelector("#record-table tbody");                      
0301.00         #recordForm = document.getElementById("record-form");                        
0302.00         #emptyRecord = null;                                                         
0303.00                                                                                      
0304.00         constructor() {                                                              
0305.00           this.#scrollEndObserver = new ScrollEndObserver((entry, observer) => {  
0306.00             this.readPage();                                                         
0307.00           }, {                                                                       
0308.00             threshold: 0.8,                                                          
0309.00           });                                                                        
0310.00         }                                                                            
0311.00                                                                                      
0312.00         /**                                                                                                    
0313.00          *  ページが読み込まれたときに最初に実行される処理。                                                   
0314.00          */                                                                                                    
0315.00         async start() {                                                                                        
0316.00           //  ユーザーがヘッダーまたはフッターを変更したときは目印をつける。                                   
0317.00           [this.#header, this.#footer].forEach(element => element.addEventListener("input", event => {   
0318.00             document.querySelector("input[name='_CHG']").value = "*YES";                                       
0319.00           }));                                                                                                 
0320.00           //  ユーザーが明細レコードを変更した時はそのレコードに目印を付ける。                                 
0321.00           this.#main.addEventListener("input", event => {                                                   
0322.00             const tr = event.target.closest("tr");                                                             
0323.00             if(tr)                                                                                             
0324.00               tr.setAttribute("data-modified", "");                                                            
0325.00           });                                                                                                  
0326.00                                                                                                                
0327.00           //  ユーザーが変更したレコードのみを submit する。                                                   
0328.00           this.#recordForm.addEventListener("submit", event => {                                            
0329.00             //  変更されていないレコードに一時的に  disabled をつける。                                        
0330.00             this.#tbody.querySelectorAll("tr:not([data-modified])").forEach(tr => {                         
0331.00               tr.querySelectorAll("input:enabled, select:enabled, textarea:enabled").forEach(element => {   
0332.00                 element.setAttribute("data-disabled-temporarily", "");                                         
0333.00                 element.disabled = true;                                                                       
0334.00               });                                                                                              
0335.00             });                                                                                                
0336.00             // submit 後に元に戻す。                                                                   
0337.00             setTimeout(() => {                                                                      
0338.00               this.#tbody.querySelectorAll("[data-disabled-temporarily").forEach(element => {       
0339.00                 element.disabled = false;                                                              
0340.00                 element.removeAttribute("data-disabled-temporarily");                                  
0341.00               });                                                                                      
0342.00             }, 0);                                                                                     
0343.00           });                                                                                          
0344.00           //  削除ボタンの処理                                                                         
0345.00           document.querySelectorAll("[formaction='/delete']").forEach(e => {                        
0346.00             e.addEventListener("click", event => {                                                  
0347.00               const count = this.#tbody.querySelectorAll("input[name='_CHG']:checked").length;         
0348.00               if(count >= 1) {                                                                      
0349.00                 //  チェックされている明細が存在する→明細の削除                                       
0350.00                 if(!window.confirm(` 明細  ${count}  件を削除しますか? `)) {                          
0351.00                   event.preventDefault();                                                              
0352.00                 }                                                                                      
0353.00               } else {                                                                                 
0354.00                 //  チェックされている明細が存在しない→ヘッダー毎削除                                 
0355.00                 if(!window.confirm(" この伝票を削除しますか? ")) {                                    
0356.00                   event.preventDefault();                                                              
0357.00                 }                                                                                      
0358.00               }                                                                                        
0359.00             });                                                                                        
0360.00           });                                                    
0361.00           //  入力画面の場合はサーバーからデータを読み取らない。 
0362.00           if(document.body.matches("[data-dspmod='INPPTN']")) {  
0363.00             this.#endOfData = true;                              
0364.00           }                                                      
0365.00           //  最初から表示されている部分をサーバーから読み取る。 
0366.00           await this.readPage();                                 
0367.00           //  スクロールの監視を開始する。                       
0368.00           this.#scrollEndObserver.observe(this.#container);      
0369.00         }                                                        
0370.00         /**                                                      
0371.00          *  入力用の空行の HTML をサーバーから取得する。         
0372.00          */                                                      
0373.00         async fetchEmptyRecord() {                               
0374.00           return await this.fetchText("/template", {             
0375.00             method: "GET",                                       
0376.00             cache: "no-cache",                                   
0377.00           });                                                    
0378.00         }                                                        
0379.00                                                                  
0380.00         /**                                                      
0381.00          *  入力用の空行の要素を取得する。                       
0382.00          */                                                      
0383.00         async getEmptyRecord() {                                 
0384.00           if(!this.#emptyRecord) {                                        
0385.00             //  サーバーから空行の HTML を取得する。                      
0386.00             this.#emptyRecord = new Promise(async resolve => {         
0387.00               const html = await this.fetchEmptyRecord();                 
0388.00               const emptyRecord = document.getElementById("empty-record");
0389.00               emptyRecord.innerHTML = html;                               
0390.00               resolve(emptyRecord);                                       
0391.00             });                                                           
0392.00           }                                                               
0393.00           return (await this.#emptyRecord).content.cloneNode(true);       
0394.00         }                                                                 
0395.00                                                                           
0396.00         /**                                                               
0397.00          *  入力用の空行を一行追加する。                                  
0398.00          */                                                               
0399.00         async appendEmptyRecord(key) {                                    
0400.00           const emptyRecord = await this.getEmptyRecord();                
0401.00           emptyRecord.querySelector("input[type='text']").value = key;    
0402.00           this.#tbody.append(emptyRecord);                                
0403.00         }                                                                 
0404.00         /**                                                               
0405.00          *  非同期通信でテキストを受信する。                              
0406.00          */                                                               
0407.00         async fetchText(...args) {                                        
0408.00           let response;                                                                                  
0409.00           try {                                                                                          
0410.00             response = await fetch(...args);                                                             
0411.00           } catch(error) {                                                                               
0412.00             throw new Error(` 通信エラーが発生しました。 : nn${error}`);                               
0413.00           }                                                                                              
0414.00           if(!response.ok) {                                                                             
0415.00             throw new Error(` サーバーがエラーを返しました : ${response.status} ${response.statusText}`);
0416.00           }                                                                                              
0417.00           try {                                                                                          
0418.00             return await response.text();                                                                
0419.00           } catch(error) {                                                                               
0420.00             throw new Error(` サーバーからの応答を解釈出来ません : nn${error}`);                       
0421.00           }                                                                                              
0422.00         }                                                                                                
0423.00         /**                                                                                              
0424.00          *  指定のレコード番号から始まるデータをサーバーに問い合わせる。                                 
0425.00          */                                                                                              
0426.00         async fetchNext(index) {                                                                         
0427.00           const formData = new FormData(document.querySelector("form[name='key']"));                     
0428.00           formData.append("_RRN", index)                                                                 
0429.00           return await this.fetchText("/record", {                                                       
0430.00             method: "POST",                                                                              
0431.00             body: formData,                                                                              
0432.00             cache: "no-store",                                             
0433.00           });                                                              
0434.00         }                                                                  
0435.00                                                                            
0436.00         /**                                                                
0437.00          *  サーバーから一画面分受け取って画面に表示する。                 
0438.00          */                                                                
0439.00         async readPage() {                                                 
0440.00           const tbody = document.querySelector("tbody");                   
0441.00           do {                                                             
0442.00             const index = tbody.querySelectorAll("tr").length;             
0443.00             if(this.#endOfData) {                                          
0444.00               //  既に最後のデータまでサーバーから受け取っている。         
0445.00               if(document.body.matches(":not([data-dspmod='DSPPTN'])")) {  
0446.00                 //  入力画面なら空行を追加する。                           
0447.00                 await this.appendEmptyRecord(index + 1);                   
0448.00               } else                                                       
0449.00                 break;                                                     
0450.00             } else {                                                       
0451.00               let text;                                                    
0452.00               try {                                                        
0453.00                 text = await this.fetchNext(index);                        
0454.00               } catch(error) {                                             
0455.00                 alert(error);                                              
0456.00                 break;                                                        
0457.00               }                                                               
0458.00               if(text.length === 0) {                                         
0459.00                 //  最後のレコードまで読み取った。                            
0460.00                 this.#endOfData = true;                                       
0461.00               }                                                               
0462.00               tbody.innerHTML += text;                                        
0463.00             }                                                                 
0464.00           } while(this.#scrollEndObserver.needsMoreContent(this.#container)); 
0465.00         }                                                                     
0466.00       }                                                                       
0467.00                                                                               
0468.00       const app = new App();                                                  
0469.00       app.start();                                                            
0470.00     </script>                                                           
0471.00   </body>                                                               
0472.00 </html>                                                                 

[解説]

このHTMLテンプレートは伝票型式のWeb画面の明細行の表示として
使われます。

0013.00     <title>$TEXT</title> 

=> $TEXTはPython適用業務のテキストに置換えられます。

0149.00             <span class="name">$COLHDG</span>                                                    
0150.00             <span class="value">                                                                       
0151.00               <input type="text" name="$FLD" value="{{values[0].get('$FLD', '')}}"                        
0152.00                   size="$SIZE" maxlength="$FLDLEN"   {{"readonly" if dspmod != "INPPTN" else ""}}$ALIGN>  
0153.00             </span>                                                                                    
0154.00           </label>                                                                                     

がフィールドの記述であり
$COLHDG = フィールドの欄見出し
$FLD  = フィールド名
$FLDLEN = フィールド長
$SIZE = フィールド長 + 1

に置換えられます。