2026年6月17日水曜日

FileMaker Gantt

ガント

 AssociatronやAtraの開発にも使っているFileMaker。
普段公開しているデモはJavaScript、Python、C++だけど、
FileMakerはJavaScriptのテストデータ送り込むための実験室のように使ってます。
僕の昼間の仕事(システム開発)はFMがメインです。



もちろん、AssociatronやAtra開発するときなんかも使ってます。ファイルが乱雑に整理されていないときは、FileMakerでフォルダ管理したりしてますからね。実験って、ログがめちゃくちゃ多いので・・・。何時の、どんなふるまいのログなのか人の頭じゃ思い出せないでしょ・・・JSONファイルの山なんです。
随分と前のログを確認する頻度も高いのでフォルダ管理は必須なんです。(そっちは非公開:そのうち・・)


今回のようなGantもFileMakerで作って管理しちゃう。
あんまり凝ったもの作っても実際使わないから、単純でシンプルなもの。
例えば、普通のソフト開発みたいに


1目的の定義Define the purpose
2利用者・使用場面の整理Identify users and use cases
3要件定義Requirements definition
4制約条件の確認Confirm constraints
5全体構成設計Overall architecture design
6データ設計Data design
7画面・操作設計UI / UX design
8処理フロー設計Process flow design
9モジュール設計Module design
10外部連携設計External integration design
11エラー・例外設計Error and exception handling design
12セキュリティ設計Security design
13ログ・監視設計Logging and monitoring design
14テスト設計Test design
15実装Implementation
16単体テストUnit testing
17結合テストIntegration testing
18運用テストOperational testing
19リリースRelease
20保守・改善Maintenance and improvement

みたいにも使ってるんですけど、これは昼間のシステム開発用。






AssociatronやAtraなどを管理する場合は、dirを並べる

Atra/
├── .vscode/
│ └── settings.json
├── __pycache__/
│ ├── lab_input.cpython-314.pyc
│ └── main.cpython-314.pyc
├── core/
│ ├── __pycache__/
│ │ ├── __init__.cpython-314.pyc
│ │ ├── action.cpython-314.pyc
│ │ ├── associatron.cpython-314.pyc
│ │ ├── attractor.cpython-314.pyc
│ │ ├── carry.cpython-314.pyc
│ │ ├── clock.cpython-314.pyc
│ │ ├── difference.cpython-314.pyc
│ │ ├── experience.cpython-314.pyc
│ │ ├── field_index.cpython-314.pyc
│ │ ├── field_log.cpython-314.pyc
│ │ ├── initial_state.cpython-314.pyc
│ │ ├── recall.cpython-314.pyc
│ │ └── state.cpython-314.pyc
│ ├── __init__.py
│ ├── action.py
│ ├── associatron.py
│ ├── attractor.py
│ ├── carry.py
│ ├── clock.py
│ ├── difference.py
│ ├── experience.py
│ ├── field_index.py
│ ├── field_log.py
│ ├── initial_state.py
│ ├── recall.py
│ └── state.py
├── data/
│ ├── .gitkeep
│ ├── atra_state.json
│ ├── experience_log.jsonl
│ ├── field_log.jsonl
│ └── view_snapshot.json
├── docs/
│ ├── architecture.mmd
│ ├── camera_portal_tracking_step03.md
│ ├── comment_rules.md
│ ├── design.md
│ └── file_roles.md
├── output/
│ ├── __pycache__/
│ │ ├── __init__.cpython-314.pyc
│ │ └── txtspeech.cpython-314.pyc
│ ├── __init__.py
│ └── txtspeech.py
├── sensors/
│ ├── __pycache__/
│ │ └── __init__.cpython-314.pyc
│ ├── __init__.py
│ ├── camera.py
│ └── microphone.py
├── .gitignore
├── camera_test.py
├── index.html
├── index_portal_camera.html
├── lab_input.py
├── main.py
├── main_camera_portal.py
├── README.md
└── requirements.txt


__pycache__/ Python が自動で作った実行キャッシュなんて含める必要ないけどね。

こんな感じで、まだファイルがどんどん増えていくんですけど、task名をディレクトリのファイル名にする。実は最初頃はRedmineみたいので進捗みたいな感じでやってたんだけど、
うちみたいにVS Codeで作業していると、


