ken_nogi/pleasanter/develop/#リフォームラボ/■イオン多摩平シフト管理/sites/scripts/2.予定の自動生成.js
Kenichiro NOGI 7176dc33ec pleasanter
2025-07-10 10:15:35 +09:00

355 lines
13 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

/*
@config : next999.json
@filename : ${psFileBasename}
@title : 2.予定の自動生成
@name :
@siteIds : 310511
@siteTitles : イオン多摩平シフト管理
@disabled: false
*/
async function createPrompotText(shiftData) {
if (totalCount > 0) {
window.alert('対象年月のシフトデータはすでに存在します。\nAIで生成し直す場合は、シフトをすべて削除してから実行してください。');
return false;
}
// 実行確認のポップアップ
if (!window.confirm('シフト希望に基づいて対象月のシフトを生成しますか?')) {
return false;
}
let selectMonth = $('#monthSelect').val();
let prompt = `
#指示
営業所の店長として、次の条件で勤務シフトを作成し、CSV形式で出力しなさい。
#営業時間
午前10時から午後9時 年中無休
#メンバーとそれぞれのシフト可能内容のCSVデータ
${shiftData}
#シフトの条件として下記のルールをすべて完璧に守って構成を作成しなさい。
下記に列挙したルールをすべて厳密に厳守してシフトを構成しなさい。
・営業時間午前10時から午後9時の各時間に必ず1名以上の勤務者がいること。
・開始時間は10から20までとすること。
・終了時間は11から21までとすること。
・開始時間は終了時間よりも必ず少ない数字にすること。
・同じ時間帯に同時に勤務するメンバーは2名までとすること。
・勤務時間は午前10時から午後9時の間で、1時間単位で設定すること。
・1日の勤務時間は4時間よりも多い場合は休憩時間を1時間とし、休憩時間は勤務時間の真ん中あたりに設定すること。
・休憩時間は必ず1時間とする。勤務時間が奇数の場合は、後半の勤務時間を1時間多くすること。
・休憩時間を除く勤務時間の合計は最大8時間までとすること。
・1日の勤務時間が4時間以下の場合は休憩時間を含めない。
・勤務可能日,勤務可能曜日,勤務可能時間帯よりも、「その他の条件」を最優先に考慮すること。
・勤務可能日,勤務可能曜日,勤務可能時間帯よりも、「その他の条件」を最優先に考慮すること。
・昼12時から13時には2名の勤務が必ず入るようにすること。
・同じメンバーが1日に2回以上の勤務時間が入らないようにすること。
・同じメンバーが5日以上連続して絶対に勤務しないこと。
・社員よりパートを優先的に配置し、パートで配置しきれない日は社員を配置するここと。
・欠員という名前のメンバーの種別は欠員とすること。
・対象年月のすべての日程の営業時間午前10時から午後9時において何も割り当てられていない時間帯が無いようにすること。
・他の勤務者がいる時間帯には欠員は配置しないこと。
・対象年月のすべての日程の営業時間午前10時から午後9時において、誰も勤務できない時間帯がある場合は欠員を割り当てること。
#対象年月
${selectMonth}
#出力形式
次のカラムで項目名を含めて構成されるCSV形式のみを出力すること。
時間部分には必ず整数を設定すること。
対象年月はyyyy年mm月の形式で出力すること。
対象年月,対象日付,種別,名前,開始時間,終了時間,勤務時間,休憩時間
`;
//console.log(prompt);
generateAndSaveSchedule(prompt);
/*
// ▼▼▼ プロンプトをテキストファイルでダウンロード ▼▼▼
const now = new Date();
const ymdhms = [
now.getFullYear(),
String(now.getMonth() + 1).padStart(2, '0'),
String(now.getDate()).padStart(2, '0'),
String(now.getHours()).padStart(2, '0'),
String(now.getMinutes()).padStart(2, '0'),
String(now.getSeconds()).padStart(2, '0')
].join('');
const promptFileName = `シフト自動生成プロンプト_${selectMonth}_${ymdhms}.txt`;
const promptBlob = new Blob([prompt], { type: 'text/plain' });
const promptUrl = URL.createObjectURL(promptBlob);
const promptA = document.createElement('a');
promptA.href = promptUrl;
promptA.download = promptFileName;
document.body.appendChild(promptA);
promptA.click();
setTimeout(() => {
document.body.removeChild(promptA);
URL.revokeObjectURL(promptUrl);
}, 100);
// ▲▲▲ プロンプトをテキストファイルでダウンロード ▲▲▲
*/
}
// モーダル表示用関数
function showLoadingModal(message = "AIでシフトを自動生成中です...") {
if ($('#gemini-loading-modal').length) return;
const modalHtml = `
<div id="gemini-loading-modal" style="
position: fixed; z-index: 9999; left: 0; top: 0; width: 100vw; height: 100vh;
background: rgba(0,0,0,0.4); display: flex; align-items: center; justify-content: center;">
<div style="
background: #fff; padding: 30px 40px; border-radius: 12px; box-shadow: 0 2px 16px #888;
font-size: 1.3em; color: #333; text-align: center;">
<span class="gemini-spinner" style="
display: inline-block; width: 32px; height: 32px; border: 4px solid #ccc; border-top: 4px solid #0078d7;
border-radius: 50%; animation: gemini-spin 1s linear infinite; margin-bottom: 16px;"></span><br>
${message}
</div>
</div>
<style>
@keyframes gemini-spin {
0% { transform: rotate(0deg);}
100% { transform: rotate(360deg);}
}
</style>
`;
$('body').append(modalHtml);
}
// モーダル非表示用関数
function hideLoadingModal() {
$('#gemini-loading-modal').remove();
}
// Google Gemini API でプロンプト送信→CSV取得→プリザンターへ登録
async function generateAndSaveSchedule(promptText) {
showLoadingModal();
try {
// 1. Google Gemini API へプロンプト送信
const geminiApiKey = 'AIzaSyAy_BhxPbZmyA7uN6wOrFZpOc3GJQhLAu8';
const geminiApiUrl = 'https://generativelanguage.googleapis.com/v1beta/models/gemini-2.0-flash:generateContent?key=' + geminiApiKey;
const geminiBody = {
'contents': [{ 'parts': [{ 'text': promptText }] }]
};
const geminiRes = await fetch(geminiApiUrl, {
'method': 'POST',
'headers': { 'Content-Type': 'application/json' },
'body': JSON.stringify(geminiBody)
});
const geminiData = await geminiRes.json();
// Geminiの返答からCSVテキストを抽出
const csvText = geminiData.candidates?.[0]?.content?.parts?.[0]?.text || '';
// 2. CSVをパース
const rows = csvText.trim().split('\n').map(line => line.split(','));
const filteredRows = rows.filter(arr => arr.length >= 2);
//console.log('CSV Data:', filteredRows);
const jsonArray = convertFilteredRowsToJson(filteredRows);
console.log('JSON Data:', jsonArray);
uploadShiftData(jsonArray);
// ▼▼▼ ここからCSV自動ダウンロード処理Shift_JIS対応 ▼▼▼
// 年月の取得(例: プロンプト内や画面のselectから取得
let ym = '';
if ($('#monthSelect').length) {
ym = $('#monthSelect').val();
} else {
const ymMatch = promptText.match(/#対象年月\s*([0-9]{4}年[0-9]{1,2}月)/);
ym = ymMatch ? ymMatch[1] : '年月未取得';
}
// ファイル名生成(末尾に年月日時分秒を追加)
const now = new Date();
const ymdhms = [
now.getFullYear(),
String(now.getMonth() + 1).padStart(2, '0'),
String(now.getDate()).padStart(2, '0'),
String(now.getHours()).padStart(2, '0'),
String(now.getMinutes()).padStart(2, '0'),
String(now.getSeconds()).padStart(2, '0')
].join('');
const fileName = `イオン多摩平自動生成シフト表_${ym}_${ymdhms}.csv`;
// Shift_JISでエンコードしてダウンロード
let sjisArray = Encoding.convert(Encoding.stringToCode(csvText), 'SJIS', 'UNICODE');
let sjisUint8 = new Uint8Array(sjisArray);
const blob = new Blob([sjisUint8], { type: 'text/csv' });
const url = URL.createObjectURL(blob);
const a = document.createElement('a');
a.href = url;
a.download = fileName;
document.body.appendChild(a);
//a.click();
setTimeout(() => {
document.body.removeChild(a);
URL.revokeObjectURL(url);
}, 100);
// ▲▲▲ ここまでCSV自動ダウンロード処理Shift_JIS対応 ▲▲▲
} finally {
hideLoadingModal();
window.location.reload();
}
}
async function uploadShiftData(jsonArray) {
const tableId = $p.siteId();
const uploadUrl = `https://${location.hostname}/pleasanter/api/items/${tableId}/bulkupsert`;
const uploadBody = {
'ApiVersion': 1.1,
'ApiKey': apiKey,
'Data': jsonArray
}
//プリザンターから対象のデータをすべて一括削除
const uploadRes = await fetch(uploadUrl, {
'method': 'POST',
'headers': { 'Content-Type': 'application/json' },
'body': JSON.stringify(uploadBody)
});
const resData = await uploadRes.json();
console.log(resData);
}
// filteredRows からJSON配列へ変換項目名置き換えルール適用
function convertFilteredRowsToJson(filteredRows) {
if (!filteredRows || filteredRows.length < 2) return [];
// 1行目をヘッダーとして取得
const header = filteredRows[0];
// 置き換えルール
const keyMap = {
'対象年月': 'ClassA',
'対象日付': 'DateA',
'種別': 'ClassP',
'名前': 'ClassB',
'開始時間': 'NumA',
'終了時間': 'NumB',
'勤務時間': 'NumC',
'休憩時間': 'NumD'
};
// ヘッダーをPleasanter用キーに変換
const mappedHeader = header.map(col => keyMap[col.trim()] || col.trim());
// データ行をJSON化
const jsonArray = filteredRows.slice(1).map(row => {
let obj = {};
obj.ClassHash = {};
obj.NumHash = {};
obj.DateHash = {};
mappedHeader.forEach((key, idx) => {
const val = row[idx] !== undefined ? row[idx].trim() : '';
if (key.startsWith('Class')) {
obj.ClassHash[key] = val;
} else if (key.startsWith('Num')) {
obj.NumHash[key] = val;
} else if (key.startsWith('Date')) {
obj.DateHash[key] = val;
} else {
obj[key] = val;
}
});
return obj;
});
return jsonArray;
}
// 使い方例
// const jsonArray = convertFilteredRowsToJson(filteredRows);
/**
* プリザンターのテーブルからDateA項目が指定年月の範囲にあるデータを一括削除する
*/
async function deletePleasanterDataByDateRange() {
// 実行確認のポップアップ
if (!window.confirm('本当にこの年月のデータを一括削除してよろしいですか?')) {
return false;
}
//自分自身
let tableId = $p.siteId();
//選択した年月を取得
if ($('#monthSelect').length) {
ym = $('#monthSelect').val();
} else {
return false;
}
const year = ym.split('年')[0]; // "2025"
const month = ym.split('年')[1].replace('月', ''); // "06"
//期間の開始日と終了日を作成
const fromDate = year + '/' + month + '/01';
//終了年月の月末日を計算
const lastDate = new Date(fromDate);
lastDate.setMonth(lastDate.getMonth() + 1);
lastDate.setDate(0); // 月末日を取得
const toDate = `${year}/${month}/${lastDate.getDate()}`; // "2025/06/30"
//一括削除
const deleteUrl = `https://${location.hostname}/pleasanter/api/items/${tableId}/bulkdelete`;
const deleteBody = {
'ApiVersion': 1.1,
'ApiKey': apiKey,
'View': {
'ColumnFilterHash': {
'DateA': `["${fromDate} 00:00:00,${toDate} 23:59:59"]`,
'ClassP': '["社員","パート","欠員"]'
}
}
}
//プリザンターから対象のデータをすべて一括削除
const deleteRes = await fetch(deleteUrl, {
'method': 'POST',
'headers': { 'Content-Type': 'application/json' },
'body': JSON.stringify(deleteBody)
});
const resData = await deleteRes.json();
//console.log(resData);
window.location.reload();
}
/**
* #hiddenタグに保存されている件数をチェック
*/
async function getPleasanterCountByMonth() {
let ym = '';
//選択した年月を取得
if ($('#monthSelect').length) {
ym = $('#monthSelect').val();
} else {
return 0;
}
const year = ym.split('年')[0]; // "2025"
const month = ym.split('年')[1].replace('月', ''); // "06"
return parseFloat($('#' + year + '_' + month + '_Count').val()) || 0;
}