374 lines
16 KiB
Django/Jinja
374 lines
16 KiB
Django/Jinja
<style>
|
||
.timeline {
|
||
display: flex;
|
||
flex-wrap: wrap;
|
||
gap: 3px;
|
||
user-select: none;
|
||
/* Prevent text selection during drag */
|
||
}
|
||
|
||
.hour-block {
|
||
width: 49px;
|
||
height: 70px;
|
||
/* Increased height to fit additional text */
|
||
border-radius: 5px;
|
||
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;
|
||
}
|
||
|
||
.weight-gradient {
|
||
width: 50px;
|
||
height: 15px;
|
||
background: linear-gradient(to right, hsl(210, 10%, 95%), hsl(210, 10%, 30%));
|
||
border-radius: 2px;
|
||
}
|
||
</style>
|
||
|
||
<script>
|
||
const initialSchedule = {{ schedule | tojson }};
|
||
const totalVolume = {{ volume }};
|
||
</script>
|
||
|
||
<div x-data="scheduleManager(initialSchedule, totalVolume)" class="tab-pane active">
|
||
<div class="card">
|
||
<div class="card-header d-flex justify-content-between">
|
||
<h5>Scheduling Configuration</h5>
|
||
<span>
|
||
<button class="btn btn-sm btn-outline-secondary" type="button" data-bs-toggle="collapse"
|
||
data-bs-target="#helpContent">
|
||
<i class="fas fa-question-circle"></i> Help
|
||
</button>
|
||
</span>
|
||
</div>
|
||
<div class="card-body">
|
||
<!-- include flash messages template -->
|
||
{% include "partials/flash_messages.html.jinja" %}
|
||
|
||
<!-- Collapsible Help Content -->
|
||
<div class="collapse mt-3" id="helpContent">
|
||
<div class="card card-body">
|
||
<ul class="nav nav-tabs" id="helpTabs" role="tablist">
|
||
<li class="nav-item" role="presentation">
|
||
<button class="nav-link active" id="calculation-tab" data-bs-toggle="tab"
|
||
data-bs-target="#calculation" type="button">Calculation</button>
|
||
</li>
|
||
<li class="nav-item" role="presentation">
|
||
<button class="nav-link" id="usage-tab" data-bs-toggle="tab" data-bs-target="#usage"
|
||
type="button">Usage</button>
|
||
</li>
|
||
<li class="nav-item" role="presentation">
|
||
<button class="nav-link" id="example-tab" data-bs-toggle="tab" data-bs-target="#example"
|
||
type="button">Example</button>
|
||
</li>
|
||
</ul>
|
||
<div class="tab-content p-3 border border-top-0 rounded-bottom">
|
||
<!-- Calculation Tab -->
|
||
<div class="tab-pane fade show active" id="calculation" role="tabpanel">
|
||
<h5>Quota Calculation</h5>
|
||
<p>Each hour's quota is calculated as:</p>
|
||
<div class="bg-light p-2 mb-2 rounded">
|
||
<code>Papers per hour = (Hour Weight ÷ Total Weight) × Daily Volume</code>
|
||
</div>
|
||
<p class="small mb-0">Changes to either volume or schedule weights will immediately
|
||
recalculate all hourly quotas.</p>
|
||
</div>
|
||
|
||
<!-- Usage Instructions Tab -->
|
||
<div class="tab-pane fade" id="usage" role="tabpanel">
|
||
<h5>Usage Instructions</h5>
|
||
<ol class="mb-0">
|
||
<li>Click to select one or more hour blocks (use drag to select multiple)</li>
|
||
<li>Adjust the weight value for selected hours (0.1-5.0)</li>
|
||
<li>Click "Apply to Selected" to set the weights</li>
|
||
<li>Click "Save Schedule" to commit your changes</li>
|
||
</ol>
|
||
</div>
|
||
|
||
<!-- Example Tab -->
|
||
<div class="tab-pane fade" id="example" role="tabpanel">
|
||
<h5>Example</h5>
|
||
<p class="mb-0">
|
||
With a daily volume of <strong>240 papers</strong> and hour weights of
|
||
<strong>1.0, 2.0, and 3.0</strong>, the distribution will be
|
||
<strong>40, 80, and 120 papers</strong> respectively
|
||
(based on ratios 1:2:3 of the total weight 6.0).
|
||
</p>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- Volume and Schedule Controls -->
|
||
<div class="row g-3 mb-3">
|
||
<!-- Daily Volume Column -->
|
||
<div class="col-md-4" x-data="{ volumeValue: volume }">
|
||
<div class="card h-100">
|
||
<div class="card-header bg-light">
|
||
<h5 class="mb-0"><i class="fas fa-chart-line"></i> Daily Volume</h5>
|
||
</div>
|
||
<div class="card-body">
|
||
<p class="lead mb-2">
|
||
<strong x-text="volume"></strong> papers/day
|
||
</p>
|
||
<div class="input-group input-group-sm mb-2">
|
||
<input type="number" class="form-control" x-model="volumeValue" min="1"
|
||
max="{{ max_volume }}" required />
|
||
<button type="button" class="btn btn-primary" @click="updateVolume()">
|
||
<i class="fas fa-save"></i> Update
|
||
</button>
|
||
</div>
|
||
<small class="text-muted">Range: 1-{{ max_volume }}</small>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- Legend Column -->
|
||
<div class="col-md-8">
|
||
<div class="card h-100">
|
||
<div class="card-header bg-light">
|
||
<h5 class="mb-0"><i class="fas fa-info-circle"></i> Quick Guide</h5>
|
||
</div>
|
||
<div class="card-body">
|
||
<div class="d-flex align-items-center justify-content-between mb-2">
|
||
<div class="d-flex align-items-center">
|
||
<div class="weight-gradient me-2"></div>
|
||
<small>Darker blocks = higher weight</small>
|
||
</div>
|
||
<div class="badge bg-info">Formula: (Weight ÷ Total) × Volume</div>
|
||
</div>
|
||
<div class="d-flex align-items-center small text-muted">
|
||
<div class="me-3"><i class="fas fa-mouse-pointer"></i> Click to select hours</div>
|
||
<div class="me-3"><i class="fas fa-arrows-alt-h"></i> Drag to select multiple</div>
|
||
<div><i class="fas fa-save"></i> Save after changes</div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- 24-Hour Schedule -->
|
||
<form id="scheduleForm">
|
||
<div class="card">
|
||
<div class="card-header d-flex justify-content-between align-items-center bg-light">
|
||
<h5 class="mb-0"><i class="fas fa-clock"></i> 24-Hour Schedule</h5>
|
||
<span class="badge bg-info"
|
||
x-text="selectedHours.length ? selectedHours.length + ' hours selected' : ''"></span>
|
||
</div>
|
||
<div class="card-body">
|
||
<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-2">
|
||
<label class="input-group-text"><i class="fas fa-weight"></i> Weight</label>
|
||
<input type="number" step="0.1" min="0.1" max="5" x-model="newWeight"
|
||
class="form-control" />
|
||
<button type="button" class="btn btn-primary" @click="applyWeight()"
|
||
:disabled="selectedHours.length === 0">
|
||
Apply to <span x-text="selectedHours.length"></span> Selected
|
||
</button>
|
||
</div>
|
||
</div>
|
||
<div class="card-footer">
|
||
<div class="d-flex justify-content-between">
|
||
<a href="{{ url_for('config.general') }}" class="btn btn-outline-secondary">
|
||
<i class="fas fa-arrow-left"></i> Back
|
||
</a>
|
||
<button type="button" class="btn btn-success" @click="saveSchedule()">
|
||
<i class="fas fa-save"></i> Save Schedule
|
||
</button>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</form>
|
||
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
<script>
|
||
function scheduleManager(initial, volume) {
|
||
return {
|
||
schedule: { ...initial },
|
||
volume: volume,
|
||
selectedHours: [],
|
||
newWeight: 1.0,
|
||
volumeValue: volume,
|
||
isDragging: false,
|
||
dragOperation: null,
|
||
|
||
formatHour(h) {
|
||
return String(h).padStart(2, "0") + ":00";
|
||
},
|
||
|
||
updateVolume() {
|
||
fetch('{{ url_for('config.api_update_config') }}', {
|
||
method: 'POST',
|
||
headers: {
|
||
'Content-Type': 'application/json',
|
||
},
|
||
body: JSON.stringify({
|
||
volume: this.volumeValue
|
||
})
|
||
})
|
||
.then(response => response.json())
|
||
.then(data => {
|
||
if (data.success) {
|
||
this.volume = parseFloat(this.volumeValue);
|
||
showFlashMessage('Volume updated successfully!', 'success');
|
||
} else {
|
||
showFlashMessage(data.updates?.[0]?.message || 'Error updating volume', 'error');
|
||
}
|
||
})
|
||
.catch(error => {
|
||
console.error('Error:', error);
|
||
showFlashMessage('Network error occurred', 'error');
|
||
});
|
||
},
|
||
|
||
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,
|
||
};
|
||
},
|
||
|
||
getBackgroundStyleFromValue(value) {
|
||
const weight = parseFloat(value);
|
||
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);
|
||
},
|
||
|
||
saveSchedule() {
|
||
fetch('{{ url_for('config.api_update_config') }}', {
|
||
method: 'POST',
|
||
headers: {
|
||
'Content-Type': 'application/json',
|
||
},
|
||
body: JSON.stringify({
|
||
schedule: this.schedule
|
||
})
|
||
})
|
||
.then(response => response.json())
|
||
.then(data => {
|
||
if (data.success) {
|
||
showFlashMessage('Schedule updated successfully!', 'success');
|
||
} else {
|
||
showFlashMessage(data.updates?.[0]?.message || 'Error updating schedule', 'error');
|
||
}
|
||
})
|
||
.catch(error => {
|
||
console.error('Error:', error);
|
||
showFlashMessage('Network error occurred', 'error');
|
||
});
|
||
}
|
||
};
|
||
}
|
||
</script> |