Size
11.0 KB
Version
1.1.1
Created
Nov 30, 2025
Updated
12 days ago
1// ==UserScript==
2// @name Chatwork 納期・単価ハイライター
3// @description 納期の時間帯を色分けし、単価情報を見やすく表示します
4// @version 1.1.1
5// @match https://*.chatwork.com/*
6// @icon https://assets.chatwork.com/images/favicon/favicon00.ico?1
7// ==/UserScript==
8(function() {
9 'use strict';
10
11 console.log('Chatwork 納期・単価ハイライター: 起動しました');
12
13 // デバウンス関数
14 function debounce(func, wait) {
15 let timeout;
16 return function executedFunction(...args) {
17 const later = () => {
18 clearTimeout(timeout);
19 func(...args);
20 };
21 clearTimeout(timeout);
22 timeout = setTimeout(later, wait);
23 };
24 }
25
26 // 時間帯を判定して色を返す関数
27 function getTimeColor(hour) {
28 if (hour >= 6 && hour < 12) {
29 // 朝(6:00-11:59):明るいオレンジ
30 return {
31 bg: '#FFE4B5',
32 text: '#D2691E',
33 label: '朝'
34 };
35 } else if (hour >= 12 && hour < 18) {
36 // 昼(12:00-17:59):明るい黄色
37 return {
38 bg: '#FFFACD',
39 text: '#DAA520',
40 label: '昼'
41 };
42 } else if (hour >= 18 && hour < 21) {
43 // 夕刻(18:00-20:59):オレンジ
44 return {
45 bg: '#FFD4A3',
46 text: '#FF8C00',
47 label: '夕刻'
48 };
49 } else {
50 // 夜(21:00-5:59):濃い青
51 return {
52 bg: '#B0C4DE',
53 text: '#191970',
54 label: '夜'
55 };
56 }
57 }
58
59 // 要素が画面内または画面に近いかチェック(バッファ付き)
60 function isElementNearViewport(el) {
61 const rect = el.getBoundingClientRect();
62 const buffer = 500; // 画面外500pxまで処理
63 return (
64 rect.bottom >= -buffer &&
65 rect.top <= (window.innerHeight || document.documentElement.clientHeight) + buffer
66 );
67 }
68
69 // メッセージ内の納期と単価をハイライトする関数
70 function highlightDeadlinesAndPrices(onlyViewport = false) {
71 console.log('納期・単価のハイライト処理を開始', onlyViewport ? '(画面内のみ)' : '(全体)');
72
73 // すべてのメッセージ要素を取得
74 const messages = document.querySelectorAll('pre.sc-fbFiXs');
75
76 let processedCount = 0;
77
78 messages.forEach(messageElement => {
79 // すでに処理済みかチェック
80 if (messageElement.hasAttribute('data-deadline-highlighted')) {
81 return;
82 }
83
84 // 画面内のみ処理する場合はチェック
85 if (onlyViewport && !isElementNearViewport(messageElement)) {
86 return;
87 }
88
89 let html = messageElement.innerHTML;
90 let modified = false;
91
92 // 納期パターン1: 納期:12/09/24:00 または 納期:12/09/24:00
93 const deadlinePattern1 = /納期[::]\s*(\d{1,2})\/(\d{1,2})\/(\d{1,2}):(\d{1,2})/g;
94
95 html = html.replace(deadlinePattern1, (match, month, day, hour, minute) => {
96 modified = true;
97 const hourNum = parseInt(hour, 10);
98 const timeInfo = getTimeColor(hourNum);
99
100 // 納期ラベル
101 const deadlineLabel = '<span style="background-color: #FF6B6B; color: white; padding: 3px 8px; border-radius: 4px; font-weight: bold; font-size: 13px; margin-right: 4px;">納期</span>';
102
103 // 日付部分(月/日)
104 const datePart = `<span style="background-color: #4A90E2; color: white; padding: 3px 8px; border-radius: 4px; font-weight: bold; font-size: 14px; margin-right: 4px;">${month}/${day}</span>`;
105
106 // 時間帯ラベル(朝・昼・夕刻・夜)
107 const timePeriodLabel = `<span style="background-color: ${timeInfo.text}; color: white; padding: 3px 8px; border-radius: 4px; font-weight: bold; font-size: 13px; margin-right: 4px;">${timeInfo.label}</span>`;
108
109 // 時刻部分(24:00)
110 const timePart = `<span style="background-color: ${timeInfo.bg}; color: ${timeInfo.text}; padding: 3px 8px; border-radius: 4px; font-weight: bold; font-size: 14px; border: 2px solid ${timeInfo.text};">${hour}:${minute}</span>`;
111
112 return `<span style="display: inline-block; margin: 2px 0;">${deadlineLabel}${datePart}${timePeriodLabel}${timePart}</span>`;
113 });
114
115 // 納期パターン2: 納期12/01/24:00(コロンなし)
116 const deadlinePattern2 = /納期(\d{1,2})\/(\d{1,2})\/(\d{1,2}):(\d{1,2})/g;
117
118 html = html.replace(deadlinePattern2, (match, month, day, hour, minute) => {
119 modified = true;
120 const hourNum = parseInt(hour, 10);
121 const timeInfo = getTimeColor(hourNum);
122
123 // 納期ラベル
124 const deadlineLabel = '<span style="background-color: #FF6B6B; color: white; padding: 3px 8px; border-radius: 4px; font-weight: bold; font-size: 13px; margin-right: 4px;">納期</span>';
125
126 // 日付部分(月/日)
127 const datePart = `<span style="background-color: #4A90E2; color: white; padding: 3px 8px; border-radius: 4px; font-weight: bold; font-size: 14px; margin-right: 4px;">${month}/${day}</span>`;
128
129 // 時間帯ラベル(朝・昼・夕刻・夜)
130 const timePeriodLabel = `<span style="background-color: ${timeInfo.text}; color: white; padding: 3px 8px; border-radius: 4px; font-weight: bold; font-size: 13px; margin-right: 4px;">${timeInfo.label}</span>`;
131
132 // 時刻部分(24:00)
133 const timePart = `<span style="background-color: ${timeInfo.bg}; color: ${timeInfo.text}; padding: 3px 8px; border-radius: 4px; font-weight: bold; font-size: 14px; border: 2px solid ${timeInfo.text};">${hour}:${minute}</span>`;
134
135 return `<span style="display: inline-block; margin: 2px 0;">${deadlineLabel}${datePart}${timePeriodLabel}${timePart}</span>`;
136 });
137
138 // 基本日付パターン: 12/01 や 12/31 など(納期以外の日付)
139 const datePattern = /(\d{1,2})\/(\d{1,2})(?!\/|\d)/g;
140
141 html = html.replace(datePattern, (match, month, day) => {
142 // すでにハイライト済みの部分は除外
143 if (html.includes(`>${match}<`) || html.includes(`"${match}"`)) {
144 return match;
145 }
146 modified = true;
147 return `<span style="background-color: #E8F4F8; color: #2C5F7C; padding: 2px 6px; border-radius: 3px; font-weight: bold; font-size: 13px; border: 1px solid #2C5F7C;">${month}/${day}</span>`;
148 });
149
150 // 単価パターン1: 単価:2000円 または 単価は2000円
151 const pricePattern1 = /単価[::は]\s*(\d+(?:,\d{3})*)\s*円/g;
152
153 html = html.replace(pricePattern1, (match, price) => {
154 modified = true;
155 // 単価ラベル
156 const priceLabel = '<span style="background-color: #10B981; color: white; padding: 3px 8px; border-radius: 4px; font-weight: bold; font-size: 13px; margin-right: 4px;">単価</span>';
157
158 // 金額部分
159 const pricePart = `<span style="background-color: #D1FAE5; color: #065F46; padding: 3px 8px; border-radius: 4px; font-weight: bold; font-size: 14px; border: 2px solid #10B981;">${price}円</span>`;
160
161 return `<span style="display: inline-block; margin: 2px 0;">${priceLabel}${pricePart}</span>`;
162 });
163
164 // 単価パターン2: 1カット 2000円 または 1カット 2000円
165 const pricePattern2 = /(\d+)\s*カット\s*[ \s]+(\d+(?:,\d{3})*)\s*円/g;
166
167 html = html.replace(pricePattern2, (match, cuts, price) => {
168 modified = true;
169 // カット数ラベル
170 const cutLabel = `<span style="background-color: #8B5CF6; color: white; padding: 3px 8px; border-radius: 4px; font-weight: bold; font-size: 13px; margin-right: 4px;">${cuts}カット</span>`;
171
172 // 金額部分
173 const pricePart = `<span style="background-color: #D1FAE5; color: #065F46; padding: 3px 8px; border-radius: 4px; font-weight: bold; font-size: 14px; border: 2px solid #10B981;">${price}円</span>`;
174
175 return `<span style="display: inline-block; margin: 2px 0;">${cutLabel}${pricePart}</span>`;
176 });
177
178 if (modified) {
179 messageElement.innerHTML = html;
180 messageElement.setAttribute('data-deadline-highlighted', 'true');
181 processedCount++;
182 console.log('メッセージを処理しました:', messageElement);
183 }
184 });
185
186 if (processedCount > 0) {
187 console.log(`${processedCount}件のメッセージを処理しました`);
188 }
189 }
190
191 // スクロールイベントハンドラ(デバウンス付き)
192 const handleScroll = debounce(() => {
193 console.log('スクロール検知、ハイライト処理を実行');
194 highlightDeadlinesAndPrices(true);
195 }, 1000);
196
197 // 初回実行
198 function init() {
199 console.log('初期化を開始');
200
201 // ページ読み込み後に実行(初回は画面内のみ)
202 setTimeout(() => {
203 highlightDeadlinesAndPrices(true);
204 }, 2000);
205
206 // MutationObserverでDOM変更を監視
207 const observer = new MutationObserver(debounce(() => {
208 console.log('DOM変更を検知、ハイライト処理を実行');
209 highlightDeadlinesAndPrices(true);
210 }, 500));
211
212 // タイムライン要素を監視
213 const timelineElement = document.querySelector('#_timeLine');
214 if (timelineElement) {
215 observer.observe(timelineElement, {
216 childList: true,
217 subtree: true
218 });
219 console.log('MutationObserver設定完了');
220
221 // スクロールイベントをタイムライン要素に追加
222 timelineElement.addEventListener('scroll', handleScroll);
223 console.log('スクロールイベント設定完了');
224 } else {
225 console.log('タイムライン要素が見つかりません、再試行します');
226 setTimeout(init, 1000);
227 }
228 }
229
230 // ページ読み込み完了後に初期化
231 if (document.readyState === 'loading') {
232 document.addEventListener('DOMContentLoaded', init);
233 } else {
234 init();
235 }
236
237})();