222 lines
7.2 KiB
HTML

{% extends 'base.html' %}
{% block content %}
<style>
.timeline {
display: flex;
flex-wrap: wrap;
gap: 4px;
user-select: none; /* Prevent text selection during drag */
}
.hour-block {
width: 60px;
height: 70px; /* Increased height to fit additional text */
border-radius: 6px;
text-align: center;
line-height: 1.2;
font-size: 0.9rem;
padding-top: 6px;
cursor: pointer;
user-select: none;
transition: background-color 0.2s ease-in-out;
margin: 1px;
}
.hour-block.selected {
outline: 2px solid #4584b8;
}
.papers {
font-size: 0.7rem;
margin-top: 2px;
}
.flash-message {
padding: 12px;
margin-bottom: 20px;
border-radius: 6px;
opacity: 1;
transition: opacity 2s ease-in-out;
}
.flash-message.success {
background-color: #d4edda;
border-color: #c3e6cb;
color: #155724;
}
.flash-message.error {
background-color: #f8d7da;
border-color: #f5c6cb;
color: #721c24;
}
.flash-message.fade {
opacity: 0;
}
</style>
<script>
const initialSchedule = {{ schedule | tojson }};
const totalVolume = {{ volume }};
</script>
<div x-data="scheduleManager(initialSchedule, totalVolume)" class="container my-5">
<h2 class="mb-4">🕒 Configure Hourly Download Weights</h2>
<!-- Flash Messages -->
{% with messages = get_flashed_messages(with_categories=true) %}
{% if messages %}
<div id="flash-messages">
{% for category, message in messages %}
<div class="flash-message {{ category }}" x-data="{}" x-init="setTimeout(() => $el.classList.add('fade'), 100); setTimeout(() => $el.remove(), 5000)">
{{ message }}
</div>
{% endfor %}
</div>
{% endif %}
{% endwith %}
<p class="text-muted">
Click to select one or more hours below. Then assign a weight to them using the input and apply it. Color indicates relative intensity. The total daily volume will be split proportionally across these weights.
</p>
<h3 class="mt-4">Volume</h3>
<p class="text-muted">The total volume of data to be downloaded each day is <strong x-text="volume"></strong> papers.</p>
<div class="d-flex justify-content-between align-items-start flex-wrap gap-2">
<p class="text-muted mb-0" style="max-width: 600px;">
Click to select one or more hours below. Then assign a weight to them using the input and apply it. Color indicates relative intensity. The total daily volume will be split proportionally across these weights.
</p>
<div class="hour-block"
style="pointer-events: none;"
x-init="$nextTick(() => previewWeight = 1.0)"
x-data="{ previewWeight: 1.0 }"
:style="getBackgroundStyleFromValue(previewWeight)">
<div><strong>14:00</strong></div>
<div class="weight"><span x-text="previewWeight.toFixed(1)"></span></div>
<div class="papers text-muted">example</div>
</div>
</div>
<h3 class="mt-4">Current Schedule</h3>
<form method="POST" action="{{ url_for('main.schedule') }}">
<div class="timeline mb-3" @mouseup="endDrag()" @mouseleave="endDrag()">
<template x-for="hour in Object.keys(schedule)" :key="hour">
<div class="hour-block"
:id="'hour-' + hour"
:data-hour="hour"
:style="getBackgroundStyle(hour)"
:class="{'selected': isSelected(hour)}"
@mousedown="startDrag($event, hour)"
@mouseover="dragSelect(hour)">
<div><strong x-text="formatHour(hour)"></strong></div>
<div class="weight"><span x-text="schedule[hour]"></span></div>
<div class="papers"><span x-text="getPapersPerHour(hour)"></span> p.</div>
<input type="hidden" :name="'hour_' + hour" :value="schedule[hour]">
</div>
</template>
</div>
<div class="input-group mb-4 w-50">
<label class="input-group-text">Set Weight:</label>
<input type="number" step="0.1" min="0" max="5" x-model="newWeight" class="form-control">
<button type="button" class="btn btn-outline-primary" @click="applyWeight()">Apply to Selected</button>
</div>
<div class="d-flex justify-content-between">
<a href="/" class="btn btn-outline-secondary">⬅ Back</a>
<button type="submit" class="btn btn-success">💾 Save Schedule</button>
</div>
</form>
</div>
<script>
function scheduleManager(initial, volume) {
return {
schedule: { ...initial },
volume: volume,
selectedHours: [],
newWeight: 1.0,
isDragging: false,
dragOperation: null,
formatHour(h) {
return String(h).padStart(2, '0') + ":00";
},
getBackgroundStyle(hour) {
const weight = parseFloat(this.schedule[hour]);
const maxWeight = 2.5; // You can adjust this
// Normalize weight (0.0 to 1.0)
const t = Math.min(weight / maxWeight, 1.0);
// Interpolate HSL lightness: 95% (light) to 30% (dark)
const lightness = 95 - (t * 65); // 95 → 30
const backgroundColor = `hsl(210, 10%, ${lightness}%)`; // soft gray-blue palette
const textColor = t > 0.65 ? 'white' : 'black'; // adaptive text color
return {
backgroundColor,
color: textColor
};
},
startDrag(event, hour) {
event.preventDefault();
this.isDragging = true;
this.dragOperation = this.isSelected(hour) ? 'remove' : 'add';
this.toggleSelect(hour);
},
dragSelect(hour) {
if (!this.isDragging) return;
const selected = this.isSelected(hour);
if (this.dragOperation === 'add' && !selected) {
this.selectedHours.push(hour);
} else if (this.dragOperation === 'remove' && selected) {
this.selectedHours = this.selectedHours.filter(h => h !== hour);
}
},
endDrag() {
this.isDragging = false;
},
toggleSelect(hour) {
if (this.isSelected(hour)) {
this.selectedHours = this.selectedHours.filter(h => h !== hour);
} else {
this.selectedHours.push(hour);
}
},
isSelected(hour) {
return this.selectedHours.includes(hour);
},
applyWeight() {
this.selectedHours.forEach(hour => {
this.schedule[hour] = parseFloat(this.newWeight).toFixed(1);
});
this.selectedHours = [];
},
getTotalWeight() {
return Object.values(this.schedule).reduce((sum, w) => sum + parseFloat(w), 0);
},
getPapersPerHour(hour) {
const total = this.getTotalWeight();
if (total === 0) return 0;
return ((parseFloat(this.schedule[hour]) / total) * this.volume).toFixed(1);
}
};
}
</script>{% endblock %}