Chatwork 納期・単価ハイライター

納期の時間帯を色分けし、単価情報を見やすく表示します

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})();
Chatwork 納期・単価ハイライター | Robomonkey