試しに作ったファイル
一時テスト
古い版
動いた版
修正版
backup
copy
v2
v3
final
final_fixed

みたいに、作業フォルダの中が増殖するんですよね。

それでRedmine止めて、とりあえず、dirぶち込んで、実際に開発した日だけ打ち込んでる状態。なので、予定無の実績のみの管理。Atraの研究は何処からか予算をもらって納期迫られて開発してるわけじゃないし、野良研究所は当てにならない予定より実績を優先します。codeの追加、変更、書直しがほとんどなので、予定は要らないという使い方。
日付BarをクリックするとTaskが現れます。ここがFMのDBになってる箇所。


(Win FMP20~)19は使えませんでした。


Webビューア自体は

なるべく短めに。

scriptで、Webビューアに入れるhtmlだけ貼っておきますね。

g_GanttHTML(グローバル・フィールド)に貼り付けるhtml

---------------html------------------

<!doctype html> <html lang="ja"> <head> <meta charset="utf-8"> <meta name="viewport" content="width=device-width,initial-scale=1"> <title>工程表</title> <style> /* ========================================================= 工程表(65日固定・PDF複数ページヘッダ繰り返し) - レーン割当:開始日順に「重ならない最上段レーンへ詰める」(完全自動) - ソート:顧客/現場(複数条件) - FileMaker連動:バークリックでTasksを開く(入力はFM側) - FMから受け取るJSON:baseStart/days/customers を __FM_INIT_JSON__ で注入 ========================================================= */ :root{ /* ===================================================== ★ 表示倍率(ここだけ触ればOK) 画面(Webビューア): 100% PDF(印刷) : 100% ===================================================== */ --zoom-screen: 1.08; --zoom-print: 1.00; --label-w: 220px; --row-h: 34px; --day-w: 22px; --grid: #cbd5e1; --major-grid: #64748b; --major-w: 2px; /* PDF用(印刷直前だけ JS で切替) */ --pdf-day-w-a4: 14px; --pdf-day-w-a3: 18px; --pdf-label-w-a4: 190px; --pdf-label-w-a3: 210px; /* ★土日色 */ --sun: #dc2626; /* 赤 */ --sat: #2563eb; /* 青 */ } /* ===== Base ===== */ body{ margin:0; padding:14px; font-family:"Meiryo","メイリオ","Hiragino Kaku Gothic ProN","Yu Gothic",system-ui,sans-serif; font-size:11px; line-height:1.4; background:#f3f4f6; /* ★画面(Webビューア)だけ 110% */ zoom: var(--zoom-screen); } .wrap{ background:#fff; border:1px solid #e5e7eb; border-radius:10px; overflow:hidden; box-shadow:0 1px 8px rgba(0,0,0,.06); } /* ===== Header ===== */ .head{ padding:10px 12px; border-bottom:1px solid #e5e7eb; font-weight:900; display:flex; gap:12px; align-items:center; flex-wrap:wrap; } .head small{ font-weight:700; color:#374151; } .ctrl{ display:flex; gap:8px; align-items:center; flex-wrap:wrap; } input[type="date"], select, input[type="text"], input[type="number"]{ padding:6px 8px; border:1px solid #e5e7eb; border-radius:8px; font-weight:800; background:#fff; } /* ヘッダー入力が無駄に広がらないよう固定 */ #startDate{ width:140px; } #custSortSel{ width:150px; } #siteSortSel{ width:140px; } #paperSel{ width:110px; } /* Buttons */ .btn{ border:1px solid #e5e7eb; border-radius:10px; padding:8px 12px; font-weight:800; cursor:pointer; display:inline-flex; align-items:center; gap:8px; user-select:none; } .btn:active{ transform: translateY(1px); } .btn-gray{ background:#e5e7eb; color:#111827; } .btn-orange{ background:#f97316; border-color:#fb923c; color:#fff; } .btn-blue{ background:#3b82f6; border-color:#60a5fa; color:#fff; } .btn-green{ background:#16a34a; border-color:#22c55e; color:#fff; } .btn-red{ background:#ef4444; border-color:#f87171; color:#fff; } .btn-mini{ padding:6px 8px; border-radius:10px; font-weight:900; font-size:11px; } /* ===== Gantt layout ===== */ .gantt{ display:grid; grid-template-columns: var(--label-w) 1fr; } .leftHead{ background:#f9fafb; border-right:1px solid #e5e7eb; padding:8px 10px; font-weight:900; } .topHead{ background:#f9fafb; border-bottom:1px solid #e5e7eb; overflow:auto; white-space:nowrap; } /* ===== Header cells(border方式)===== */ .days{ display:flex; width:max-content; } /* ★ヘッダを3段にするので高さを増やす */ .day{ width:var(--day-w); flex:0 0 var(--day-w); /* ★日付列を縮ませない */ height:44px; /* ★32→44 */ box-sizing:border-box; border-left:1px solid var(--grid); position:relative; } .day.major{ border-left: var(--major-w) solid var(--major-grid); } /* 上段:月(必要な日だけ表示) */ .day .m{ position:absolute; top:2px; left:4px; font-weight:900; font-size:10px; color:#374151; white-space:nowrap; } /* 中段:日(1〜31)…曜日の上に毎日表示 */ .day .dnum{ position:absolute; top:18px; left:4px; /* ★曜日の上に固定 */ font-weight:900; font-size:11px; color:#111827; line-height:1; white-space:nowrap; } /* 下段:曜日(10pt) */ .day .dow{ position:absolute; top:32px; left:4px; /* ★最下段に固定 */ font-weight:900; font-size:10pt; /* ★指定どおり */ line-height:1; color:#111827; white-space:nowrap; } /* 土日色(曜日と日を色付け) */ .day.sun .dow, .day.sun .dnum{ color: var(--sun); } .day.sat .dow, .day.sat .dnum{ color: var(--sat); } /* ===== Left body ===== */ .left{ border-right:1px solid #e5e7eb; } .section{ height:var(--row-h); display:flex; align-items:center; gap:8px; padding:0 10px; font-weight:900; background:#f3f4f6; border-bottom:1px solid #e5e7eb; } .section .name{ flex:1 1 auto; min-width:0; overflow:hidden; text-overflow:ellipsis; white-space:nowrap; } .section .tools{ display:flex; gap:6px; align-items:center; flex:0 0 auto; } /* ===== Right body ===== */ .right{ overflow:auto; } .gridRow{ position:relative; height:var(--row-h); border-bottom:1px solid #eef2f7; width:max-content; } .cells{ display:flex; height:100%; } .cell{ width:var(--day-w); flex:0 0 var(--day-w); /* ★日付列を縮ませない */ height:100%; box-sizing:border-box; border-left:1px solid var(--grid); } .cell.major{ border-left: var(--major-w) solid var(--major-grid); } .bar{ position:absolute; top:6px; height:22px; box-sizing:border-box; /* ★重要:padding込みで日付幅に収める。6/1〜6/2 が 6/3 に食い込まない */ border-radius:6px; color:#fff; font-weight:700; font-size:11px; display:flex; align-items:center; padding:0 6px; /* ★PDFでもはみ出しにくいよう少しだけ詰める */ white-space:nowrap; overflow:hidden; text-overflow:ellipsis; cursor:pointer; box-shadow:0 1px 4px rgba(0,0,0,.15); text-shadow:0 1px 2px rgba(0,0,0,.35); z-index:2; } /* ========================================================= ★ check==1 の時だけ comment をバー内に表示(現場名は消さない) - work:白(既存どおり) - comment:黒 / 9pt / メイリオ / バー幅いっぱい / …省略 ========================================================= */ .bar .barName{ flex:0 0 auto; max-width:55%; /* ★現場名が長すぎる時はここで抑える(必要なら調整) */ overflow:hidden; text-overflow:ellipsis; white-space:nowrap; } .bar .barComment{ flex:1 1 auto; width:100%; overflow:hidden; text-overflow:ellipsis; white-space:nowrap; font-family:"Meiryo","メイリオ","Hiragino Kaku Gothic ProN","Yu Gothic",system-ui,sans-serif; font-size:9pt; color:#000; text-shadow:none; } /* ===== 印刷(printRoot方式)===== */ @media print{ html, body{ -webkit-print-color-adjust: exact; print-color-adjust: exact; } @page { margin: 6mm; } body{ background:#fff; padding:0; /* ★PDF(印刷)だけ 115% */ zoom: var(--zoom-print); } body[data-printing="1"] .wrap{ display:none !important; } body[data-printing="1"] #printRoot{ display:block !important; } #printRoot{ display:block; } .pPage{ page-break-after: always; margin:0; } .pPage:last-child{ page-break-after: auto; } .pHead{ padding:6px 0 8px; font-weight:900; display:flex; justify-content:space-between; align-items:flex-end; gap:12px; } .pHead small{ font-weight:700; color:#374151; } .topHead, .right{ overflow: visible !important; } } /* PDF用紙切替 */ body[data-paper="A4"]{ --label-w: var(--pdf-label-w-a4); --day-w: var(--pdf-day-w-a4); } body[data-paper="A3"]{ --label-w: var(--pdf-label-w-a3); --day-w: var(--pdf-day-w-a3); } /* 印刷用DOMは通常は非表示 */ #printRoot{ display:none; } </style> </head> <body> <div class="wrap"> <div class="head"> <div> 工程表(65日) <small> </small> </div> <div class="ctrl"> <label style="font-weight:900;">開始日</label> <input type="date" id="startDate" value="2026-01-01"> <label style="font-weight:900; margin-left:6px;">taskソート</label> <select id="custSortSel" title="並び順"> <option value="none">なし(登録順)</option> <option value="custId">ID</option> <option value="custName">task名</option> <option value="custStart">開始日(最早)</option> <option value="custStartThenId">開始日→顧客ID</option> </select> <label style="font-weight:900; margin-left:6px;">workソート</label> <select id="siteSortSel" title="workの並び順(顧客内)"> <option value="none">なし(登録順)</option> <option value="siteName">work名</option> <option value="siteStart">work別開始日</option> <option value="siteStartThenName">work別開始日→work</option> </select> <label style="font-weight:900; margin-left:6px;">PDF</label> <select id="paperSel" title="PDF用紙"> <option value="A4">A4(横)</option> <option value="A3" selected>A3(横)</option> </select> <button id="apply" class="btn btn-blue">反映</button> <button id="pdfBtn" class="btn btn-orange">PDF</button> <span style="width:12px;"></span> <button id="resetData" class="btn btn-red" title="受信データを捨てて初期DATAに戻す(テスト用)">リセット</button> </div> </div> <div class="gantt"> <div class="leftHead">task</div> <div class="topHead" id="topHead"> <div class="days" id="days"></div> </div> <div class="left" id="left"></div> <div class="right" id="right"></div> </div> </div> <div id="printRoot"></div> <script> function fmCall(scriptName, obj){ if(!window.FileMaker) return; FileMaker.PerformScript(scriptName, JSON.stringify(obj)); } const FM_INIT = __FM_INIT_JSON__; const INIT_DATA = [ { customerId: "2", customer: "taskA", color: "#2563eb", sites: [ { taskId: 1, name:"workA", start:"2026-01-10", end:"2026-06-14", comment:"" }, { taskId: 2, name:"workA-2", start:"2026-01-28", end:"2026-06-08", comment:"" } ] }, { customerId: "3", customer: "design", color: "#16a34a", sites: [ { taskId: 3, name:"workBB", start:"2026-06-30", end:"2026-07-05", comment:"" } ] } ]; function deepClone(x){ return JSON.parse(JSON.stringify(x)); } let DATA = deepClone(INIT_DATA); let DAYS = 65; const toDate = s => { const [y,m,d]=String(s).split("-").map(Number); return new Date(y,m-1,d); }; const addDays = (d,n)=>{ const x=new Date(d); x.setDate(x.getDate()+n); return x; }; const diffDays = (a,b)=>Math.round((b-a)/(1000*60*60*24)); const pad2 = n => String(n).padStart(2,"0"); const toISO = d => `${d.getFullYear()}-${pad2(d.getMonth()+1)}-${pad2(d.getDate())}`; const monthJa = d => `${d.getMonth()+1}月`; const dayNum = d => String(d.getDate()); const dowJa = d => ["日","月","火","水","木","金","土"][d.getDay()]; const dayClass = d => (d.getDay()===0 ? "sun" : (d.getDay()===6 ? "sat" : "")); function isMajorDay(i, base){ const d = addDays(base, i); return (i === 0) || (d.getDay() === 1); } function earliestSiteTime(block){ let t = Infinity; for(const s of (block.sites || [])){ if(!s?.start) continue; const st = toDate(s.start).getTime(); if(st < t) t = st; } return t; } function cmpCustIdByValue(A, B){ const a = String(A ?? ""); const b = String(B ?? ""); const isNa = /^\d+$/.test(a), isNb = /^\d+$/.test(b); if(isNa && isNb) return Number(a) - Number(b); return a.localeCompare(b, "ja"); } function getSortedCustomerIndices(){ const mode = document.getElementById("custSortSel").value; const idx = DATA.map((_, i)=>i); const byId = (a,b)=> cmpCustIdByValue(DATA[a].customerId, DATA[b].customerId) || (a-b); const byName = (a,b)=> String(DATA[a].customer ?? "").localeCompare(String(DATA[b].customer ?? ""), "ja") || (a-b); const byStart = (a,b)=> (earliestSiteTime(DATA[a]) - earliestSiteTime(DATA[b])) || (a-b); if(mode === "custId") idx.sort(byId); else if(mode === "custName") idx.sort(byName); else if(mode === "custStart") idx.sort(byStart); else if(mode === "custStartThenId") idx.sort((a,b)=> byStart(a,b) || byId(a,b)); return idx; } function getSortedSiteIndices(block){ const mode = document.getElementById("siteSortSel").value; const sites = (block.sites || []); const idx = sites.map((_, i)=>i); const byName = (a,b)=> String(sites[a]?.name ?? "").localeCompare(String(sites[b]?.name ?? ""), "ja") || (a-b); const byStart = (a,b)=> { const A = sites[a]?.start ? toDate(sites[a].start).getTime() : Infinity; const B = sites[b]?.start ? toDate(sites[b].start).getTime() : Infinity; return (A - B) || (a-b); }; if(mode === "siteName") idx.sort(byName); else if(mode === "siteStart") idx.sort(byStart); else if(mode === "siteStartThenName") idx.sort((a,b)=> byStart(a,b) || byName(a,b)); return idx; } function packLanes(block, base, orderedIdx){ const sitesArr = (block.sites || []); const orderIdx = Array.isArray(orderedIdx) ? orderedIdx : sitesArr.map((_,i)=>i); const items = orderIdx.map((si) => { const s = sitesArr[si]; const st = toDate(s.start); const x0 = diffDays(base, st); const w0 = calcDurationDays(s); return { si, x0, w0, stTime: st.getTime() }; }); const laneEnds = []; const order = []; for(const it of items){ const start = it.x0; const end = it.x0 + it.w0; let lane = -1; for(let i=0;i<laneEnds.length;i++){ if(laneEnds[i] <= start){ lane = i; break; } } if(lane === -1){ lane = laneEnds.length; laneEnds.push(end); }else{ laneEnds[lane] = end; } order.push({ si: it.si, lane }); } return { order, lanesNeeded: laneEnds.length }; } function calcDurationDays(site){ try{ if(site?.start && site?.end){ const sd = toDate(site.start); const ed = toDate(site.end); return Math.max(1, diffDays(sd, ed) + 1); } }catch(e){} return Math.max(1, Number(site?.duration || 1)); } /* 追加:表示範囲に掛かるか判定 */ function isSiteVisible(site, base){ try{ if(!site?.start) return false; const s = toDate(site.start); const x0 = diffDays(base, s); const w0 = calcDurationDays(site); const leftCut = Math.max(0, x0); const rightCut = Math.min(DAYS, x0 + w0); return (rightCut - leftCut) > 0; }catch(e){ return false; } } function buildDaysEl(base){ const daysEl = document.createElement("div"); daysEl.className = "days"; for(let i=0;i<DAYS;i++){ const d = addDays(base, i); const cls = dayClass(d); const cell = document.createElement("div"); cell.className = "day" + (isMajorDay(i, base) ? " major" : "") + (cls ? (" " + cls) : ""); if(i === 0 || d.getDate() === 1){ const m = document.createElement("div"); m.className = "m"; m.textContent = monthJa(d); cell.appendChild(m); } const dn = document.createElement("div"); dn.className = "dnum"; dn.textContent = dayNum(d); cell.appendChild(dn); const dow = document.createElement("div"); dow.className = "dow"; dow.textContent = dowJa(d); cell.appendChild(dow); daysEl.appendChild(cell); } const endCap = document.createElement("div"); endCap.style.width = "1px"; endCap.style.borderLeft = "1px solid var(--grid)"; daysEl.appendChild(endCap); return daysEl; } function render(){ const startStr = document.getElementById("startDate").value; const base = toDate(startStr); /* ★重要:CSS変数(--day-w/--row-h)は body から読む(paper切替が body に付くため) */ const cs = getComputedStyle(document.body); const dayW = parseFloat(cs.getPropertyValue("--day-w")); const rowH = parseFloat(cs.getPropertyValue("--row-h")); const gridW = DAYS * dayW; const daysHost = document.getElementById("days"); const left = document.getElementById("left"); const right = document.getElementById("right"); daysHost.innerHTML = ""; left.innerHTML = ""; right.innerHTML = ""; daysHost.appendChild(buildDaysEl(base)); const custIdx = getSortedCustomerIndices(); custIdx.forEach((bi) => { const block = DATA[bi]; const siteIdx0 = getSortedSiteIndices(block); const siteIdx = siteIdx0.filter(si => { const site = (block.sites || [])[si]; return isSiteVisible(site, base); }); if(siteIdx.length === 0) return; const packed = packLanes(block, base, siteIdx); const lanesUsed = Math.max(1, packed.lanesNeeded); const blockH = lanesUsed * rowH; const sec = document.createElement("div"); sec.className = "section"; sec.style.height = blockH + "px"; const sw = document.createElement("div"); sw.style.width = "28px"; sw.style.height = "28px"; sw.style.borderRadius = "6px"; sw.style.background = block.color || "#2563eb"; sw.style.boxShadow = "0 1px 3px rgba(0,0,0,.15)"; const name = document.createElement("div"); name.className = "name"; name.textContent = block.customer; sec.appendChild(sw); sec.appendChild(name); left.appendChild(sec); const gridWrap = document.createElement("div"); gridWrap.style.width = gridW + "px"; gridWrap.style.height = blockH + "px"; gridWrap.style.position = "relative"; right.appendChild(gridWrap); const laneRows = []; for(let li=0; li<lanesUsed; li++){ const row = document.createElement("div"); row.className = "gridRow"; row.style.width = gridW + "px"; const cells = document.createElement("div"); cells.className = "cells"; for(let i=0;i<DAYS;i++){ const c = document.createElement("div"); c.className = "cell" + (isMajorDay(i, base) ? " major" : ""); cells.appendChild(c); } row.appendChild(cells); gridWrap.appendChild(row); laneRows.push(row); } const laneBySi = new Map(packed.order.map(x => [x.si, x.lane])); siteIdx.forEach((si) => { const site = (block.sites || [])[si]; if(!site) return; const laneIndex = laneBySi.get(si) ?? 0; const row = laneRows[Math.min(laneIndex, laneRows.length - 1)]; const s = toDate(site.start); const x0 = diffDays(base, s); const w0 = calcDurationDays(site); const leftCut = Math.max(0, x0); const rightCut = Math.min(DAYS, x0 + w0); const visibleDays = rightCut - leftCut; if (visibleDays <= 0) return; const bar = document.createElement("div"); bar.className="bar"; bar.style.left = (leftCut * dayW) + "px"; bar.style.width = (visibleDays * dayW) + "px"; bar.style.background = block.color || "#2563eb"; /* ====================================================== ★構造は変えず、バー内文字だけ差し替え workは必ず表示。check==1 の時だけ comment を追加表示。 ====================================================== */ bar.textContent = ""; const spName = document.createElement("span"); spName.className = "barName"; spName.textContent = String(site.name ?? ""); bar.appendChild(spName); if (Number(site.check) === 1) { const txt = String(site.comment ?? "").trim(); if (txt !== "") { bar.appendChild(document.createTextNode("\u00A0")); // ★スペース1個 const spCom = document.createElement("span"); spCom.className = "barComment"; spCom.textContent = txt; bar.appendChild(spCom); } } bar.addEventListener("click", (ev)=>{ ev.stopPropagation(); fmCall("Gantt_OpenTask", { taskId: site.taskId }); }); row.appendChild(bar); }); }); const topHead = document.getElementById("topHead"); topHead.scrollLeft = right.scrollLeft; } function buildPrintPages(){ const root = document.getElementById("printRoot"); root.innerHTML = ""; const startStr = document.getElementById("startDate").value; const base = toDate(startStr); const paper = document.getElementById("paperSel").value; const cs = getComputedStyle(document.body); const rowH = parseFloat(cs.getPropertyValue("--row-h")); const dayW = parseFloat(cs.getPropertyValue("--day-w")); const gridW = DAYS * dayW; const pageLimit = (paper === "A3") ? 1050 : 650; const makePage = (pageNo) => { const page = document.createElement("div"); page.className = "pPage"; const pWrap = document.createElement("div"); page.appendChild(pWrap); const ph = document.createElement("div"); ph.className = "pHead"; ph.innerHTML = ` <div> 工程表(${DAYS}日) <small>開始日 ${startStr} / page ${pageNo}</small> </div> <div style="font-weight:900; color:#111827;">PDF</div> `; pWrap.appendChild(ph); const g = document.createElement("div"); g.className = "gantt"; pWrap.appendChild(g); const lh = document.createElement("div"); lh.className = "leftHead"; lh.textContent = "task"; g.appendChild(lh); const th = document.createElement("div"); th.className = "topHead"; th.appendChild(buildDaysEl(base)); g.appendChild(th); const left = document.createElement("div"); left.className = "left"; g.appendChild(left); const right = document.createElement("div"); right.className = "right"; g.appendChild(right); return { page, left, right, gridW, rowH, dayW }; }; let pageNo = 1; let cur = makePage(pageNo); root.appendChild(cur.page); let usedH = 0; const custIdx = getSortedCustomerIndices(); for(const bi of custIdx){ const block = DATA[bi]; const siteIdx0 = getSortedSiteIndices(block); const siteIdx = siteIdx0.filter(si => { const site = (block.sites || [])[si]; return isSiteVisible(site, base); }); if(siteIdx.length === 0) continue; const packed = packLanes(block, base, siteIdx); const lanesUsed = Math.max(1, packed.lanesNeeded); const blockH = lanesUsed * cur.rowH; if(usedH > 0 && usedH + blockH > pageLimit){ pageNo++; cur = makePage(pageNo); root.appendChild(cur.page); usedH = 0; } usedH += blockH; const sec = document.createElement("div"); sec.className = "section"; sec.style.height = blockH + "px"; const sw = document.createElement("div"); sw.style.width = "28px"; sw.style.height = "28px"; sw.style.borderRadius = "6px"; sw.style.background = block.color || "#2563eb"; sw.style.boxShadow = "0 1px 3px rgba(0,0,0,.15)"; const nm = document.createElement("div"); nm.className = "name"; nm.textContent = block.customer; sec.appendChild(sw); sec.appendChild(nm); cur.left.appendChild(sec); const gridWrap = document.createElement("div"); gridWrap.style.width = cur.gridW + "px"; gridWrap.style.height = blockH + "px"; gridWrap.style.position = "relative"; cur.right.appendChild(gridWrap); const laneRows = []; for(let li=0; li<lanesUsed; li++){ const row = document.createElement("div"); row.className = "gridRow"; row.style.width = cur.gridW + "px"; const cells = document.createElement("div"); cells.className = "cells"; for(let i=0;i<DAYS;i++){ const c = document.createElement("div"); c.className = "cell" + (isMajorDay(i, base) ? " major" : ""); cells.appendChild(c); } row.appendChild(cells); gridWrap.appendChild(row); laneRows.push(row); } const laneBySi = new Map(packed.order.map(x => [x.si, x.lane])); siteIdx.forEach((si)=>{ const site = (block.sites || [])[si]; if(!site) return; const laneIndex = laneBySi.get(si) ?? 0; const row = laneRows[Math.min(laneIndex, laneRows.length - 1)]; const s = toDate(site.start); const x0 = diffDays(base, s); const w0 = calcDurationDays(site); const leftCut = Math.max(0, x0); const rightCut = Math.min(DAYS, x0 + w0); const visibleDays = rightCut - leftCut; if (visibleDays <= 0) return; const bar = document.createElement("div"); bar.className = "bar"; bar.style.left = (leftCut * cur.dayW) + "px"; bar.style.width = (visibleDays * cur.dayW) + "px"; bar.style.background = block.color || "#2563eb"; /* ★印刷側も同じ(work+check==1でcomment追加) */ bar.textContent = ""; const spName = document.createElement("span"); spName.className = "barName"; spName.textContent = String(site.name ?? ""); bar.appendChild(spName); if (Number(site.check) === 1) { const txt = String(site.comment ?? "").trim(); if (txt !== "") { bar.appendChild(document.createTextNode("\u00A0")); // ★スペース1個 const spCom = document.createElement("span"); spCom.className = "barComment"; spCom.textContent = txt; bar.appendChild(spCom); } } row.appendChild(bar); }); } } const originalTitle = document.title; function nowStamp(){ const d = new Date(); const pad2 = n => String(n).padStart(2,"0"); return `${d.getFullYear()}${pad2(d.getMonth()+1)}${pad2(d.getDate())}${pad2(d.getHours())}${pad2(d.getMinutes())}`; } window.addEventListener("beforeprint", ()=>{ document.title = `工程表${nowStamp()}`; }); window.addEventListener("afterprint", ()=>{ document.title = originalTitle; document.body.dataset.printing = "0"; document.getElementById("printRoot").innerHTML = ""; }); document.getElementById("pdfBtn").addEventListener("click", ()=>{ const paper = document.getElementById("paperSel").value; document.body.dataset.paper = paper; /* ★paper切替直後のCSS再計算待ち(ズレ防止) */ requestAnimationFrame(() => { render(); buildPrintPages(); document.body.dataset.printing = "1"; window.print(); setTimeout(()=>{ delete document.body.dataset.paper; render(); }, 0); }); }); document.getElementById("apply").addEventListener("click", render); document.getElementById("custSortSel").addEventListener("change", render); document.getElementById("siteSortSel").addEventListener("change", render); document.getElementById("resetData").addEventListener("click", ()=>{ const ok = confirm("表示を空に戻します(FMデータは変えません)。よろしいですか?"); if(!ok) return; DATA = []; DAYS = 65; render(); }); const topHead = document.getElementById("topHead"); const right = document.getElementById("right"); right.addEventListener("scroll", ()=> topHead.scrollLeft = right.scrollLeft); topHead.addEventListener("scroll", ()=> right.scrollLeft = topHead.scrollLeft); (function applyFmInit(){ try{ if(!FM_INIT || typeof FM_INIT !== "object") { render(); return; } if(FM_INIT.baseStart){ document.getElementById("startDate").value = FM_INIT.baseStart; } if(Number.isFinite(FM_INIT.days)){ DAYS = Number(FM_INIT.days); } if(Array.isArray(FM_INIT.customers)){ DATA = FM_INIT.customers; } }catch(e){ console.error(e); } render(); })(); </script> </body> </html>

----------------------------------





FileMaker側のscript 元々は顧客別、現場管理の依頼で作ったものなので、スクリプト名の残存としてcustomerなどとありますが この辺はtaskに修正しても、修正しなくても動きます。 用途によって変えればいいかと思います。

API, J-Queryや他のツールなどは使用していません。



ただ、FMのAI機能の追加とか、よく分からない。
どちらかというと、レコードがちゃがちゃやってるFMが好きなのに・・・って感じかな。
JSONデータ元にAI呼ぶならまだわかるけど、がちゃがちゃしてAI?なのか・・・・
ん~分からないw





0 件のコメント:

コメントを投稿

Atra Emotions_Conditions 感情・状態

 -----------------C++------------------ struct EmotionsConditionsNow { // Unpredictability double input_irregularity_now = 0.0 ; ...