pretix-map 0.1.5__tar.gz → 0.1.7__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- {pretix_map-0.1.5/pretix_map.egg-info → pretix_map-0.1.7}/PKG-INFO +1 -1
- {pretix_map-0.1.5 → pretix_map-0.1.7/pretix_map.egg-info}/PKG-INFO +1 -1
- pretix_map-0.1.7/pretix_mapplugin/__init__.py +1 -0
- pretix_map-0.1.7/pretix_mapplugin/static/pretix_mapplugin/css/salesmap.css +101 -0
- {pretix_map-0.1.5 → pretix_map-0.1.7}/pretix_mapplugin/static/pretix_mapplugin/js/salesmap.js +302 -33
- {pretix_map-0.1.5 → pretix_map-0.1.7}/pretix_mapplugin/templates/pretix_mapplugin/map_page.html +47 -12
- {pretix_map-0.1.5 → pretix_map-0.1.7}/pretix_mapplugin/views.py +32 -1
- pretix_map-0.1.5/pretix_mapplugin/__init__.py +0 -1
- pretix_map-0.1.5/pretix_mapplugin/static/pretix_mapplugin/css/salesmap.css +0 -52
- {pretix_map-0.1.5 → pretix_map-0.1.7}/LICENSE +0 -0
- {pretix_map-0.1.5 → pretix_map-0.1.7}/MANIFEST.in +0 -0
- {pretix_map-0.1.5 → pretix_map-0.1.7}/README.md +0 -0
- {pretix_map-0.1.5 → pretix_map-0.1.7}/pretix_map.egg-info/SOURCES.txt +0 -0
- {pretix_map-0.1.5 → pretix_map-0.1.7}/pretix_map.egg-info/dependency_links.txt +0 -0
- {pretix_map-0.1.5 → pretix_map-0.1.7}/pretix_map.egg-info/entry_points.txt +0 -0
- {pretix_map-0.1.5 → pretix_map-0.1.7}/pretix_map.egg-info/requires.txt +0 -0
- {pretix_map-0.1.5 → pretix_map-0.1.7}/pretix_map.egg-info/top_level.txt +0 -0
- {pretix_map-0.1.5 → pretix_map-0.1.7}/pretix_mapplugin/apps.py +0 -0
- {pretix_map-0.1.5 → pretix_map-0.1.7}/pretix_mapplugin/geocoding.py +0 -0
- {pretix_map-0.1.5 → pretix_map-0.1.7}/pretix_mapplugin/locale/de/LC_MESSAGES/django.po +0 -0
- {pretix_map-0.1.5 → pretix_map-0.1.7}/pretix_mapplugin/locale/de_Informal/.gitkeep +0 -0
- {pretix_map-0.1.5 → pretix_map-0.1.7}/pretix_mapplugin/locale/de_Informal/LC_MESSAGES/django.po +0 -0
- {pretix_map-0.1.5 → pretix_map-0.1.7}/pretix_mapplugin/management/__init__.py +0 -0
- {pretix_map-0.1.5 → pretix_map-0.1.7}/pretix_mapplugin/management/commands/__init__.py +0 -0
- {pretix_map-0.1.5 → pretix_map-0.1.7}/pretix_mapplugin/management/commands/geocode_existing_orders.py +0 -0
- {pretix_map-0.1.5 → pretix_map-0.1.7}/pretix_mapplugin/migrations/0001_initial.py +0 -0
- {pretix_map-0.1.5 → pretix_map-0.1.7}/pretix_mapplugin/migrations/0002_remove_ordergeocodedata_geocoded_timestamp_and_more.py +0 -0
- {pretix_map-0.1.5 → pretix_map-0.1.7}/pretix_mapplugin/migrations/0003_mapmilestone.py +0 -0
- {pretix_map-0.1.5 → pretix_map-0.1.7}/pretix_mapplugin/migrations/__init__.py +0 -0
- {pretix_map-0.1.5 → pretix_map-0.1.7}/pretix_mapplugin/models.py +0 -0
- {pretix_map-0.1.5 → pretix_map-0.1.7}/pretix_mapplugin/signals.py +0 -0
- {pretix_map-0.1.5 → pretix_map-0.1.7}/pretix_mapplugin/static/pretix_mapplugin/.gitkeep +0 -0
- {pretix_map-0.1.5 → pretix_map-0.1.7}/pretix_mapplugin/static/pretix_mapplugin/libs/leaflet-sales-map/MarkerCluster.Default.css +0 -0
- {pretix_map-0.1.5 → pretix_map-0.1.7}/pretix_mapplugin/static/pretix_mapplugin/libs/leaflet-sales-map/MarkerCluster.css +0 -0
- {pretix_map-0.1.5 → pretix_map-0.1.7}/pretix_mapplugin/static/pretix_mapplugin/libs/leaflet-sales-map/images/layers-2x.png +0 -0
- {pretix_map-0.1.5 → pretix_map-0.1.7}/pretix_mapplugin/static/pretix_mapplugin/libs/leaflet-sales-map/images/layers.png +0 -0
- {pretix_map-0.1.5 → pretix_map-0.1.7}/pretix_mapplugin/static/pretix_mapplugin/libs/leaflet-sales-map/images/marker-icon-2x.png +0 -0
- {pretix_map-0.1.5 → pretix_map-0.1.7}/pretix_mapplugin/static/pretix_mapplugin/libs/leaflet-sales-map/images/marker-icon.png +0 -0
- {pretix_map-0.1.5 → pretix_map-0.1.7}/pretix_mapplugin/static/pretix_mapplugin/libs/leaflet-sales-map/images/marker-shadow.png +0 -0
- {pretix_map-0.1.5 → pretix_map-0.1.7}/pretix_mapplugin/static/pretix_mapplugin/libs/leaflet-sales-map/leaflet-heat.js +0 -0
- {pretix_map-0.1.5 → pretix_map-0.1.7}/pretix_mapplugin/static/pretix_mapplugin/libs/leaflet-sales-map/leaflet-src.esm.js +0 -0
- {pretix_map-0.1.5 → pretix_map-0.1.7}/pretix_mapplugin/static/pretix_mapplugin/libs/leaflet-sales-map/leaflet-src.esm.js.map +0 -0
- {pretix_map-0.1.5 → pretix_map-0.1.7}/pretix_mapplugin/static/pretix_mapplugin/libs/leaflet-sales-map/leaflet-src.js +0 -0
- {pretix_map-0.1.5 → pretix_map-0.1.7}/pretix_mapplugin/static/pretix_mapplugin/libs/leaflet-sales-map/leaflet-src.js.map +0 -0
- {pretix_map-0.1.5 → pretix_map-0.1.7}/pretix_mapplugin/static/pretix_mapplugin/libs/leaflet-sales-map/leaflet.css +0 -0
- {pretix_map-0.1.5 → pretix_map-0.1.7}/pretix_mapplugin/static/pretix_mapplugin/libs/leaflet-sales-map/leaflet.js +0 -0
- {pretix_map-0.1.5 → pretix_map-0.1.7}/pretix_mapplugin/static/pretix_mapplugin/libs/leaflet-sales-map/leaflet.js.map +0 -0
- {pretix_map-0.1.5 → pretix_map-0.1.7}/pretix_mapplugin/static/pretix_mapplugin/libs/leaflet-sales-map/leaflet.markercluster.js +0 -0
- {pretix_map-0.1.5 → pretix_map-0.1.7}/pretix_mapplugin/static/pretix_mapplugin/libs/leaflet-sales-map/leaflet.markercluster.js.map +0 -0
- {pretix_map-0.1.5 → pretix_map-0.1.7}/pretix_mapplugin/tasks.py +0 -0
- {pretix_map-0.1.5 → pretix_map-0.1.7}/pretix_mapplugin/templates/pretix_mapplugin/.gitkeep +0 -0
- {pretix_map-0.1.5 → pretix_map-0.1.7}/pretix_mapplugin/templates/pretix_mapplugin/milestones.html +0 -0
- {pretix_map-0.1.5 → pretix_map-0.1.7}/pretix_mapplugin/urls.py +0 -0
- {pretix_map-0.1.5 → pretix_map-0.1.7}/pyproject.toml +0 -0
- {pretix_map-0.1.5 → pretix_map-0.1.7}/setup.cfg +0 -0
- {pretix_map-0.1.5 → pretix_map-0.1.7}/setup.py +0 -0
- {pretix_map-0.1.5 → pretix_map-0.1.7}/tests/test_main.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: pretix-map
|
|
3
|
-
Version: 0.1.
|
|
3
|
+
Version: 0.1.7
|
|
4
4
|
Summary: An overview map of the catchment area of previous orders. Measured by postcode
|
|
5
5
|
Author-email: MarkenJaden <jjsch1410@gmail.com>
|
|
6
6
|
Maintainer-email: MarkenJaden <jjsch1410@gmail.com>
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: pretix-map
|
|
3
|
-
Version: 0.1.
|
|
3
|
+
Version: 0.1.7
|
|
4
4
|
Summary: An overview map of the catchment area of previous orders. Measured by postcode
|
|
5
5
|
Author-email: MarkenJaden <jjsch1410@gmail.com>
|
|
6
6
|
Maintainer-email: MarkenJaden <jjsch1410@gmail.com>
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
__version__ = "0.1.7"
|
|
@@ -0,0 +1,101 @@
|
|
|
1
|
+
.plugin-map-content-wrapper {
|
|
2
|
+
display: flex;
|
|
3
|
+
flex-direction: column;
|
|
4
|
+
height: calc(100vh - 100px);
|
|
5
|
+
min-height: 450px;
|
|
6
|
+
}
|
|
7
|
+
|
|
8
|
+
.plugin-map-content-wrapper h1,
|
|
9
|
+
.plugin-map-content-wrapper .form-group {
|
|
10
|
+
flex-shrink: 0;
|
|
11
|
+
margin-bottom: 1em;
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
.map-wrapper {
|
|
15
|
+
flex-grow: 1;
|
|
16
|
+
position: relative;
|
|
17
|
+
min-height: 0;
|
|
18
|
+
border: 1px solid #ccc;
|
|
19
|
+
display: flex;
|
|
20
|
+
flex-direction: column;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
#sales-map-container {
|
|
24
|
+
height: 100%;
|
|
25
|
+
width: 100%;
|
|
26
|
+
display: block;
|
|
27
|
+
position: relative;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
/* Side-by-side comparison styles */
|
|
31
|
+
.map-split-container {
|
|
32
|
+
display: flex;
|
|
33
|
+
flex-direction: row;
|
|
34
|
+
width: 100%;
|
|
35
|
+
flex-grow: 1;
|
|
36
|
+
min-height: 500px;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
#sales-map-compare-container {
|
|
40
|
+
width: 50%;
|
|
41
|
+
height: 100%;
|
|
42
|
+
border-left: 2px solid #fff;
|
|
43
|
+
display: none;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
.map-split-active #sales-map-container {
|
|
47
|
+
width: 50% !important;
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
.map-split-active #sales-map-compare-container {
|
|
51
|
+
display: block;
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
.map-label-overlay {
|
|
55
|
+
position: absolute;
|
|
56
|
+
top: 10px;
|
|
57
|
+
left: 50px;
|
|
58
|
+
z-index: 1000;
|
|
59
|
+
background: rgba(255, 255, 255, 0.8);
|
|
60
|
+
padding: 2px 8px;
|
|
61
|
+
border-radius: 4px;
|
|
62
|
+
pointer-events: none;
|
|
63
|
+
font-weight: bold;
|
|
64
|
+
border: 1px solid #ccc;
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
#timeline-chart-container {
|
|
68
|
+
box-shadow: inset 0 1px 3px rgba(0,0,0,0.1);
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
.timeline-milestone-marker:hover span {
|
|
72
|
+
background: rgba(255, 255, 255, 1) !important;
|
|
73
|
+
z-index: 10;
|
|
74
|
+
border: 1px solid #ccc;
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
#map-status-overlay {
|
|
78
|
+
position: absolute;
|
|
79
|
+
top: 0;
|
|
80
|
+
left: 0;
|
|
81
|
+
width: 100%;
|
|
82
|
+
height: 100%;
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
#map-status-overlay p {
|
|
86
|
+
padding: 1em;
|
|
87
|
+
background: #fff;
|
|
88
|
+
border-radius: 5px;
|
|
89
|
+
box-shadow: 0 2px 5px rgba(0, 0, 0, 0.2);
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
#heatmap-options-panel input[type=range] {
|
|
93
|
+
width: 100%;
|
|
94
|
+
display: block;
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
#map-status-overlay p.text-danger {
|
|
98
|
+
color: #a94442;
|
|
99
|
+
background-color: #f2dede;
|
|
100
|
+
border-color: #ebccd1;
|
|
101
|
+
}
|
{pretix_map-0.1.5 → pretix_map-0.1.7}/pretix_mapplugin/static/pretix_mapplugin/js/salesmap.js
RENAMED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
document.addEventListener('DOMContentLoaded', function () {
|
|
2
|
-
console.log("Sales Map JS Loaded (Charts, CSP Fix, Toggle Mode Fix)");
|
|
2
|
+
console.log("Sales Map JS Loaded (Charts, CSP Fix, Toggle Mode Fix, Split-View)");
|
|
3
3
|
|
|
4
4
|
// --- Leaflet Setup ---
|
|
5
5
|
try {
|
|
@@ -40,23 +40,38 @@ document.addEventListener('DOMContentLoaded', function () {
|
|
|
40
40
|
filterCheckboxes: document.getElementById('filter-checkboxes'),
|
|
41
41
|
radiusIn: document.getElementById('heatmap-radius'),
|
|
42
42
|
blurIn: document.getElementById('heatmap-blur'),
|
|
43
|
-
maxZoomIn: document.getElementById('heatmap-maxZoom'),
|
|
43
|
+
maxZoomIn: document.getElementById('heatmap-maxZoom'),
|
|
44
44
|
radiusVal: document.getElementById('radius-value'),
|
|
45
45
|
blurVal: document.getElementById('blur-value'),
|
|
46
|
-
maxZoomVal: document.getElementById('maxzoom-value'),
|
|
46
|
+
maxZoomVal: document.getElementById('maxzoom-value'),
|
|
47
|
+
gridSizeSlider: document.getElementById('grid-size-slider'),
|
|
48
|
+
gridSizeVal: document.getElementById('grid-size-value'),
|
|
49
|
+
heatmapControls: document.getElementById('heatmap-only-controls'),
|
|
50
|
+
gridControls: document.getElementById('grid-only-controls'),
|
|
51
|
+
mapSplitRoot: document.getElementById('map-split-root'),
|
|
52
|
+
compareMapContainer: document.getElementById('sales-map-compare-container'),
|
|
53
|
+
compareLabel: document.getElementById('compare-map-label'),
|
|
54
|
+
mainLabel: document.getElementById('main-map-label'),
|
|
47
55
|
heatmapReset: document.getElementById('heatmap-reset-btn'),
|
|
48
56
|
timeSlider: document.getElementById('timeline-slider'),
|
|
49
57
|
timeDisplay: document.getElementById('timeline-date-display'),
|
|
50
58
|
timePlayBtn: document.getElementById('timeline-play-btn'),
|
|
51
59
|
timeCount: document.getElementById('timeline-count-display'),
|
|
52
|
-
intervalSelect: document.getElementById('timeline-interval-select')
|
|
60
|
+
intervalSelect: document.getElementById('timeline-interval-select'),
|
|
61
|
+
timelineChart: document.getElementById('timeline-volume-chart'),
|
|
62
|
+
timelineMarkers: document.getElementById('timeline-milestone-markers'),
|
|
63
|
+
itemAvailContainer: document.getElementById('timeline-item-availability'),
|
|
64
|
+
itemAvailList: document.getElementById('item-availability-list')
|
|
53
65
|
};
|
|
54
66
|
|
|
55
67
|
// --- State ---
|
|
56
|
-
let map = null, allData = [], filteredData = [], displayData = [], milestones = [], currency = "EUR";
|
|
68
|
+
let map = null, mapCompare = null, allData = [], filteredData = [], displayData = [], milestones = [], currency = "EUR";
|
|
69
|
+
let itemAvailData = {};
|
|
70
|
+
let compareData = [], compareFilteredData = [], compareDisplayData = [];
|
|
57
71
|
let pinLayer = null, heatmapLayer = null, gridLayer = null, comparisonLayer = null, eventMarkerLayer = null;
|
|
72
|
+
let pinLayerComp = null, heatmapLayerComp = null, gridLayerComp = null, eventMarkerLayerComp = null;
|
|
58
73
|
let currentView = 'pins', isClustering = true, isWeighted = false, showCanceled = false;
|
|
59
|
-
let isPlaying = false, playInterval = null;
|
|
74
|
+
let isPlaying = false, playInterval = null, currentCompareSlug = "";
|
|
60
75
|
let availableItems = new Set(), selectedItems = new Set();
|
|
61
76
|
let currentDisplayMode = 'map'; // map | list | stats
|
|
62
77
|
let charts = {};
|
|
@@ -73,8 +88,26 @@ document.addEventListener('DOMContentLoaded', function () {
|
|
|
73
88
|
function init() {
|
|
74
89
|
if (!sel.map) return;
|
|
75
90
|
const dataUrl = sel.map.dataset.dataUrl;
|
|
91
|
+
|
|
76
92
|
map = L.map('sales-map-container').setView([48.85, 2.35], 5);
|
|
77
93
|
L.tileLayer('https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png', { attribution: '© OpenStreetMap contributors' }).addTo(map);
|
|
94
|
+
|
|
95
|
+
mapCompare = L.map('sales-map-compare-container', { zoomControl: false }).setView([48.85, 2.35], 5);
|
|
96
|
+
L.tileLayer('https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png', { attribution: '© OpenStreetMap contributors' }).addTo(mapCompare);
|
|
97
|
+
|
|
98
|
+
// Synchronize maps
|
|
99
|
+
let isSyncing = false;
|
|
100
|
+
const sync = (src, dest) => {
|
|
101
|
+
if (isSyncing) return;
|
|
102
|
+
isSyncing = true;
|
|
103
|
+
dest.setView(src.getCenter(), src.getZoom(), { animate: false });
|
|
104
|
+
isSyncing = false;
|
|
105
|
+
};
|
|
106
|
+
map.on('move', () => sync(map, mapCompare));
|
|
107
|
+
mapCompare.on('move', () => sync(mapCompare, map));
|
|
108
|
+
map.on('zoomend', () => sync(map, mapCompare));
|
|
109
|
+
mapCompare.on('zoomend', () => sync(mapCompare, map));
|
|
110
|
+
|
|
78
111
|
setupControls();
|
|
79
112
|
fetchData(dataUrl);
|
|
80
113
|
}
|
|
@@ -85,6 +118,7 @@ document.addEventListener('DOMContentLoaded', function () {
|
|
|
85
118
|
if (data.error) throw new Error(data.error);
|
|
86
119
|
allData = (data.locations || []).map(loc => { loc.ts = loc.date ? new Date(loc.date).getTime() : 0; return loc; }).sort((a, b) => a.ts - b.ts);
|
|
87
120
|
milestones = (data.milestones || []).map(m => { m.ts = new Date(m.date).getTime(); return m; });
|
|
121
|
+
itemAvailData = data.item_availability || {};
|
|
88
122
|
currency = (data.stats && data.stats.currency) ? data.stats.currency : "EUR";
|
|
89
123
|
|
|
90
124
|
extractItems(); buildFilterUI(); initTimeline(); applyFilters();
|
|
@@ -123,15 +157,17 @@ document.addEventListener('DOMContentLoaded', function () {
|
|
|
123
157
|
radius: parseInt(sel.radiusIn.value),
|
|
124
158
|
blur: parseInt(sel.blurIn.value),
|
|
125
159
|
maxZoom: parseInt(sel.maxZoomIn.value),
|
|
126
|
-
minOpacity: 0.4
|
|
160
|
+
minOpacity: 0.4
|
|
127
161
|
});
|
|
128
162
|
}
|
|
129
163
|
};
|
|
130
164
|
[sel.radiusIn, sel.blurIn, sel.maxZoomIn].forEach(i => i.oninput = updateHeat);
|
|
131
165
|
sel.heatmapReset.onclick = () => {
|
|
132
166
|
sel.radiusIn.value = 25; sel.blurIn.value = 15; sel.maxZoomIn.value = 18;
|
|
167
|
+
sel.gridSizeSlider.value = 70;
|
|
133
168
|
updateHeat();
|
|
134
169
|
};
|
|
170
|
+
sel.gridSizeSlider.oninput = () => refreshLayers();
|
|
135
171
|
}
|
|
136
172
|
|
|
137
173
|
function switchDisplayMode(mode) {
|
|
@@ -185,7 +221,6 @@ document.addEventListener('DOMContentLoaded', function () {
|
|
|
185
221
|
});
|
|
186
222
|
}
|
|
187
223
|
|
|
188
|
-
// --- Timeline Logik ---
|
|
189
224
|
function initTimeline() {
|
|
190
225
|
if (!sel.timeSlider) return;
|
|
191
226
|
updateTimelineSlider();
|
|
@@ -207,9 +242,179 @@ document.addEventListener('DOMContentLoaded', function () {
|
|
|
207
242
|
const steps = Array.from(uniqueKeys).sort((a, b) => a - b);
|
|
208
243
|
sel.timeSlider.min = 0; sel.timeSlider.max = steps.length - 1; sel.timeSlider.value = steps.length - 1;
|
|
209
244
|
sel.timeSlider._steps = steps;
|
|
245
|
+
|
|
246
|
+
renderTimelineVolumeChart(steps);
|
|
247
|
+
renderMilestoneMarkers(steps);
|
|
248
|
+
renderItemAvailability(steps);
|
|
210
249
|
updateTimelineDisplay();
|
|
211
250
|
}
|
|
212
251
|
|
|
252
|
+
function renderItemAvailability(steps) {
|
|
253
|
+
if (!sel.itemAvailList || !steps || steps.length === 0) return;
|
|
254
|
+
sel.itemAvailList.innerHTML = '';
|
|
255
|
+
sel.itemAvailContainer.style.display = 'block';
|
|
256
|
+
|
|
257
|
+
const minTs = steps[0];
|
|
258
|
+
const maxTs = steps[steps.length - 1];
|
|
259
|
+
const range = maxTs - minTs;
|
|
260
|
+
|
|
261
|
+
Object.keys(itemAvailData).forEach(itemName => {
|
|
262
|
+
const d = itemAvailData[itemName];
|
|
263
|
+
const firstTs = d.first_sale ? new Date(d.first_sale).getTime() : null;
|
|
264
|
+
const lastTs = d.last_sale ? new Date(d.last_sale).getTime() : null;
|
|
265
|
+
const availFrom = d.available_from ? new Date(d.available_from).getTime() : null;
|
|
266
|
+
const availUntil = d.available_until ? new Date(d.available_until).getTime() : null;
|
|
267
|
+
|
|
268
|
+
if (!firstTs && !availFrom) return;
|
|
269
|
+
|
|
270
|
+
const row = document.createElement('div');
|
|
271
|
+
row.style.height = '14px';
|
|
272
|
+
row.style.marginBottom = '2px';
|
|
273
|
+
row.style.position = 'relative';
|
|
274
|
+
row.style.width = '100%';
|
|
275
|
+
row.style.display = 'flex';
|
|
276
|
+
row.style.alignItems = 'center';
|
|
277
|
+
|
|
278
|
+
const label = document.createElement('div');
|
|
279
|
+
label.textContent = itemName;
|
|
280
|
+
label.style.width = '120px';
|
|
281
|
+
label.style.overflow = 'hidden';
|
|
282
|
+
label.style.textOverflow = 'ellipsis';
|
|
283
|
+
label.style.whiteSpace = 'nowrap';
|
|
284
|
+
label.style.marginRight = '5px';
|
|
285
|
+
row.appendChild(label);
|
|
286
|
+
|
|
287
|
+
const barContainer = document.createElement('div');
|
|
288
|
+
barContainer.style.flexGrow = '1';
|
|
289
|
+
barContainer.style.height = '8px';
|
|
290
|
+
barContainer.style.background = '#f9f9f9';
|
|
291
|
+
barContainer.style.position = 'relative';
|
|
292
|
+
barContainer.style.borderRadius = '4px';
|
|
293
|
+
row.appendChild(barContainer);
|
|
294
|
+
|
|
295
|
+
// Planned availability (light grey bar)
|
|
296
|
+
if (availFrom || availUntil) {
|
|
297
|
+
const start = availFrom ? Math.max(availFrom, minTs) : minTs;
|
|
298
|
+
const end = availUntil ? Math.min(availUntil, maxTs) : maxTs;
|
|
299
|
+
if (start < end) {
|
|
300
|
+
const left = ((start - minTs) / range) * 100;
|
|
301
|
+
const width = ((end - start) / range) * 100;
|
|
302
|
+
const bar = document.createElement('div');
|
|
303
|
+
bar.style.position = 'absolute';
|
|
304
|
+
bar.style.left = `${left}%`;
|
|
305
|
+
bar.style.width = `${width}%`;
|
|
306
|
+
bar.style.height = '100%';
|
|
307
|
+
bar.style.background = '#eee';
|
|
308
|
+
bar.style.borderRadius = '4px';
|
|
309
|
+
bar.title = `${itemName} (Configured Availability)`;
|
|
310
|
+
barContainer.appendChild(bar);
|
|
311
|
+
}
|
|
312
|
+
}
|
|
313
|
+
|
|
314
|
+
// Actual sales period (blue bar)
|
|
315
|
+
if (firstTs && lastTs) {
|
|
316
|
+
const start = Math.max(firstTs, minTs);
|
|
317
|
+
const end = Math.min(lastTs, maxTs);
|
|
318
|
+
if (start <= end) {
|
|
319
|
+
const left = ((start - minTs) / range) * 100;
|
|
320
|
+
const width = Math.max(((end - start) / range) * 100, 0.5); // Min width for visibility
|
|
321
|
+
const bar = document.createElement('div');
|
|
322
|
+
bar.style.position = 'absolute';
|
|
323
|
+
bar.style.left = `${left}%`;
|
|
324
|
+
bar.style.width = `${width}%`;
|
|
325
|
+
bar.style.height = '100%';
|
|
326
|
+
bar.style.background = 'rgba(54, 162, 235, 0.6)';
|
|
327
|
+
bar.style.borderRadius = '4px';
|
|
328
|
+
bar.style.zIndex = '1';
|
|
329
|
+
bar.title = `${itemName} (Actual Sales)`;
|
|
330
|
+
barContainer.appendChild(bar);
|
|
331
|
+
}
|
|
332
|
+
}
|
|
333
|
+
|
|
334
|
+
sel.itemAvailList.appendChild(row);
|
|
335
|
+
});
|
|
336
|
+
}
|
|
337
|
+
|
|
338
|
+
function renderTimelineVolumeChart(steps) {
|
|
339
|
+
if (!sel.timelineChart) return;
|
|
340
|
+
const ctx = sel.timelineChart.getContext('2d');
|
|
341
|
+
if (charts.timeline) charts.timeline.destroy();
|
|
342
|
+
|
|
343
|
+
// Calculate counts per step from filteredData
|
|
344
|
+
const stepCounts = steps.map(ts => {
|
|
345
|
+
const nextTsIndex = steps.indexOf(ts) + 1;
|
|
346
|
+
const nextTs = nextTsIndex < steps.length ? steps[nextTsIndex] : Infinity;
|
|
347
|
+
return filteredData.filter(d => d.ts >= ts && d.ts < nextTs).length;
|
|
348
|
+
});
|
|
349
|
+
|
|
350
|
+
charts.timeline = new Chart(ctx, {
|
|
351
|
+
type: 'bar',
|
|
352
|
+
data: {
|
|
353
|
+
labels: steps.map(ts => new Date(ts).toLocaleDateString()),
|
|
354
|
+
datasets: [{
|
|
355
|
+
data: stepCounts,
|
|
356
|
+
backgroundColor: 'rgba(54, 162, 235, 0.4)',
|
|
357
|
+
borderWidth: 0,
|
|
358
|
+
barPercentage: 1.0,
|
|
359
|
+
categoryPercentage: 1.0
|
|
360
|
+
}]
|
|
361
|
+
},
|
|
362
|
+
options: {
|
|
363
|
+
responsive: true,
|
|
364
|
+
maintainAspectRatio: false,
|
|
365
|
+
plugins: { legend: { display: false }, tooltip: { enabled: false } },
|
|
366
|
+
scales: {
|
|
367
|
+
x: { display: false },
|
|
368
|
+
y: { display: false, beginAtZero: true }
|
|
369
|
+
}
|
|
370
|
+
}
|
|
371
|
+
});
|
|
372
|
+
}
|
|
373
|
+
|
|
374
|
+
function renderMilestoneMarkers(steps) {
|
|
375
|
+
if (!sel.timelineMarkers || !steps || steps.length === 0) return;
|
|
376
|
+
sel.timelineMarkers.innerHTML = '';
|
|
377
|
+
|
|
378
|
+
milestones.forEach(m => {
|
|
379
|
+
// Find closest step index
|
|
380
|
+
let closestIdx = -1;
|
|
381
|
+
let minDiff = Infinity;
|
|
382
|
+
steps.forEach((ts, idx) => {
|
|
383
|
+
const diff = Math.abs(ts - m.ts);
|
|
384
|
+
if (diff < minDiff) {
|
|
385
|
+
minDiff = diff;
|
|
386
|
+
closestIdx = idx;
|
|
387
|
+
}
|
|
388
|
+
});
|
|
389
|
+
|
|
390
|
+
if (closestIdx !== -1 && minDiff < 86400000 * 2) { // Only show if close enough (2 days)
|
|
391
|
+
const percent = (closestIdx / (steps.length - 1)) * 100;
|
|
392
|
+
const marker = document.createElement('div');
|
|
393
|
+
marker.className = 'timeline-milestone-marker';
|
|
394
|
+
marker.style.position = 'absolute';
|
|
395
|
+
marker.style.left = `${percent}%`;
|
|
396
|
+
marker.style.top = '0';
|
|
397
|
+
marker.style.width = '2px';
|
|
398
|
+
marker.style.height = '100%';
|
|
399
|
+
marker.style.background = '#d9534f';
|
|
400
|
+
marker.style.zIndex = '2';
|
|
401
|
+
|
|
402
|
+
const label = document.createElement('span');
|
|
403
|
+
label.textContent = m.label;
|
|
404
|
+
label.style.position = 'absolute';
|
|
405
|
+
label.style.top = '2px';
|
|
406
|
+
label.style.left = '4px';
|
|
407
|
+
label.style.fontSize = '9px';
|
|
408
|
+
label.style.whiteSpace = 'nowrap';
|
|
409
|
+
label.style.background = 'rgba(255,255,255,0.7)';
|
|
410
|
+
label.style.padding = '0 2px';
|
|
411
|
+
|
|
412
|
+
marker.appendChild(label);
|
|
413
|
+
sel.timelineMarkers.appendChild(marker);
|
|
414
|
+
}
|
|
415
|
+
});
|
|
416
|
+
}
|
|
417
|
+
|
|
213
418
|
function updateTimelineDisplay() {
|
|
214
419
|
const steps = sel.timeSlider._steps;
|
|
215
420
|
if (!steps) return;
|
|
@@ -217,7 +422,12 @@ document.addEventListener('DOMContentLoaded', function () {
|
|
|
217
422
|
const dateObj = new Date(currentTs);
|
|
218
423
|
const activeMilestone = milestones.find(m => new Date(m.date).toDateString() === dateObj.toDateString());
|
|
219
424
|
sel.timeDisplay.innerHTML = dateObj.toLocaleDateString() + (activeMilestone ? ` — <strong>★ ${activeMilestone.label}</strong>` : "");
|
|
425
|
+
|
|
220
426
|
displayData = filteredData.filter(d => d.ts === 0 || d.ts <= currentTs);
|
|
427
|
+
if (compareFilteredData.length > 0) {
|
|
428
|
+
compareDisplayData = compareFilteredData.filter(d => d.ts === 0 || d.ts <= currentTs);
|
|
429
|
+
}
|
|
430
|
+
|
|
221
431
|
if (sel.timeCount) sel.timeCount.textContent = displayData.length;
|
|
222
432
|
refreshLayers();
|
|
223
433
|
}
|
|
@@ -238,13 +448,38 @@ document.addEventListener('DOMContentLoaded', function () {
|
|
|
238
448
|
function refreshLayers() {
|
|
239
449
|
if (!map) return;
|
|
240
450
|
[pinLayer, heatmapLayer, gridLayer].forEach(l => { if (l && map.hasLayer(l)) map.removeLayer(l); });
|
|
451
|
+
if (mapCompare) {
|
|
452
|
+
[pinLayerComp, heatmapLayerComp, gridLayerComp, eventMarkerLayerComp].forEach(l => { if (l && mapCompare.hasLayer(l)) mapCompare.removeLayer(l); });
|
|
453
|
+
}
|
|
454
|
+
if (comparisonLayer && map.hasLayer(comparisonLayer)) map.removeLayer(comparisonLayer);
|
|
455
|
+
|
|
456
|
+
const isSplitNeeded = currentCompareSlug && (currentView === 'heatmap' || currentView === 'grid');
|
|
457
|
+
if (isSplitNeeded) {
|
|
458
|
+
sel.mapSplitRoot.classList.add('map-split-active');
|
|
459
|
+
sel.mainLabel.style.display = 'block';
|
|
460
|
+
sel.compareMapContainer.style.display = 'block';
|
|
461
|
+
setTimeout(() => { map.invalidateSize(); mapCompare.invalidateSize(); }, 50);
|
|
462
|
+
} else {
|
|
463
|
+
sel.mapSplitRoot.classList.remove('map-split-active');
|
|
464
|
+
sel.mainLabel.style.display = 'none';
|
|
465
|
+
sel.compareMapContainer.style.display = 'none';
|
|
466
|
+
setTimeout(() => { map.invalidateSize(); }, 50);
|
|
467
|
+
}
|
|
468
|
+
|
|
241
469
|
createPinLayer(); createHeatmapLayer(); createGridLayer();
|
|
470
|
+
if (isSplitNeeded) {
|
|
471
|
+
createPinLayer(true); createHeatmapLayer(true); createGridLayer(true);
|
|
472
|
+
} else if (currentCompareSlug && currentView === 'pins') {
|
|
473
|
+
const dots = compareData.map(l => l.lat ? L.circleMarker([l.lat, l.lon], { radius: 4, color: '#888', fillOpacity: 0.5 }).bindTooltip("Comparison") : null).filter(l => l);
|
|
474
|
+
comparisonLayer = L.layerGroup(dots);
|
|
475
|
+
}
|
|
242
476
|
showCurrentView();
|
|
243
477
|
}
|
|
244
478
|
|
|
245
|
-
function createPinLayer() {
|
|
246
|
-
|
|
247
|
-
|
|
479
|
+
function createPinLayer(isCompare = false) {
|
|
480
|
+
const data = isCompare ? compareDisplayData : displayData;
|
|
481
|
+
if (data.length === 0) { if(isCompare) pinLayerComp = null; else pinLayer = null; return; }
|
|
482
|
+
const markers = data.map(loc => {
|
|
248
483
|
if (loc.lat == null) return null;
|
|
249
484
|
let icon = icons.blue;
|
|
250
485
|
if (loc.status === 'pending') icon = icons.orange;
|
|
@@ -254,25 +489,32 @@ document.addEventListener('DOMContentLoaded', function () {
|
|
|
254
489
|
if (loc.order_url) m.on('click', () => window.open(loc.order_url, '_blank'));
|
|
255
490
|
return m;
|
|
256
491
|
}).filter(m => m !== null);
|
|
257
|
-
|
|
492
|
+
const layer = isClustering ? L.markerClusterGroup().addLayers(markers) : L.layerGroup(markers);
|
|
493
|
+
if (isCompare) pinLayerComp = layer; else pinLayer = layer;
|
|
258
494
|
}
|
|
259
495
|
|
|
260
|
-
function createHeatmapLayer() {
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
496
|
+
function createHeatmapLayer(isCompare = false) {
|
|
497
|
+
const data = isCompare ? compareDisplayData : displayData;
|
|
498
|
+
if (data.length === 0) { if(isCompare) heatmapLayerComp = null; else heatmapLayer = null; return; }
|
|
499
|
+
let maxRev = Math.max(...data.map(d => d.revenue || 0), 1);
|
|
500
|
+
const points = data.map(l => [l.lat, l.lon, isWeighted ? (l.revenue || 0) / maxRev : 1.0]);
|
|
501
|
+
const layer = L.heatLayer(points, {
|
|
265
502
|
radius: parseInt(sel.radiusIn.value),
|
|
266
503
|
blur: parseInt(sel.blurIn.value),
|
|
267
504
|
maxZoom: parseInt(sel.maxZoomIn.value),
|
|
268
505
|
minOpacity: 0.4
|
|
269
506
|
});
|
|
507
|
+
if (isCompare) heatmapLayerComp = layer; else heatmapLayer = layer;
|
|
270
508
|
}
|
|
271
509
|
|
|
272
|
-
function createGridLayer() {
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
510
|
+
function createGridLayer(isCompare = false) {
|
|
511
|
+
const data = isCompare ? compareDisplayData : displayData;
|
|
512
|
+
if (data.length === 0) { if(isCompare) gridLayerComp = null; else gridLayer = null; return; }
|
|
513
|
+
const rawVal = parseInt(sel.gridSizeSlider.value);
|
|
514
|
+
const gridSize = 0.4 * Math.pow(rawVal / 100, 2);
|
|
515
|
+
sel.gridSizeVal.textContent = gridSize.toFixed(3);
|
|
516
|
+
const bins = {}; let maxVal = 0;
|
|
517
|
+
data.forEach(l => {
|
|
276
518
|
if (l.lat == null) return;
|
|
277
519
|
const key = `${Math.floor(l.lat/gridSize)},${Math.floor(l.lon/gridSize)}`;
|
|
278
520
|
if (!bins[key]) bins[key] = { count: 0, revenue: 0, lat: Math.floor(l.lat/gridSize)*gridSize, lon: Math.floor(l.lon/gridSize)*gridSize };
|
|
@@ -284,32 +526,57 @@ document.addEventListener('DOMContentLoaded', function () {
|
|
|
284
526
|
const i = (isWeighted ? bin.revenue : bin.count) / maxVal;
|
|
285
527
|
return L.rectangle([[bin.lat, bin.lon], [bin.lat+gridSize, bin.lon+gridSize]], { color: 'transparent', fillColor: isWeighted ? 'green' : 'blue', fillOpacity: 0.2 + (i*0.6) }).bindTooltip(isWeighted ? `Revenue: ${bin.revenue.toFixed(2)}` : `Count: ${bin.count}`);
|
|
286
528
|
});
|
|
287
|
-
|
|
529
|
+
const layer = L.layerGroup(rects);
|
|
530
|
+
if (isCompare) gridLayerComp = layer; else gridLayer = layer;
|
|
288
531
|
}
|
|
289
532
|
|
|
290
533
|
function showCurrentView() {
|
|
291
534
|
if (!map) return;
|
|
292
|
-
[pinLayer, heatmapLayer, gridLayer].forEach(l => { if (l && map.hasLayer(l)) map.removeLayer(l); });
|
|
293
535
|
if (currentView === 'pins' && pinLayer) map.addLayer(pinLayer);
|
|
294
536
|
else if (currentView === 'heatmap' && heatmapLayer) map.addLayer(heatmapLayer);
|
|
295
537
|
else if (currentView === 'grid' && gridLayer) map.addLayer(gridLayer);
|
|
296
|
-
|
|
538
|
+
|
|
539
|
+
sel.heatmapPanel.style.display = (currentView === 'heatmap' || currentView === 'grid' ? 'block' : 'none');
|
|
540
|
+
sel.heatmapControls.style.display = (currentView === 'heatmap' ? 'block' : 'none');
|
|
541
|
+
sel.gridControls.style.display = (currentView === 'grid' ? 'block' : 'none');
|
|
542
|
+
|
|
297
543
|
if (sel.clusterToggle) sel.clusterToggle.style.display = (currentView === 'pins' ? 'inline-block' : 'none');
|
|
298
544
|
if (eventMarkerLayer) map.addLayer(eventMarkerLayer);
|
|
299
|
-
if (comparisonLayer) map.addLayer(comparisonLayer);
|
|
545
|
+
if (comparisonLayer && currentView === 'pins') map.addLayer(comparisonLayer);
|
|
546
|
+
|
|
547
|
+
if (mapCompare && sel.mapSplitRoot.classList.contains('map-split-active')) {
|
|
548
|
+
if (currentView === 'pins' && pinLayerComp) mapCompare.addLayer(pinLayerComp);
|
|
549
|
+
else if (currentView === 'heatmap' && heatmapLayerComp) mapCompare.addLayer(heatmapLayerComp);
|
|
550
|
+
else if (currentView === 'grid' && gridLayerComp) mapCompare.addLayer(gridLayerComp);
|
|
551
|
+
if (eventMarkerLayerComp) mapCompare.addLayer(eventMarkerLayerComp);
|
|
552
|
+
}
|
|
300
553
|
}
|
|
301
554
|
|
|
302
|
-
function renderEventMarker(em) {
|
|
303
|
-
|
|
304
|
-
|
|
555
|
+
function renderEventMarker(em, isCompare = false) {
|
|
556
|
+
const layer = L.marker([em.lat, em.lon], { icon: icons.red, zIndexOffset: 2000 }).bindTooltip(`<strong>EVENT: ${em.name}</strong><br>${em.location}`);
|
|
557
|
+
if (isCompare) {
|
|
558
|
+
if (eventMarkerLayerComp && mapCompare.hasLayer(eventMarkerLayerComp)) mapCompare.removeLayer(eventMarkerLayerComp);
|
|
559
|
+
eventMarkerLayerComp = layer.addTo(mapCompare);
|
|
560
|
+
} else {
|
|
561
|
+
if (eventMarkerLayer && map.hasLayer(eventMarkerLayer)) map.removeLayer(eventMarkerLayer);
|
|
562
|
+
eventMarkerLayer = layer.addTo(map);
|
|
563
|
+
}
|
|
305
564
|
}
|
|
306
565
|
|
|
307
566
|
function loadComparison(slug) {
|
|
308
|
-
|
|
567
|
+
currentCompareSlug = slug;
|
|
568
|
+
if (!slug) {
|
|
569
|
+
compareData = []; compareFilteredData = []; compareDisplayData = [];
|
|
570
|
+
sel.compareLabel.textContent = "";
|
|
571
|
+
refreshLayers(); return;
|
|
572
|
+
}
|
|
573
|
+
const option = sel.compareSelect.options[sel.compareSelect.selectedIndex];
|
|
574
|
+
sel.compareLabel.textContent = option.text;
|
|
309
575
|
const newUrl = sel.map.dataset.dataUrl.replace(/\/event\/([^\/]+)\/([^\/]+)\//, `/event/$1/${slug}/`);
|
|
310
576
|
fetch(newUrl).then(r => r.json()).then(data => {
|
|
311
|
-
|
|
312
|
-
|
|
577
|
+
compareData = (data.locations || []).map(loc => { loc.ts = loc.date ? new Date(loc.date).getTime() : 0; return loc; }).sort((a, b) => a.ts - b.ts);
|
|
578
|
+
if (data.event_marker) renderEventMarker(data.event_marker, true);
|
|
579
|
+
applyFilters();
|
|
313
580
|
});
|
|
314
581
|
}
|
|
315
582
|
|
|
@@ -325,7 +592,9 @@ document.addEventListener('DOMContentLoaded', function () {
|
|
|
325
592
|
}
|
|
326
593
|
function applyFilters() {
|
|
327
594
|
showCanceled = sel.showCanceledCheck.checked;
|
|
328
|
-
|
|
595
|
+
const filterFn = loc => (loc.items.length === 0 || loc.items.some(i => selectedItems.has(i))) && (showCanceled || loc.status !== 'canceled');
|
|
596
|
+
filteredData = allData.filter(filterFn);
|
|
597
|
+
if (compareData.length > 0) compareFilteredData = compareData.filter(filterFn);
|
|
329
598
|
updateTimelineSlider();
|
|
330
599
|
}
|
|
331
600
|
|
|
@@ -339,4 +608,4 @@ document.addEventListener('DOMContentLoaded', function () {
|
|
|
339
608
|
}
|
|
340
609
|
|
|
341
610
|
init();
|
|
342
|
-
});
|
|
611
|
+
});
|
{pretix_map-0.1.5 → pretix_map-0.1.7}/pretix_mapplugin/templates/pretix_mapplugin/map_page.html
RENAMED
|
@@ -52,7 +52,9 @@
|
|
|
52
52
|
<div class="form-group">
|
|
53
53
|
<form action="{% url 'plugins:pretix_mapplugin:event.settings.salesmap.trigger' organizer=request.organizer.slug event=request.event.slug %}" method="post" style="display: inline-block;">
|
|
54
54
|
{% csrf_token %}
|
|
55
|
-
<button type="submit" class="btn btn-warning"
|
|
55
|
+
<button type="submit" class="btn btn-warning">
|
|
56
|
+
<i class="fa fa-refresh"></i> {% trans "Geocode all" %}
|
|
57
|
+
</button>
|
|
56
58
|
</form>
|
|
57
59
|
</div>
|
|
58
60
|
{% endif %}
|
|
@@ -60,13 +62,22 @@
|
|
|
60
62
|
|
|
61
63
|
<div id="heatmap-options-panel" class="panel panel-default" style="display: none; padding: 10px; min-width: 300px;">
|
|
62
64
|
<div style="display: flex; justify-content: space-between;">
|
|
63
|
-
<h5 style="margin: 0;">
|
|
65
|
+
<h5 style="margin: 0;">{% trans "View Options" %}</h5>
|
|
64
66
|
<button id="heatmap-reset-btn" class="btn btn-xs btn-link">Reset</button>
|
|
65
67
|
</div>
|
|
66
68
|
<div class="form-horizontal" style="margin-top: 5px;">
|
|
67
|
-
<div
|
|
68
|
-
|
|
69
|
-
|
|
69
|
+
<div id="heatmap-only-controls">
|
|
70
|
+
<div class="form-group form-group-sm"><label class="col-sm-4">Radius</label><div class="col-sm-6"><input type="range" id="heatmap-radius" min="1" max="100" value="25"></div><div class="col-sm-2"><span id="radius-value">25</span></div></div>
|
|
71
|
+
<div class="form-group form-group-sm"><label class="col-sm-4">Blur</label><div class="col-sm-6"><input type="range" id="heatmap-blur" min="1" max="50" value="15"></div><div class="col-sm-2"><span id="blur-value">15</span></div></div>
|
|
72
|
+
<div class="form-group form-group-sm"><label class="col-sm-4">Max Zoom</label><div class="col-sm-6"><input type="range" id="heatmap-maxZoom" min="1" max="18" value="18"></div><div class="col-sm-2"><span id="maxzoom-value">18</span></div></div>
|
|
73
|
+
</div>
|
|
74
|
+
<div id="grid-only-controls" style="display: none;">
|
|
75
|
+
<div class="form-group form-group-sm">
|
|
76
|
+
<label class="col-sm-4">{% trans "Resolution" %}</label>
|
|
77
|
+
<div class="col-sm-6"><input type="range" id="grid-size-slider" min="1" max="100" value="70"></div>
|
|
78
|
+
<div class="col-sm-2"><span id="grid-size-value">0.12</span></div>
|
|
79
|
+
</div>
|
|
80
|
+
</div>
|
|
70
81
|
</div>
|
|
71
82
|
</div>
|
|
72
83
|
|
|
@@ -76,11 +87,28 @@
|
|
|
76
87
|
</div>
|
|
77
88
|
</div>
|
|
78
89
|
|
|
79
|
-
<div class="map-wrapper"
|
|
80
|
-
<div id="
|
|
90
|
+
<div class="map-wrapper">
|
|
91
|
+
<div id="map-split-root" class="map-split-container">
|
|
92
|
+
<div id="sales-map-container" style="min-height: 500px;" data-data-url="{% url 'plugins:pretix_mapplugin:event.settings.salesmap.data' organizer=request.organizer.slug event=request.event.slug %}">
|
|
93
|
+
<div class="map-label-overlay" id="main-map-label" style="display: none;">{{ request.event.name }}</div>
|
|
94
|
+
</div>
|
|
95
|
+
<div id="sales-map-compare-container" style="min-height: 500px;">
|
|
96
|
+
<div class="map-label-overlay" id="compare-map-label"></div>
|
|
97
|
+
</div>
|
|
98
|
+
</div>
|
|
81
99
|
|
|
82
100
|
<div id="failed-orders-container" style="display: none; padding: 20px; background: white; flex-grow: 1; overflow-y: auto;">
|
|
83
|
-
<
|
|
101
|
+
<div style="display: flex; justify-content: space-between; align-items: center; margin-bottom: 20px;">
|
|
102
|
+
<h3 style="margin: 0;">{% trans "Orders with missing geodata" %}</h3>
|
|
103
|
+
{% if "can_change_event_settings" in request.eventperm %}
|
|
104
|
+
<form action="{% url 'plugins:pretix_mapplugin:event.settings.salesmap.trigger' organizer=request.organizer.slug event=request.event.slug %}" method="post">
|
|
105
|
+
{% csrf_token %}
|
|
106
|
+
<button type="submit" class="btn btn-warning">
|
|
107
|
+
<i class="fa fa-refresh"></i> {% trans "Retry all geocoding" %}
|
|
108
|
+
</button>
|
|
109
|
+
</form>
|
|
110
|
+
{% endif %}
|
|
111
|
+
</div>
|
|
84
112
|
<table class="table table-hover">
|
|
85
113
|
<thead><tr><th>Code</th><th>Address</th><th>Action</th></tr></thead>
|
|
86
114
|
<tbody id="failed-orders-tbody"></tbody>
|
|
@@ -123,8 +151,8 @@
|
|
|
123
151
|
</div>
|
|
124
152
|
</div>
|
|
125
153
|
|
|
126
|
-
<div id="timeline-controls" style="margin-top: 10px; padding: 10px; background: #f5f5f5; border-radius: 4px; border: 1px solid #ddd;">
|
|
127
|
-
<div style="display: flex; justify-content: space-between; align-items: center;">
|
|
154
|
+
<div id="timeline-controls" style="margin-top: 10px; padding: 10px; background: #f5f5f5; border-radius: 4px; border: 1px solid #ddd; position: relative;">
|
|
155
|
+
<div style="display: flex; justify-content: space-between; align-items: center; margin-bottom: 5px;">
|
|
128
156
|
<div>
|
|
129
157
|
<label style="margin-bottom: 0;">{% trans "Timeline:" %} <span id="timeline-date-display" style="font-weight: normal;"></span></label>
|
|
130
158
|
<span id="timeline-count-display" class="badge"></span>
|
|
@@ -138,9 +166,16 @@
|
|
|
138
166
|
</select>
|
|
139
167
|
</div>
|
|
140
168
|
</div>
|
|
141
|
-
<div style="
|
|
169
|
+
<div id="timeline-chart-container" style="position: relative; height: 40px; margin-bottom: 5px; background: white; border: 1px solid #eee; overflow: hidden;">
|
|
170
|
+
<canvas id="timeline-volume-chart" style="position: absolute; top: 0; left: 0; width: 100%; height: 100%;"></canvas>
|
|
171
|
+
<div id="timeline-milestone-markers" style="position: absolute; top: 0; left: 0; width: 100%; height: 100%; pointer-events: none;"></div>
|
|
172
|
+
</div>
|
|
173
|
+
<div id="timeline-item-availability" style="margin-bottom: 10px; font-size: 10px; max-height: 100px; overflow-y: auto; background: white; border: 1px solid #eee; display: none;">
|
|
174
|
+
<div id="item-availability-list" style="padding: 5px; position: relative;"></div>
|
|
175
|
+
</div>
|
|
176
|
+
<div style="display: flex; gap: 10px; align-items: center;">
|
|
142
177
|
<button id="timeline-play-btn" class="btn btn-sm btn-default"><i class="fa fa-play"></i> Play</button>
|
|
143
|
-
<input type="range" id="timeline-slider" min="0" max="100" value="100" style="flex-grow: 1; cursor: pointer;">
|
|
178
|
+
<input type="range" id="timeline-slider" min="0" max="100" value="100" style="flex-grow: 1; cursor: pointer; position: relative; z-index: 5;">
|
|
144
179
|
</div>
|
|
145
180
|
</div>
|
|
146
181
|
</div>
|
|
@@ -32,11 +32,25 @@ except ImportError:
|
|
|
32
32
|
logger = logging.getLogger(__name__)
|
|
33
33
|
|
|
34
34
|
|
|
35
|
+
from django import forms
|
|
36
|
+
try:
|
|
37
|
+
from pretix.control.forms.widgets import DatePickerWidget
|
|
38
|
+
except ImportError:
|
|
39
|
+
class DatePickerWidget(forms.DateInput):
|
|
40
|
+
def __init__(self, *args, **kwargs):
|
|
41
|
+
attrs = kwargs.get('attrs', {})
|
|
42
|
+
attrs['class'] = attrs.get('class', '') + ' datepickerfield'
|
|
43
|
+
kwargs['attrs'] = attrs
|
|
44
|
+
super().__init__(*args, **kwargs)
|
|
45
|
+
|
|
35
46
|
# --- Milestone Management ---
|
|
36
47
|
MilestoneFormSet = inlineformset_factory(
|
|
37
48
|
Event,
|
|
38
49
|
MapMilestone,
|
|
39
50
|
fields=('date', 'label'),
|
|
51
|
+
widgets={
|
|
52
|
+
'date': DatePickerWidget(),
|
|
53
|
+
},
|
|
40
54
|
extra=1,
|
|
41
55
|
can_delete=True
|
|
42
56
|
)
|
|
@@ -143,6 +157,15 @@ class SalesMapDataView(EventSettingsViewMixin, View):
|
|
|
143
157
|
for m in MapMilestone.objects.filter(event=event)
|
|
144
158
|
]
|
|
145
159
|
|
|
160
|
+
item_availability = {}
|
|
161
|
+
for item in event.items.all():
|
|
162
|
+
item_availability[str(item.name)] = {
|
|
163
|
+
'available_from': item.available_from.isoformat() if item.available_from else None,
|
|
164
|
+
'available_until': item.available_until.isoformat() if item.available_until else None,
|
|
165
|
+
'first_sale': None,
|
|
166
|
+
'last_sale': None,
|
|
167
|
+
}
|
|
168
|
+
|
|
146
169
|
locations_data = []
|
|
147
170
|
failed_orders_data = []
|
|
148
171
|
city_counter = Counter()
|
|
@@ -182,7 +205,14 @@ class SalesMapDataView(EventSettingsViewMixin, View):
|
|
|
182
205
|
|
|
183
206
|
item_names = [str(p.item.name) for p in order.positions.all() if p.item]
|
|
184
207
|
if status != 'canceled':
|
|
185
|
-
for name in item_names:
|
|
208
|
+
for name in item_names:
|
|
209
|
+
item_counter[name] += 1
|
|
210
|
+
# Track sales period
|
|
211
|
+
if name in item_availability:
|
|
212
|
+
if not item_availability[name]['first_sale'] or iso_date < item_availability[name]['first_sale']:
|
|
213
|
+
item_availability[name]['first_sale'] = iso_date
|
|
214
|
+
if not item_availability[name]['last_sale'] or iso_date > item_availability[name]['last_sale']:
|
|
215
|
+
item_availability[name]['last_sale'] = iso_date
|
|
186
216
|
|
|
187
217
|
tooltip_parts = [f"<strong>Order:</strong> {order.code} ({status.upper()})"]
|
|
188
218
|
if order.datetime:
|
|
@@ -211,6 +241,7 @@ class SalesMapDataView(EventSettingsViewMixin, View):
|
|
|
211
241
|
return JsonResponse({
|
|
212
242
|
'locations': locations_data, 'failed_orders': failed_orders_data,
|
|
213
243
|
'event_marker': event_marker, 'milestones': milestones,
|
|
244
|
+
'item_availability': item_availability,
|
|
214
245
|
'stats': {
|
|
215
246
|
'top_cities': city_counter.most_common(10), 'top_items': item_counter.most_common(10),
|
|
216
247
|
'total_orders': len(locations_data) + len(failed_orders_data),
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
__version__ = "0.1.5"
|
|
@@ -1,52 +0,0 @@
|
|
|
1
|
-
.plugin-map-content-wrapper {
|
|
2
|
-
display: flex;
|
|
3
|
-
flex-direction: column;
|
|
4
|
-
height: calc(100vh - 100px);
|
|
5
|
-
min-height: 450px;
|
|
6
|
-
}
|
|
7
|
-
|
|
8
|
-
.plugin-map-content-wrapper h1,
|
|
9
|
-
.plugin-map-content-wrapper .form-group {
|
|
10
|
-
flex-shrink: 0;
|
|
11
|
-
margin-bottom: 1em;
|
|
12
|
-
}
|
|
13
|
-
|
|
14
|
-
.map-wrapper {
|
|
15
|
-
flex-grow: 1;
|
|
16
|
-
position: relative;
|
|
17
|
-
min-height: 0;
|
|
18
|
-
border: 1px solid #ccc;
|
|
19
|
-
}
|
|
20
|
-
|
|
21
|
-
#sales-map-container {
|
|
22
|
-
height: 100%;
|
|
23
|
-
width: 100%;
|
|
24
|
-
display: block;
|
|
25
|
-
position: relative;
|
|
26
|
-
}
|
|
27
|
-
|
|
28
|
-
#map-status-overlay {
|
|
29
|
-
position: absolute;
|
|
30
|
-
top: 0;
|
|
31
|
-
left: 0;
|
|
32
|
-
width: 100%;
|
|
33
|
-
height: 100%;
|
|
34
|
-
}
|
|
35
|
-
|
|
36
|
-
#map-status-overlay p {
|
|
37
|
-
padding: 1em;
|
|
38
|
-
background: #fff;
|
|
39
|
-
border-radius: 5px;
|
|
40
|
-
box-shadow: 0 2px 5px rgba(0, 0, 0, 0.2);
|
|
41
|
-
}
|
|
42
|
-
|
|
43
|
-
#heatmap-options-panel input[type=range] {
|
|
44
|
-
width: 100%;
|
|
45
|
-
display: block;
|
|
46
|
-
}
|
|
47
|
-
|
|
48
|
-
#map-status-overlay p.text-danger {
|
|
49
|
-
color: #a94442;
|
|
50
|
-
background-color: #f2dede;
|
|
51
|
-
border-color: #ebccd1;
|
|
52
|
-
}
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{pretix_map-0.1.5 → pretix_map-0.1.7}/pretix_mapplugin/locale/de_Informal/LC_MESSAGES/django.po
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{pretix_map-0.1.5 → pretix_map-0.1.7}/pretix_mapplugin/templates/pretix_mapplugin/milestones.html
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|