Corin1998 commited on
Commit
71b1041
·
verified ·
1 Parent(s): 1cec5f7

Update static/index.html

Browse files
Files changed (1) hide show
  1. static/index.html +223 -260
static/index.html CHANGED
@@ -1,277 +1,240 @@
1
  <!doctype html>
2
  <html lang="ja">
3
- <head>
4
- <meta charset="utf-8" />
5
- <title>Mini Invoice かんたん作成</title>
6
- <meta name="viewport" content="width=device-width, initial-scale=1" />
7
- <style>
8
- body { font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, "Noto Sans JP", Arial, "Hiragino Kaku Gothic ProN", "Meiryo", sans-serif; margin: 24px; }
9
- h1 { margin: 0 0 8px; }
10
- .row { display: flex; gap: 16px; flex-wrap: wrap; }
11
- .card { border: 1px solid #e5e7eb; border-radius: 12px; padding: 16px; flex: 1; min-width: 320px; background: #fff; box-shadow: 0 2px 8px rgba(0,0,0,.04); }
12
- label { display:block; font-size: 12px; color:#374151; margin-top: 8px; }
13
- input, select, textarea { width: 100%; padding: 10px; border: 1px solid #d1d5db; border-radius: 8px; font-size: 14px; }
14
- table { width: 100%; border-collapse: collapse; }
15
- th, td { border-bottom: 1px solid #eee; padding: 8px; font-size: 14px; }
16
- tfoot td { font-weight: bold; }
17
- .btn { padding: 10px 14px; border: 1px solid #0ea5e9; background: #0ea5e9; color:#fff; border-radius: 8px; cursor: pointer; }
18
- .btn.alt { background:#fff; color:#0ea5e9; }
19
- .btn:disabled{ opacity: .6; cursor: not-allowed; }
20
- .right { text-align: right; }
21
- .muted { color:#6b7280; font-size: 13px; }
22
- .ok { color:#059669; }
23
- .err { color:#dc2626; }
24
- .toolbar { display:flex; gap:8px; align-items:center; justify-content: space-between; margin-bottom: 16px; }
25
- .pill { border:1px solid #d1d5db; border-radius: 999px; padding: 4px 10px; }
26
- .list { max-height: 220px; overflow:auto; border:1px solid #eee; border-radius:8px; padding:8px; }
27
- .list-item{ padding:6px 8px; border-radius:6px; cursor:pointer; }
28
- .list-item:hover{ background:#f3f4f6; }
29
- .footer { margin-top: 16px; font-size: 12px; color:#6b7280; }
30
- code { background:#f3f4f6; padding:2px 6px; border-radius:6px; }
31
- </style>
32
- </head>
33
  <body>
34
- <div class="toolbar">
35
- <div>
36
- <h1>かんたん請求書作成</h1>
37
- <div class="muted">顧客の入力 明細入力 「一括で作成」で、顧客・請求書・明細をまめて登録しま</div>
 
 
 
 
 
 
 
 
 
 
 
 
 
 
38
  </div>
39
- <div>
40
- <span class="muted">API Key</span>
41
- <input id="apiKey" placeholder="dev" style="width:180px; margin-left:6px;">
42
- <button class="btn alt" id="saveKey">保存</button>
 
 
 
 
 
43
  </div>
 
 
44
  </div>
45
 
46
- <div class="row">
47
- <!-- 顧客 -->
48
- <div class="card" style="max-width:520px">
49
- <h3>顧客</h3>
50
- <div class="muted">既存顧客を選ぶ or 新規で入力</div>
51
- <label>既存顧客を検索</label>
52
- <input id="searchQ" placeholder="社名・メール・電話">
53
- <div class="toolbar" style="margin-top:8px">
54
- <button class="btn alt" id="btnSearch">一覧更新</button>
55
- <span class="muted" id="custCount"></span>
56
- </div>
57
- <div class="list" id="custList"></div>
58
-
59
- <div style="margin-top:12px; border-top:1px dashed #e5e7eb; padding-top:12px">
60
- <div class="muted">新規作成する場合はこちらに入力</div>
61
- <label>会社*</label>
62
- <input id="c_name" placeholder="株式会社〇〇">
63
- <div class="row">
64
- <div style="flex:1">
65
- <label>メール</label><input id="c_email" placeholder="info@example.com">
66
- </div>
67
- <div style="flex:1">
68
- <label>電話</label><input id="c_phone" placeholder="03-xxxx-xxxx">
69
- </div>
70
  </div>
71
- <label>住所</label><input id="c_address" placeholder="東京都〇〇…">
72
- <div class="row">
73
- <div style="flex:1"><label>市区町村</label><input id="c_city"></div>
74
- <div style="flex:1"><label>国</label><input id="c_country" placeholder="JP"></div>
75
- </div>
76
- <div class="footer">※ 既存顧客をクリックすると右上に <code>customer_id</code> として選択されます。</div>
77
- <div class="muted">選択中の customer_id: <span id="customerId" class="pill">未選択</span></div>
78
  </div>
79
- </div>
80
-
81
- <!-- 明細+請求 -->
82
- <div class="card">
83
- <h3>明細</h3>
84
- <table id="itemsTbl">
85
- <thead><tr><th>内容</th><th style="width:90px">数量</th><th style="width:130px">単価</th><th style="width:110px">税率</th><th style="width:40px"></th></tr></thead>
86
- <tbody></tbody>
87
- <tfoot>
88
- <tr><td colspan="5"><button class="btn alt" id="addRow">+ 行を追加</button></td></tr>
89
- </tfoot>
90
- </table>
91
 
92
- <div class="row" style="margin-top:12px">
93
- <div class="card" style="flex:1">
94
- <h4>請求情報</h4>
95
- <label>支払期日</label><input id="due" type="date">
96
- <label>備考</label><textarea id="notes" rows="3" placeholder="いつもお世話になっております…"></textarea>
97
- <div class="row" style="margin-top:8px">
98
- <button class="btn" id="btnCreate">一括で作成</button>
99
- <button class="btn alt" id="btnAi">AI文面(件名+本文)</button>
100
- </div>
101
- <div id="msg" style="margin-top:6px"></div>
102
- </div>
103
- <div class="card" style="flex:1">
104
- <h4>合計</h4>
105
- <div class="row">
106
- <div class="right" style="flex:1">
107
- 小計 <div id="sum_sub">0</div>
108
- 税額 <div id="sum_tax">0</div>
109
- 合計 <div id="sum_total" style="font-size:20px">0</div>
110
- </div>
111
- </div>
112
- </div>
113
  </div>
 
114
 
115
- <div style="margin-top:8px" class="muted">
116
- 作成後:PDFダウンロードは <code>GET /invoices/{id}/pdf</code>、
117
- メール送信は <code>POST /invoices/{id}/email</code> を使えます。
118
- </div>
119
  </div>
120
  </div>
121
-
122
- <script>
123
- const BASE = location.origin;
124
- const LSKEY = "mini_invoice_api_key";
125
- const apiKeyEl = document.getElementById('apiKey');
126
- apiKeyEl.value = localStorage.getItem(LSKEY) || "dev";
127
- document.getElementById('saveKey').onclick = () => {
128
- localStorage.setItem(LSKEY, apiKeyEl.value.trim());
129
- alert("API Key を保存しました");
130
- };
131
-
132
- // 小ユーティリティ
133
- const h = (tag, attrs={}, children=[]) => {
134
- const el = document.createElement(tag);
135
- Object.entries(attrs).forEach(([k,v]) => (k in el ? el[k]=v : el.setAttribute(k,v)));
136
- (Array.isArray(children) ? children : [children]).forEach(c => c && el.append(c));
137
- return el;
138
- };
139
- const headers = () => ({ "Content-Type":"application/json", "X-API-Key": (localStorage.getItem(LSKEY)||"dev") });
140
-
141
- // 顧客一覧
142
- const custList = document.getElementById('custList');
143
- const custCount = document.getElementById('custCount');
144
- const customerIdEl = document.getElementById('customerId');
145
-
146
- async function loadCustomers() {
147
- const q = document.getElementById('searchQ').value.trim();
148
- const url = new URL(BASE + "/customers");
149
- if (q) url.searchParams.set("q", q);
150
- const res = await fetch(url, { headers: headers() });
151
- if (!res.ok) { custList.innerHTML = "読み込みエラー"; return; }
152
- const js = await res.json();
153
- custCount.textContent = (js.pagination?.total ?? js.data?.length ?? 0) + " 件";
154
- custList.innerHTML = "";
155
- (js.data || []).forEach(row => {
156
- const div = h("div", {className:"list-item"}, [
157
- h("div", {innerHTML:`<b>${row.name}</b> <span class="muted">#${row.id}</span>`}),
158
- h("div", {className:"muted", innerText: [row.email,row.phone].filter(Boolean).join(" / ")})
159
- ]);
160
- div.onclick = () => {
161
- customerIdEl.textContent = row.id;
162
- customerIdEl.classList.add("ok");
163
- // 新規入力欄をクリア
164
- ["c_name","c_email","c_phone","c_address","c_city","c_country"].forEach(id => document.getElementById(id).value="");
165
- };
166
- custList.append(div);
167
- });
168
- }
169
- document.getElementById('btnSearch').onclick = loadCustomers;
170
- loadCustomers();
171
-
172
- // 明細テーブル
173
- const tbody = document.querySelector('#itemsTbl tbody');
174
- function addRow(v={description:"", quantity:1, unit_price:0, tax_rate:0.1}) {
175
- const tr = h("tr");
176
- tr.append(
177
- h("td", {}, h("input",{value:v.description, placeholder:"内容", oninput:recalc})),
178
- h("td", {}, h("input",{type:"number", step:"0.01", value:v.quantity, oninput:recalc})),
179
- h("td", {}, h("input",{type:"number", step:"0.01", value:v.unit_price, oninput:recalc})),
180
- h("td", {}, h("input",{type:"number", step:"0.01", value:v.tax_rate, oninput:recalc})),
181
- h("td", {}, h("button",{className:"btn alt", innerText:"×", onclick:()=>{ tr.remove(); recalc(); }}))
182
- );
183
- tbody.append(tr);
184
- recalc();
185
- }
186
- document.getElementById('addRow').onclick = () => addRow();
187
- addRow({description:"Webサイト制作一式", quantity:1, unit_price:300000, tax_rate:0.1});
188
-
189
- function recalc(){
190
- let sub=0, tax=0;
191
- [...tbody.querySelectorAll('tr')].forEach(tr=>{
192
- const [dEl,qEl,uEl,tEl] = tr.querySelectorAll('input');
193
- const q = parseFloat(qEl.value||"0"), u = parseFloat(uEl.value||"0"), r = parseFloat(tEl.value||"0");
194
- const line = q*u; sub += line; tax += line*r;
195
- });
196
- document.getElementById('sum_sub').innerText = sub.toFixed(0);
197
- document.getElementById('sum_tax').innerText = tax.toFixed(0);
198
- document.getElementById('sum_total').innerText = (sub+tax).toFixed(0);
199
- }
200
-
201
- // 一括作成
202
- const msg = document.getElementById('msg');
203
- function uiMsg(text, ok=false){ msg.innerHTML = `<span class="${ok?'ok':'err'}">${text}</span>`; }
204
-
205
- document.getElementById('btnCreate').onclick = async () => {
206
- const cidText = customerIdEl.textContent;
207
- let cust = {};
208
- if (cidText && cidText !== "未選択") {
209
- cust.id = parseInt(cidText);
210
- } else {
211
- const name = document.getElementById('c_name').value.trim();
212
- if (!name) { uiMsg("会社名を入力するか、既存顧客を選択してください"); return; }
213
- cust = {
214
- name,
215
- email: document.getElementById('c_email').value.trim() || null,
216
- phone: document.getElementById('c_phone').value.trim() || null,
217
- address: document.getElementById('c_address').value.trim() || null,
218
- city: document.getElementById('c_city').value.trim() || null,
219
- country: document.getElementById('c_country').value.trim() || null
220
- };
221
- }
222
- const items = [...tbody.querySelectorAll('tr')].map(tr=>{
223
- const [dEl,qEl,uEl,tEl] = tr.querySelectorAll('input');
224
- return {
225
- description: dEl.value.trim(),
226
- quantity: parseFloat(qEl.value||"0"),
227
- unit_price: parseFloat(uEl.value||"0"),
228
- tax_rate: parseFloat(tEl.value||"0"),
229
- };
230
- }).filter(it=>it.description && it.quantity>0);
231
-
232
- if (items.length===0){ uiMsg("明細を1行以上入力してください"); return; }
233
-
234
- const payload = {
235
- customer: cust,
236
- due_date: document.getElementById('due').value || null,
237
- notes: document.getElementById('notes').value || null,
238
- items
239
- };
240
-
241
- const res = await fetch(BASE + "/wizard/invoice", {
242
- method:"POST", headers: headers(), body: JSON.stringify(payload)
243
- });
244
- if (!res.ok){
245
- const t = await res.text();
246
- uiMsg("作成に失敗: " + t);
247
- return;
248
- }
249
- const js = await res.json();
250
- uiMsg(`作成しました。invoice_id=${js.invoice_id} 合計=${js.totals.total}`, true);
251
-
252
- // PDF直リンクとメール送信用テンプレ表示
253
- const pdf = `${BASE}/invoices/${js.invoice_id}/pdf`;
254
- msg.innerHTML += `<div style="margin-top:8px"><a class="btn alt" href="${pdf}" target="_blank">PDFを開く</a></div>`;
255
- };
256
-
257
- // AI 文面(件名+本文)
258
- document.getElementById('btnAi').onclick = async () => {
259
- const cidText = customerIdEl.textContent;
260
- const company_name = "(あなたの会社名)";
261
- const customer_name = cidText && cidText!=="未選択" ? `顧客ID #${cidText}` : (document.getElementById('c_name').value||"御中");
262
- const items = [...tbody.querySelectorAll('tr')].map(tr=>{
263
- const [dEl,qEl,uEl,tEl] = tr.querySelectorAll('input');
264
- return { description: dEl.value, quantity: parseFloat(qEl.value||"0"), unit_price: parseFloat(uEl.value||"0"), tax_rate: parseFloat(tEl.value||"0") };
265
- });
266
- const due = document.getElementById('due').value || null;
267
- const res = await fetch(BASE + "/ai/generate-email", {
268
- method:"POST", headers: headers(),
269
- body: JSON.stringify({ kind:"invoice", company_name, customer_name, language:"ja", items, due_date: due, tone:"polite" })
270
- });
271
- if (!res.ok){ uiMsg("AI文面生成に失敗しました: " + await res.text()); return; }
272
- const js = await res.json();
273
- alert(js.email);
274
  };
275
- </script>
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
276
  </body>
277
  </html>
 
1
  <!doctype html>
2
  <html lang="ja">
3
+ <meta charset="utf-8">
4
+ <title>かんたん請求書作成</title>
5
+ <meta name="viewport" content="width=device-width, initial-scale=1">
6
+ <style>
7
+ body{font-family:system-ui,-apple-system,"Noto Sans JP",sans-serif;margin:24px}
8
+ h1{font-size:28px;margin-bottom:12px}
9
+ .row{display:flex;gap:16px;flex-wrap:wrap}
10
+ .card{border:1px solid #e5e7eb;border-radius:14px;padding:16px;flex:1;min-width:320px;background:#fff}
11
+ label{display:block;font-size:12px;color:#374151;margin-top:8px}
12
+ input,textarea{width:100%;padding:10px;border:1px solid #d1d5db;border-radius:8px}
13
+ table{width:100%;border-collapse:collapse}
14
+ th,td{border-bottom:1px solid #eee;padding:8px;text-align:left}
15
+ .btn{padding:10px 14px;border-radius:8px;border:1px solid #0ea5e9;background:#0ea5e9;color:#fff;cursor:pointer}
16
+ .btn.secondary{background:#fff;color:#0ea5e9}
17
+ .muted{color:#6b7280;font-size:12px}
18
+ .right{ text-align:right }
19
+ </style>
 
 
 
 
 
 
 
 
 
 
 
 
 
20
  <body>
21
+ <h1>かんたん請求書作成</h1>
22
+ <div style="margin-bottom:10px">
23
+ API Key <input id="apiKey" style="width:160px" placeholder="dev"> <button class="btn secondary" id="saveKey">保存</button>
24
+ <span class="muted" style="margin-left:8px"> /docsAuthorize同じキーで(既定: dev)</span>
25
+ </div>
26
+
27
+ <div class="row">
28
+ <div class="card" style="max-width:520px">
29
+ <h3>顧客</h3>
30
+ <label>既存顧客を検索</label>
31
+ <input id="q" placeholder="社名・メール・電話">
32
+ <div style="margin-top:6px"><button class="btn secondary" id="refresh">一覧更新</button></div>
33
+ <div id="custErr" class="muted" style="margin-top:6px;color:#ef4444"></div>
34
+ <div style="max-height:180px;overflow:auto;margin-top:6px;border:1px solid #eee;border-radius:8px">
35
+ <table>
36
+ <thead><tr><th>ID</th><th>社名</th><th>連絡先</th><th></th></tr></thead>
37
+ <tbody id="custTb"></tbody>
38
+ </table>
39
  </div>
40
+
41
+ <div class="muted" style="margin-top:12px">新規作成する場合はこちらに入力</div>
42
+ <label>会社名*</label><input id="c_name">
43
+ <label>メール</label><input id="c_email">
44
+ <label>電話</label><input id="c_phone">
45
+ <label>住所</label><input id="c_addr">
46
+ <div class="row">
47
+ <div style="flex:1"><label>市区町村</label><input id="c_city"></div>
48
+ <div style="flex:1"><label>国</label><input id="c_country" placeholder="JP"></div>
49
  </div>
50
+ <div class="muted" style="margin-top:6px">※ 既存顧客をクリックすると上記 <code>customer_id</code> として選択されます。</div>
51
+ <div id="chosen" class="muted" style="margin-top:6px">選択中の customer_id: <b id="cid">(未選択)</b></div>
52
  </div>
53
 
54
+ <div class="card">
55
+ <h3>明細</h3>
56
+ <table>
57
+ <thead><tr><th>内容</th><th class="right">数量</th><th class="right">単価</th><th class="right">税率</th><th></th></tr></thead>
58
+ <tbody id="items"></tbody>
59
+ </table>
60
+ <div style="margin-top:6px"><button class="btn secondary" id="add">+ 行を追加</button></div>
61
+
62
+ <div class="row" style="margin-top:16px">
63
+ <div class="card" style="min-width:280px">
64
+ <h4>請求情報</h4>
65
+ <label>支払期日</label><input type="date" id="due">
66
+ <label>備考</label><textarea id="notes" rows="4" placeholder="いつもお世話になっております…"></textarea>
67
+ <div style="margin-top:10px;display:flex;gap:8px;flex-wrap:wrap">
68
+ <button class="btn" id="btnCreate">一括で作成</button>
69
+ <button class="btn secondary" id="btnAi">AI文面(件+本文)</button>
 
 
 
 
 
 
 
 
70
  </div>
71
+ <div id="msg" class="muted" style="margin-top:8px;color:#ef4444"></div>
 
 
 
 
 
 
72
  </div>
 
 
 
 
 
 
 
 
 
 
 
 
73
 
74
+ <div class="card" style="min-width:260px">
75
+ <h4>合計</h4>
76
+ <div>小計 <span id="sub" class="right" style="float:right">0</span></div>
77
+ <div>税額 <span id="tax" class="right" style="float:right">0</span></div>
78
+ <div style="font-weight:700">合計 <span id="tot" class="right" style="float:right">0</span></div>
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
79
  </div>
80
+ </div>
81
 
82
+ <div class="muted" style="margin-top:8px">
83
+ 作成後:PDFダウンロードは <code>GET /invoices/{id}/pdf</code>、メール送信は <code>POST /invoices/{id}/email</code>。
 
 
84
  </div>
85
  </div>
86
+ </div>
87
+
88
+ <script>
89
+ const BASE = location.origin;
90
+ const LSKEY = "mini_invoice_api_key";
91
+
92
+ const apiKeyEl = document.getElementById('apiKey');
93
+ apiKeyEl.value = localStorage.getItem(LSKEY) || "dev";
94
+ document.getElementById('saveKey').onclick = () => {
95
+ localStorage.setItem(LSKEY, apiKeyEl.value.trim());
96
+ alert("API Key を保存しました");
97
+ loadCustomers();
98
+ };
99
+
100
+ const headers = () => ({
101
+ "Content-Type": "application/json",
102
+ "X-API-Key": (localStorage.getItem(LSKEY) || "dev")
103
+ });
104
+
105
+ // 日付を ISO に整形
106
+ const fmtDate = (s) => {
107
+ if (!s) return null;
108
+ const t = String(s).trim().replace(/\//g,"-");
109
+ const d = new Date(t);
110
+ if (isNaN(d)) return null;
111
+ return d.toISOString().slice(0,10);
112
+ };
113
+
114
+ let selectedCustomerId = null;
115
+ const cidEl = document.getElementById('cid');
116
+ const msgEl = document.getElementById('msg');
117
+
118
+ function rowItem(desc="", qty=1, price=0, tax=0.1){
119
+ const tr = document.createElement('tr');
120
+ tr.innerHTML = `
121
+ <td><input class="d" value="${desc}"></td>
122
+ <td class="right"><input class="q" type="number" step="0.01" value="${qty}" style="width:90px"></td>
123
+ <td class="right"><input class="p" type="number" step="1" value="${price}" style="width:110px"></td>
124
+ <td class="right"><input class="t" type="number" step="0.01" value="${tax}" style="width:90px"></td>
125
+ <td><button class="btn secondary del">×</button></td>
126
+ `;
127
+ tr.querySelector('.del').onclick = () => { tr.remove(); recalc(); };
128
+ return tr;
129
+ }
130
+
131
+ function recalc(){
132
+ const rows = [...document.querySelectorAll('#items tr')];
133
+ let sub = 0, tax = 0;
134
+ rows.forEach(r=>{
135
+ const q = parseFloat(r.querySelector('.q').value || 0);
136
+ const p = parseFloat(r.querySelector('.p').value || 0);
137
+ const t = parseFloat(r.querySelector('.t').value || 0);
138
+ const line = q*p; sub += line; tax += line*t;
139
+ });
140
+ document.getElementById('sub').innerText = Math.round(sub).toLocaleString();
141
+ document.getElementById('tax').innerText = Math.round(tax).toLocaleString();
142
+ document.getElementById('tot').innerText = Math.round(sub+tax).toLocaleString();
143
+ }
144
+
145
+ document.getElementById('add').onclick = ()=>{
146
+ const tr = rowItem("Webサイト制作一式", 1, 300000, 0.1);
147
+ document.getElementById('items').append(tr);
148
+ recalc();
149
+ };
150
+ document.getElementById('add').click(); // 初期1行
151
+
152
+ async function loadCustomers(){
153
+ const q = document.getElementById('q').value.trim();
154
+ const url = new URL(BASE + "/customers");
155
+ if(q) url.searchParams.set("q", q);
156
+ const res = await fetch(url, {headers:headers()});
157
+ const tb = document.getElementById('custTb'); tb.innerHTML="";
158
+ if(!res.ok){
159
+ document.getElementById('custErr').innerText = "読み込みエラー: " + (await res.text());
160
+ return;
161
+ }
162
+ document.getElementById('custErr').innerText = "";
163
+ const js = await res.json();
164
+ (js.data||[]).forEach(c=>{
165
+ const tr = document.createElement('tr');
166
+ tr.innerHTML = `<td>${c.id}</td><td>${c.name}</td><td>${[c.email,c.phone].filter(Boolean).join(" / ")}</td><td><button class="btn secondary">選択</button></td>`;
167
+ tr.querySelector('button').onclick = ()=>{
168
+ selectedCustomerId = c.id; cidEl.innerText = c.id;
169
+ document.getElementById('c_name').value = c.name||"";
170
+ document.getElementById('c_email').value = c.email||"";
171
+ document.getElementById('c_phone').value = c.phone||"";
172
+ document.getElementById('c_addr').value = c.address||"";
173
+ document.getElementById('c_city').value = c.city||"";
174
+ document.getElementById('c_country').value = c.country||"";
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
175
  };
176
+ tb.append(tr);
177
+ });
178
+ }
179
+ document.getElementById('refresh').onclick = loadCustomers;
180
+ loadCustomers();
181
+
182
+ // 一括作成
183
+ document.getElementById('btnCreate').onclick = async ()=>{
184
+ msgEl.style.color = "#ef4444";
185
+ const items = [...document.querySelectorAll('#items tr')].map(r=>({
186
+ description: r.querySelector('.d').value,
187
+ quantity: parseFloat(r.querySelector('.q').value||0),
188
+ unit_price: parseFloat(r.querySelector('.p').value||0),
189
+ tax_rate: parseFloat(r.querySelector('.t').value||0),
190
+ }));
191
+ const cust = selectedCustomerId ? {id:selectedCustomerId} : {
192
+ name: document.getElementById('c_name').value||null,
193
+ email: document.getElementById('c_email').value||null,
194
+ phone: document.getElementById('c_phone').value||null,
195
+ address: document.getElementById('c_addr').value||null,
196
+ city: document.getElementById('c_city').value||null,
197
+ country: document.getElementById('c_country').value||null,
198
+ };
199
+ const payload = {
200
+ customer: cust,
201
+ due_date: fmtDate(document.getElementById('due').value),
202
+ notes: document.getElementById('notes').value || null,
203
+ items
204
+ };
205
+ const res = await fetch(BASE + "/wizard/invoice", {
206
+ method:"POST", headers: headers(), body: JSON.stringify(payload)
207
+ });
208
+ if(!res.ok){
209
+ const t = await res.text();
210
+ msgEl.innerText = "作成に失敗: " + t;
211
+ return;
212
+ }
213
+ const js = await res.json();
214
+ msgEl.style.color = "#16a34a";
215
+ msgEl.innerText = `作成成功! invoice_id=${js.invoice_id} / 合計=${js.totals.total}`;
216
+ };
217
+
218
+ // AI 文面
219
+ document.getElementById('btnAi').onclick = async ()=>{
220
+ msgEl.style.color = "#ef4444";
221
+ const company_name = document.getElementById('c_name').value || "(社名)";
222
+ const customer_name = company_name + " 御中";
223
+ const items = [...document.querySelectorAll('#items tr')].map(r=>({
224
+ description: r.querySelector('.d').value,
225
+ quantity: parseFloat(r.querySelector('.q').value||0),
226
+ unit_price: parseFloat(r.querySelector('.p').value||0),
227
+ tax_rate: parseFloat(r.querySelector('.t').value||0),
228
+ }));
229
+ const due = fmtDate(document.getElementById('due').value);
230
+ const res = await fetch(BASE + "/ai/generate-email", {
231
+ method:"POST", headers: headers(),
232
+ body: JSON.stringify({ kind:"invoice", company_name, customer_name, language:"ja", items, due_date: due, tone:"polite" })
233
+ });
234
+ if(!res.ok){ msgEl.innerText = "AI文面生成に失敗しました: " + await res.text(); return; }
235
+ const js = await res.json();
236
+ alert(js.email);
237
+ };
238
+ </script>
239
  </body>
240
  </html>