pretix-map 0.1.7__py3-none-any.whl → 0.1.9__py3-none-any.whl

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.
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: pretix-map
3
- Version: 0.1.7
3
+ Version: 0.1.9
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,12 +1,12 @@
1
- pretix_map-0.1.7.dist-info/licenses/LICENSE,sha256=MNMHjIJIjeQzQboYGsgFSRTBUHctF603DDa8MgCNyAg,554
2
- pretix_mapplugin/__init__.py,sha256=YpKDcdV7CqL8n45u267wKtyloM13FSVbOdrqgNZnSLM,22
1
+ pretix_map-0.1.9.dist-info/licenses/LICENSE,sha256=MNMHjIJIjeQzQboYGsgFSRTBUHctF603DDa8MgCNyAg,554
2
+ pretix_mapplugin/__init__.py,sha256=XIaxbMbyiP-L3kguR1GhxirFblTXiHR1lMfDVITvHUI,22
3
3
  pretix_mapplugin/apps.py,sha256=QOo53Z6zX0yB-Rz6I92tAu051sP38hM2iJlP9sk2JEg,802
4
4
  pretix_mapplugin/geocoding.py,sha256=N_JyjYU_JBouwE3oaL3G1wI5jAyezZzS5IBMgI_GdWQ,6053
5
- pretix_mapplugin/models.py,sha256=Ofx8S1UlpQVFt-pC9-_LKecF-Hv2uI2rDq5zJ5S_UJQ,2494
5
+ pretix_mapplugin/models.py,sha256=u_px4eaKWbh1hnUb7bdBEIuJUnvsts1AJ5CMoWLeQO8,2677
6
6
  pretix_mapplugin/signals.py,sha256=_SRT-3TllYzD9kUf0JG3PnuWa6cPHkFFSWInCDGm_j0,3155
7
- pretix_mapplugin/tasks.py,sha256=HKDFC-U1TtYDNrN26QVx133hn0K0BdBUm4qCCYHcgOY,6367
7
+ pretix_mapplugin/tasks.py,sha256=O6Xc6nFh4qMe8O5dBQWvK0pCfy6wcqKlpziZj0L8tLo,6966
8
8
  pretix_mapplugin/urls.py,sha256=gZC74OUSZI2ZUWQXm1pbz8taIV8jA9D2h7FvojRX9Ws,1458
9
- pretix_mapplugin/views.py,sha256=2SviWJunq_ZULYNNGz2F3kkJKHg1vNbhftEfmZ7QuAg,13049
9
+ pretix_mapplugin/views.py,sha256=htySZEkSGmcX_6k1kuzTFRn4uLOZonx4ih1-A58R-Jo,13580
10
10
  pretix_mapplugin/locale/de/LC_MESSAGES/django.mo,sha256=6VVRAqa0ixL-lDA1QwoVvG0wd5ZBwYjaR4P8T73hxhU,269
11
11
  pretix_mapplugin/locale/de/LC_MESSAGES/django.po,sha256=-HtJt_qb8k7C30pVXRuYeh5CIB_ISVt2HEAXGn2rVnw,311
12
12
  pretix_mapplugin/locale/de_Informal/.gitkeep,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
@@ -18,10 +18,11 @@ pretix_mapplugin/management/commands/geocode_existing_orders.py,sha256=ACDCH17Wd
18
18
  pretix_mapplugin/migrations/0001_initial.py,sha256=i33QeU7ioVpk5HpSShdF-ievDUHsjTzKf2OWO9oSkl8,993
19
19
  pretix_mapplugin/migrations/0002_remove_ordergeocodedata_geocoded_timestamp_and_more.py,sha256=U0xDF4KcLqb1QTuDJvilr9NEECVorM7W8ll86RDWi-4,857
20
20
  pretix_mapplugin/migrations/0003_mapmilestone.py,sha256=2fcuMOSWcyF-wReVuGs53_a8pLpYLfNoCIIAekdRSUQ,991
21
+ pretix_mapplugin/migrations/0004_ordergeocodedata_success_level.py,sha256=Skl-dBqJvpGCTD8G302c7LnKA5ksAdkUntJdxhhJsvE,447
21
22
  pretix_mapplugin/migrations/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
22
23
  pretix_mapplugin/static/pretix_mapplugin/.gitkeep,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
23
24
  pretix_mapplugin/static/pretix_mapplugin/css/salesmap.css,sha256=HgKozvQjWTzxJV4ua1hT9PiDzrvUSWrMnYRUvTTj4aI,1870
24
- pretix_mapplugin/static/pretix_mapplugin/js/salesmap.js,sha256=8mLC6rQqJRDoPls4ykBIuN1fsxTkgDBNL6KtZaJLoos,32678
25
+ pretix_mapplugin/static/pretix_mapplugin/js/salesmap.js,sha256=PhUEaF1JKzX0UDe2kafE1pc1WmM3yv9S_v4UkxaBw6g,32539
25
26
  pretix_mapplugin/static/pretix_mapplugin/libs/leaflet-sales-map/MarkerCluster.Default.css,sha256=YSWCMtmNZNwqex4CEw1nQhvFub2lmU7vcCKP-XVwwXA,1287
26
27
  pretix_mapplugin/static/pretix_mapplugin/libs/leaflet-sales-map/MarkerCluster.css,sha256=YU3qCpj_P06tdPBJGPax0bm6Q1wltfwjsho5TR4-TYc,872
27
28
  pretix_mapplugin/static/pretix_mapplugin/libs/leaflet-sales-map/leaflet-heat.js,sha256=65UqrlgGoRAnKfKRuriH3eeDrOhZgZo1SCenduc-SGo,5158
@@ -40,10 +41,10 @@ pretix_mapplugin/static/pretix_mapplugin/libs/leaflet-sales-map/images/marker-ic
40
41
  pretix_mapplugin/static/pretix_mapplugin/libs/leaflet-sales-map/images/marker-icon.png,sha256=V0w6XMqF9BFAhbaEFZbWLwDXyJLHsD8oy_owHesdxDc,1466
41
42
  pretix_mapplugin/static/pretix_mapplugin/libs/leaflet-sales-map/images/marker-shadow.png,sha256=Jk9cZAM58ELdcpBiz8BMF_jqDymIK1OOOEjtjxDttNo,618
42
43
  pretix_mapplugin/templates/pretix_mapplugin/.gitkeep,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
43
- pretix_mapplugin/templates/pretix_mapplugin/map_page.html,sha256=EqppF1UDMAAbR0paBjStId-1x754XdI5JVLl_hzD7zk,12346
44
- pretix_mapplugin/templates/pretix_mapplugin/milestones.html,sha256=2cdcmuA9XxDC3BFZuVL9tO-Nsz7whKasqLM9en3x5cw,2032
45
- pretix_map-0.1.7.dist-info/METADATA,sha256=hC6poaiu6tBYEaHrBfNqEeaXFRIfm0u0Q1vikjhQHr8,3575
46
- pretix_map-0.1.7.dist-info/WHEEL,sha256=wUyA8OaulRlbfwMtmQsvNngGrxQHAvkKcvRmdizlJi0,92
47
- pretix_map-0.1.7.dist-info/entry_points.txt,sha256=C3NAjeZHoCekafkLMCJynPcABRTK8AUprtQv7sUNDZs,137
48
- pretix_map-0.1.7.dist-info/top_level.txt,sha256=CAtEnkgA73zE9Gadm5mjt1SpXHBPOS-QWP0dQVoNToE,17
49
- pretix_map-0.1.7.dist-info/RECORD,,
44
+ pretix_mapplugin/templates/pretix_mapplugin/map_page.html,sha256=Sl82ULQ6qtLgI2d0OtQEX83mCs4mODsDZjIp3_3-DTc,12566
45
+ pretix_mapplugin/templates/pretix_mapplugin/milestones.html,sha256=KamdrYmSmKgsy2-Pn-ChrYZf2Gel-iEA_vzOxRfR2sg,3080
46
+ pretix_map-0.1.9.dist-info/METADATA,sha256=WFXQJiHPDUP5j7A1hQ_R0mE3hBfI_mzd5uGJwuZHu3E,3575
47
+ pretix_map-0.1.9.dist-info/WHEEL,sha256=wUyA8OaulRlbfwMtmQsvNngGrxQHAvkKcvRmdizlJi0,92
48
+ pretix_map-0.1.9.dist-info/entry_points.txt,sha256=C3NAjeZHoCekafkLMCJynPcABRTK8AUprtQv7sUNDZs,137
49
+ pretix_map-0.1.9.dist-info/top_level.txt,sha256=CAtEnkgA73zE9Gadm5mjt1SpXHBPOS-QWP0dQVoNToE,17
50
+ pretix_map-0.1.9.dist-info/RECORD,,
@@ -1 +1 @@
1
- __version__ = "0.1.7"
1
+ __version__ = "0.1.9"
@@ -0,0 +1,15 @@
1
+ from django.db import migrations, models
2
+
3
+ class Migration(migrations.Migration):
4
+
5
+ dependencies = [
6
+ ('pretix_mapplugin', '0003_mapmilestone'),
7
+ ]
8
+
9
+ operations = [
10
+ migrations.AddField(
11
+ model_name='ordergeocodedata',
12
+ name='success_level',
13
+ field=models.CharField(blank=True, help_text='Precision of geocoding (e.g., street, city, zip, failed)', max_length=50, null=True),
14
+ ),
15
+ ]
@@ -21,6 +21,12 @@ class OrderGeocodeData(LoggedModel): # Keep LoggedModel if you want audit logs
21
21
  null=True, # Allow NULL in the database if geocoding fails
22
22
  blank=True # Allow blank in forms/admin
23
23
  )
24
+ success_level = models.CharField(
25
+ max_length=50,
26
+ null=True,
27
+ blank=True,
28
+ help_text="Precision of geocoding (e.g., street, city, zip, failed)"
29
+ )
24
30
 
25
31
  # Change to auto_now to update timestamp on every save (successful or null)
26
32
  last_geocoded_at = models.DateTimeField(
@@ -53,6 +53,7 @@ document.addEventListener('DOMContentLoaded', function () {
53
53
  compareLabel: document.getElementById('compare-map-label'),
54
54
  mainLabel: document.getElementById('main-map-label'),
55
55
  heatmapReset: document.getElementById('heatmap-reset-btn'),
56
+ timelineContainer: document.getElementById('timeline-controls'),
56
57
  timeSlider: document.getElementById('timeline-slider'),
57
58
  timeDisplay: document.getElementById('timeline-date-display'),
58
59
  timePlayBtn: document.getElementById('timeline-play-btn'),
@@ -66,7 +67,6 @@ document.addEventListener('DOMContentLoaded', function () {
66
67
 
67
68
  // --- State ---
68
69
  let map = null, mapCompare = null, allData = [], filteredData = [], displayData = [], milestones = [], currency = "EUR";
69
- let itemAvailData = {};
70
70
  let compareData = [], compareFilteredData = [], compareDisplayData = [];
71
71
  let pinLayer = null, heatmapLayer = null, gridLayer = null, comparisonLayer = null, eventMarkerLayer = null;
72
72
  let pinLayerComp = null, heatmapLayerComp = null, gridLayerComp = null, eventMarkerLayerComp = null;
@@ -75,6 +75,7 @@ document.addEventListener('DOMContentLoaded', function () {
75
75
  let availableItems = new Set(), selectedItems = new Set();
76
76
  let currentDisplayMode = 'map'; // map | list | stats
77
77
  let charts = {};
78
+ let itemAvailData = {};
78
79
 
79
80
  function updateStatus(msg, isErr = false) {
80
81
  if (sel.overlay) {
@@ -152,14 +153,14 @@ document.addEventListener('DOMContentLoaded', function () {
152
153
  sel.blurVal.textContent = sel.blurIn.value;
153
154
  sel.maxZoomVal.textContent = sel.maxZoomIn.value;
154
155
 
155
- if (heatmapLayer) {
156
- heatmapLayer.setOptions({
157
- radius: parseInt(sel.radiusIn.value),
158
- blur: parseInt(sel.blurIn.value),
159
- maxZoom: parseInt(sel.maxZoomIn.value),
160
- minOpacity: 0.4
161
- });
162
- }
156
+ const opt = {
157
+ radius: parseInt(sel.radiusIn.value),
158
+ blur: parseInt(sel.blurIn.value),
159
+ maxZoom: parseInt(sel.maxZoomIn.value),
160
+ minOpacity: 0.4
161
+ };
162
+ if (heatmapLayer) heatmapLayer.setOptions(opt);
163
+ if (heatmapLayerComp) heatmapLayerComp.setOptions(opt);
163
164
  };
164
165
  [sel.radiusIn, sel.blurIn, sel.maxZoomIn].forEach(i => i.oninput = updateHeat);
165
166
  sel.heatmapReset.onclick = () => {
@@ -172,7 +173,10 @@ document.addEventListener('DOMContentLoaded', function () {
172
173
 
173
174
  function switchDisplayMode(mode) {
174
175
  currentDisplayMode = mode;
175
- sel.map.style.display = (mode === 'map' ? 'block' : 'none');
176
+ const isMap = (mode === 'map');
177
+ sel.map.style.display = isMap ? 'block' : 'none';
178
+ sel.mapSplitRoot.style.display = isMap ? 'flex' : 'none';
179
+ sel.timelineContainer.style.display = isMap ? 'block' : 'none';
176
180
  sel.failedContainer.style.display = (mode === 'list' ? 'block' : 'none');
177
181
  sel.analyticsView.style.display = (mode === 'stats' ? 'block' : 'none');
178
182
 
@@ -181,7 +185,12 @@ document.addEventListener('DOMContentLoaded', function () {
181
185
  sel.listToggle.textContent = (mode === 'list' ? 'Show Map' : 'Failed Orders');
182
186
  sel.statsToggle.textContent = (mode === 'stats' ? 'Show Map' : 'Analytics View');
183
187
 
184
- if (mode === 'map') map.invalidateSize();
188
+ if (isMap) {
189
+ setTimeout(() => {
190
+ map.invalidateSize();
191
+ if (mapCompare) mapCompare.invalidateSize();
192
+ }, 50);
193
+ }
185
194
  }
186
195
 
187
196
  function renderStats(s) {
@@ -249,98 +258,11 @@ document.addEventListener('DOMContentLoaded', function () {
249
258
  updateTimelineDisplay();
250
259
  }
251
260
 
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
261
  function renderTimelineVolumeChart(steps) {
339
262
  if (!sel.timelineChart) return;
340
263
  const ctx = sel.timelineChart.getContext('2d');
341
264
  if (charts.timeline) charts.timeline.destroy();
342
265
 
343
- // Calculate counts per step from filteredData
344
266
  const stepCounts = steps.map(ts => {
345
267
  const nextTsIndex = steps.indexOf(ts) + 1;
346
268
  const nextTs = nextTsIndex < steps.length ? steps[nextTsIndex] : Infinity;
@@ -363,10 +285,7 @@ document.addEventListener('DOMContentLoaded', function () {
363
285
  responsive: true,
364
286
  maintainAspectRatio: false,
365
287
  plugins: { legend: { display: false }, tooltip: { enabled: false } },
366
- scales: {
367
- x: { display: false },
368
- y: { display: false, beginAtZero: true }
369
- }
288
+ scales: { x: { display: false }, y: { display: false, beginAtZero: true } }
370
289
  }
371
290
  });
372
291
  }
@@ -374,20 +293,13 @@ document.addEventListener('DOMContentLoaded', function () {
374
293
  function renderMilestoneMarkers(steps) {
375
294
  if (!sel.timelineMarkers || !steps || steps.length === 0) return;
376
295
  sel.timelineMarkers.innerHTML = '';
377
-
378
296
  milestones.forEach(m => {
379
- // Find closest step index
380
- let closestIdx = -1;
381
- let minDiff = Infinity;
297
+ let closestIdx = -1, minDiff = Infinity;
382
298
  steps.forEach((ts, idx) => {
383
299
  const diff = Math.abs(ts - m.ts);
384
- if (diff < minDiff) {
385
- minDiff = diff;
386
- closestIdx = idx;
387
- }
300
+ if (diff < minDiff) { minDiff = diff; closestIdx = idx; }
388
301
  });
389
-
390
- if (closestIdx !== -1 && minDiff < 86400000 * 2) { // Only show if close enough (2 days)
302
+ if (closestIdx !== -1 && minDiff < 86400000 * 2) {
391
303
  const percent = (closestIdx / (steps.length - 1)) * 100;
392
304
  const marker = document.createElement('div');
393
305
  marker.className = 'timeline-milestone-marker';
@@ -398,7 +310,6 @@ document.addEventListener('DOMContentLoaded', function () {
398
310
  marker.style.height = '100%';
399
311
  marker.style.background = '#d9534f';
400
312
  marker.style.zIndex = '2';
401
-
402
313
  const label = document.createElement('span');
403
314
  label.textContent = m.label;
404
315
  label.style.position = 'absolute';
@@ -408,13 +319,85 @@ document.addEventListener('DOMContentLoaded', function () {
408
319
  label.style.whiteSpace = 'nowrap';
409
320
  label.style.background = 'rgba(255,255,255,0.7)';
410
321
  label.style.padding = '0 2px';
411
-
412
322
  marker.appendChild(label);
413
323
  sel.timelineMarkers.appendChild(marker);
414
324
  }
415
325
  });
416
326
  }
417
327
 
328
+ function renderItemAvailability(steps) {
329
+ if (!sel.itemAvailList || !steps || steps.length === 0) return;
330
+ sel.itemAvailList.innerHTML = '';
331
+ const minTs = steps[0], maxTs = steps[steps.length - 1], range = maxTs - minTs;
332
+
333
+ Object.keys(itemAvailData).forEach(itemName => {
334
+ const d = itemAvailData[itemName];
335
+ const firstTs = d.first_sale ? new Date(d.first_sale).getTime() : null;
336
+ const lastTs = d.last_sale ? new Date(d.last_sale).getTime() : null;
337
+ const availFrom = d.available_from ? new Date(d.available_from).getTime() : null;
338
+ const availUntil = d.available_until ? new Date(d.available_until).getTime() : null;
339
+
340
+ if (!firstTs && !availFrom) return;
341
+
342
+ const row = document.createElement('div');
343
+ row.style.marginBottom = '12px';
344
+ row.style.position = 'relative';
345
+ row.style.width = '100%';
346
+
347
+ const label = document.createElement('div');
348
+ label.textContent = itemName;
349
+ label.style.fontSize = '9px';
350
+ label.style.color = '#333';
351
+ label.style.marginBottom = '2px';
352
+ label.style.fontWeight = 'bold';
353
+ row.appendChild(label);
354
+
355
+ const barContainer = document.createElement('div');
356
+ barContainer.style.width = '100%';
357
+ barContainer.style.height = '6px';
358
+ barContainer.style.background = '#f0f0f0';
359
+ barContainer.style.position = 'relative';
360
+ barContainer.style.borderRadius = '3px';
361
+ row.appendChild(barContainer);
362
+
363
+ if (availFrom || availUntil) {
364
+ const start = availFrom ? Math.max(availFrom, minTs) : minTs;
365
+ const end = availUntil ? Math.min(availUntil, maxTs) : maxTs;
366
+ if (start < end) {
367
+ const left = ((start - minTs) / range) * 100;
368
+ const width = ((end - start) / range) * 100;
369
+ const bar = document.createElement('div');
370
+ bar.style.position = 'absolute';
371
+ bar.style.left = `${left}%`;
372
+ bar.style.width = `${width}%`;
373
+ bar.style.height = '100%';
374
+ bar.style.background = '#ddd';
375
+ bar.style.borderRadius = '3px';
376
+ barContainer.appendChild(bar);
377
+ }
378
+ }
379
+
380
+ if (firstTs && lastTs) {
381
+ const start = Math.max(firstTs, minTs);
382
+ const end = Math.min(lastTs, maxTs);
383
+ if (start <= end) {
384
+ const left = ((start - minTs) / range) * 100;
385
+ const width = Math.max(((end - start) / range) * 100, 0.5);
386
+ const bar = document.createElement('div');
387
+ bar.style.position = 'absolute';
388
+ bar.style.left = `${left}%`;
389
+ bar.style.width = `${width}%`;
390
+ bar.style.height = '100%';
391
+ bar.style.background = '#36A2EB';
392
+ bar.style.borderRadius = '3px';
393
+ bar.style.zIndex = '1';
394
+ barContainer.appendChild(bar);
395
+ }
396
+ }
397
+ sel.itemAvailList.appendChild(row);
398
+ });
399
+ }
400
+
418
401
  function updateTimelineDisplay() {
419
402
  const steps = sel.timeSlider._steps;
420
403
  if (!steps) return;
@@ -447,6 +430,7 @@ document.addEventListener('DOMContentLoaded', function () {
447
430
 
448
431
  function refreshLayers() {
449
432
  if (!map) return;
433
+
450
434
  [pinLayer, heatmapLayer, gridLayer].forEach(l => { if (l && map.hasLayer(l)) map.removeLayer(l); });
451
435
  if (mapCompare) {
452
436
  [pinLayerComp, heatmapLayerComp, gridLayerComp, eventMarkerLayerComp].forEach(l => { if (l && mapCompare.hasLayer(l)) mapCompare.removeLayer(l); });
@@ -454,6 +438,7 @@ document.addEventListener('DOMContentLoaded', function () {
454
438
  if (comparisonLayer && map.hasLayer(comparisonLayer)) map.removeLayer(comparisonLayer);
455
439
 
456
440
  const isSplitNeeded = currentCompareSlug && (currentView === 'heatmap' || currentView === 'grid');
441
+
457
442
  if (isSplitNeeded) {
458
443
  sel.mapSplitRoot.classList.add('map-split-active');
459
444
  sel.mainLabel.style.display = 'block';
@@ -467,12 +452,14 @@ document.addEventListener('DOMContentLoaded', function () {
467
452
  }
468
453
 
469
454
  createPinLayer(); createHeatmapLayer(); createGridLayer();
455
+
470
456
  if (isSplitNeeded) {
471
457
  createPinLayer(true); createHeatmapLayer(true); createGridLayer(true);
472
458
  } 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);
459
+ const dots = compareData.map(l => l.lat ? L.circleMarker([l.lat, l.lon], { radius: 4, color: '#888', fillOpacity: 0.5 }).bindTooltip("Comparison Order") : null).filter(l => l);
474
460
  comparisonLayer = L.layerGroup(dots);
475
461
  }
462
+
476
463
  showCurrentView();
477
464
  }
478
465
 
@@ -535,11 +522,9 @@ document.addEventListener('DOMContentLoaded', function () {
535
522
  if (currentView === 'pins' && pinLayer) map.addLayer(pinLayer);
536
523
  else if (currentView === 'heatmap' && heatmapLayer) map.addLayer(heatmapLayer);
537
524
  else if (currentView === 'grid' && gridLayer) map.addLayer(gridLayer);
538
-
539
525
  sel.heatmapPanel.style.display = (currentView === 'heatmap' || currentView === 'grid' ? 'block' : 'none');
540
526
  sel.heatmapControls.style.display = (currentView === 'heatmap' ? 'block' : 'none');
541
527
  sel.gridControls.style.display = (currentView === 'grid' ? 'block' : 'none');
542
-
543
528
  if (sel.clusterToggle) sel.clusterToggle.style.display = (currentView === 'pins' ? 'inline-block' : 'none');
544
529
  if (eventMarkerLayer) map.addLayer(eventMarkerLayer);
545
530
  if (comparisonLayer && currentView === 'pins') map.addLayer(comparisonLayer);
@@ -555,10 +540,10 @@ document.addEventListener('DOMContentLoaded', function () {
555
540
  function renderEventMarker(em, isCompare = false) {
556
541
  const layer = L.marker([em.lat, em.lon], { icon: icons.red, zIndexOffset: 2000 }).bindTooltip(`<strong>EVENT: ${em.name}</strong><br>${em.location}`);
557
542
  if (isCompare) {
558
- if (eventMarkerLayerComp && mapCompare.hasLayer(eventMarkerLayerComp)) mapCompare.removeLayer(eventMarkerLayerComp);
543
+ if (eventMarkerLayerComp) mapCompare.removeLayer(eventMarkerLayerComp);
559
544
  eventMarkerLayerComp = layer.addTo(mapCompare);
560
545
  } else {
561
- if (eventMarkerLayer && map.hasLayer(eventMarkerLayer)) map.removeLayer(eventMarkerLayer);
546
+ if (eventMarkerLayer) map.removeLayer(eventMarkerLayer);
562
547
  eventMarkerLayer = layer.addTo(map);
563
548
  }
564
549
  }
@@ -600,7 +585,12 @@ document.addEventListener('DOMContentLoaded', function () {
600
585
 
601
586
  function renderFailedOrders(orders) {
602
587
  const csrfToken = document.querySelector('[name=csrfmiddlewaretoken]').value;
603
- sel.failedTbody.innerHTML = orders.map(o => `<tr><td><code>${o.code}</code></td><td>${o.address}</td><td><div class="btn-group"><a href="${o.url}" target="_blank" class="btn btn-default btn-xs">View</a><button class="btn btn-warning btn-xs retry-btn" data-url="${o.retry_url}">Retry</button></div></td></tr>`).join('') || '<tr><td colspan="3">None</td></tr>';
588
+ sel.failedTbody.innerHTML = orders.map(o => {
589
+ let statusBadge = `<span class="label label-danger">Failed</span>`;
590
+ if (o.success_level === 'city') statusBadge = `<span class="label label-warning">Only City</span>`;
591
+ else if (o.success_level === 'zip') statusBadge = `<span class="label label-info">Only Zip</span>`;
592
+ return `<tr><td><code>${o.code}</code></td><td>${o.address}</td><td>${statusBadge}</td><td><div class="btn-group"><a href="${o.url}" target="_blank" class="btn btn-default btn-xs">View</a><button class="btn btn-warning btn-xs retry-btn" data-url="${o.retry_url}">Retry</button></div></td></tr>`;
593
+ }).join('') || '<tr><td colspan="4">None</td></tr>';
604
594
  sel.failedTbody.querySelectorAll('.retry-btn').forEach(btn => btn.onclick = () => {
605
595
  btn.disabled = true; btn.innerHTML = '...';
606
596
  fetch(btn.dataset.url, { method: 'POST', headers: { 'X-CSRFToken': csrfToken } }).then(r => r.json()).then(d => { if(d.success) { btn.className = 'btn btn-success btn-xs'; btn.innerHTML = '✔'; setTimeout(() => fetchData(sel.map.dataset.dataUrl), 1000); } });
pretix_mapplugin/tasks.py CHANGED
@@ -106,27 +106,40 @@ def geocode_order_task(self, order_pk: int, organizer_pk: int | None = None, nom
106
106
  # Try candidates in order
107
107
  coordinates = None
108
108
  successful_candidate = None
109
+ success_level = "failed"
109
110
 
110
- for candidate in candidates:
111
+ for i, candidate in enumerate(candidates):
111
112
  logger.debug(f"Attempting to geocode candidate for Order {order.code}: '{candidate}'")
112
113
  coordinates = geocode_address(candidate, nominatim_user_agent=nominatim_user_agent)
113
114
  if coordinates:
114
115
  successful_candidate = candidate
116
+ # Map candidate index to level (0: full, 1: zip+city, 2: zip)
117
+ if i == 0: success_level = "street"
118
+ elif i == 1: success_level = "city"
119
+ else: success_level = "zip"
115
120
  break # Success!
116
121
 
117
122
  with transaction.atomic():
118
123
  if coordinates:
119
124
  latitude, longitude = coordinates
120
125
  obj, created = OrderGeocodeData.objects.update_or_create(
121
- order=order, defaults={'latitude': latitude, 'longitude': longitude}
126
+ order=order, defaults={
127
+ 'latitude': latitude,
128
+ 'longitude': longitude,
129
+ 'success_level': success_level
130
+ }
122
131
  )
123
132
  log_level = logging.INFO if created else logging.DEBUG
124
133
  logger.log(log_level,
125
- f"Saved{' new' if created else ' updated'} geocode data for Order {order.code} using '{successful_candidate}': ({latitude}, {longitude})")
134
+ f"Saved{' new' if created else ' updated'} geocode data for Order {order.code} using '{successful_candidate}' (Level: {success_level}): ({latitude}, {longitude})")
126
135
  else:
127
136
  logger.warning(f"Geocoding failed for Order {order.code} (tried {len(candidates)} candidates). Storing null coordinates.")
128
137
  obj, created = OrderGeocodeData.objects.update_or_create(
129
- order=order, defaults={'latitude': None, 'longitude': None}
138
+ order=order, defaults={
139
+ 'latitude': None,
140
+ 'longitude': None,
141
+ 'success_level': 'failed'
142
+ }
130
143
  )
131
144
  # --- Scope deactivated automatically ---
132
145
 
@@ -110,7 +110,7 @@
110
110
  {% endif %}
111
111
  </div>
112
112
  <table class="table table-hover">
113
- <thead><tr><th>Code</th><th>Address</th><th>Action</th></tr></thead>
113
+ <thead><tr><th>Code</th><th>Address</th><th>Status</th><th>Action</th></tr></thead>
114
114
  <tbody id="failed-orders-tbody"></tbody>
115
115
  </table>
116
116
  </div>
@@ -151,10 +151,26 @@
151
151
  </div>
152
152
  </div>
153
153
 
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;">
154
+ <div id="timeline-controls" style="margin-top: 10px; padding: 15px; background: #f5f5f5; border-radius: 4px; border: 1px solid #ddd; position: relative; flex-shrink: 0;">
155
+ <div id="timeline-chart-container" style="position: relative; height: 50px; margin-bottom: 10px; background: white; border: 1px solid #eee; overflow: hidden;">
156
+ <canvas id="timeline-volume-chart" style="position: absolute; top: 0; left: 0; width: 100%; height: 100%;"></canvas>
157
+ <div id="timeline-milestone-markers" style="position: absolute; top: 0; left: 0; width: 100%; height: 100%; pointer-events: none;"></div>
158
+ </div>
159
+
160
+ <div style="display: flex; gap: 15px; align-items: center; margin-bottom: 15px;">
161
+ <button id="timeline-play-btn" class="btn btn-sm btn-default" style="flex-shrink: 0;"><i class="fa fa-play"></i> Play</button>
162
+ <div style="flex-grow: 1; position: relative; padding: 0 5px;">
163
+ <input type="range" id="timeline-slider" min="0" max="100" value="100" style="width: 100%; cursor: pointer; position: relative; z-index: 5; margin: 0;">
164
+ </div>
165
+ </div>
166
+
167
+ <div id="timeline-item-availability" style="margin-top: 10px; background: white; border: 1px solid #eee; padding: 10px 0; max-height: 150px; overflow-y: auto;">
168
+ <div id="item-availability-list" style="position: relative; padding: 0 5px;"></div>
169
+ </div>
170
+
171
+ <div style="display: flex; justify-content: space-between; align-items: center; margin-top: 10px; font-size: 12px; color: #666;">
156
172
  <div>
157
- <label style="margin-bottom: 0;">{% trans "Timeline:" %} <span id="timeline-date-display" style="font-weight: normal;"></span></label>
173
+ <label style="margin-bottom: 0;">{% trans "Timeline:" %} <span id="timeline-date-display" style="font-weight: bold; color: #333;"></span></label>
158
174
  <span id="timeline-count-display" class="badge"></span>
159
175
  </div>
160
176
  <div>
@@ -166,17 +182,6 @@
166
182
  </select>
167
183
  </div>
168
184
  </div>
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;">
177
- <button id="timeline-play-btn" class="btn btn-sm btn-default"><i class="fa fa-play"></i> Play</button>
178
- <input type="range" id="timeline-slider" min="0" max="100" value="100" style="flex-grow: 1; cursor: pointer; position: relative; z-index: 5;">
179
- </div>
180
185
  </div>
181
186
  </div>
182
187
 
@@ -50,4 +50,29 @@
50
50
  </div>
51
51
  </div>
52
52
  </form>
53
+ <script type="text/javascript" src="{% static "pretixcontrol/js/ui/main.js" %}"></script>
54
+ <script type="text/javascript">
55
+ $(function () {
56
+ $(".datepickerfield").each(function () {
57
+ var format = $("body").attr("data-dateformat");
58
+ $(this).datetimepicker({
59
+ format: format,
60
+ locale: $("body").attr("data-datelocale"),
61
+ useCurrent: false,
62
+ showClear: true,
63
+ icons: {
64
+ time: 'fa fa-clock-o',
65
+ date: 'fa fa-calendar',
66
+ up: 'fa fa-chevron-up',
67
+ down: 'fa fa-chevron-down',
68
+ previous: 'fa fa-chevron-left',
69
+ next: 'fa fa-chevron-right',
70
+ today: 'fa fa-dot-circle-o',
71
+ clear: 'fa fa-trash',
72
+ close: 'fa fa-times'
73
+ }
74
+ });
75
+ });
76
+ });
77
+ </script>
53
78
  {% endblock %}
pretix_mapplugin/views.py CHANGED
@@ -220,11 +220,15 @@ class SalesMapDataView(EventSettingsViewMixin, View):
220
220
  tooltip_parts.append(f"<strong>Items:</strong> {order.positions.count()}")
221
221
  tooltip_parts.append(f"<strong>Total:</strong> {order.total} {event.currency}")
222
222
  if dist_km: tooltip_parts.append(f"<strong>Dist:</strong> {dist_km:.1f} km")
223
+ if entry.success_level:
224
+ level_map = {'street': _('Street'), 'city': _('City'), 'zip': _('Zip code')}
225
+ tooltip_parts.append(f"<strong>Precision:</strong> {level_map.get(entry.success_level, entry.success_level)}")
223
226
 
224
227
  locations_data.append({
225
228
  "lat": entry.latitude, "lon": entry.longitude, "tooltip": "<br>".join(tooltip_parts),
226
229
  "order_url": reverse('control:event.order', kwargs={'organizer': organizer.slug, 'event': event.slug, 'code': order.code}),
227
- "items": item_names, "date": iso_date, "dist": dist_km, "revenue": revenue, "status": status
230
+ "items": item_names, "date": iso_date, "dist": dist_km, "revenue": revenue, "status": status,
231
+ "precision": entry.success_level
228
232
  })
229
233
 
230
234
  failed_entries = OrderGeocodeData.objects.filter(order__event=event, latitude__isnull=True).select_related('order', 'order__invoice_address')
@@ -233,7 +237,8 @@ class SalesMapDataView(EventSettingsViewMixin, View):
233
237
  'pk': entry.order.pk, 'code': entry.order.code,
234
238
  'address': get_best_address_string(entry.order) or _("No address"),
235
239
  'url': reverse('control:event.order', kwargs={'organizer': organizer.slug, 'event': event.slug, 'code': entry.order.code}),
236
- 'retry_url': reverse('plugins:pretix_mapplugin:event.settings.salesmap.retry', kwargs={'organizer': organizer.slug, 'event': event.slug, 'order': entry.order.pk})
240
+ 'retry_url': reverse('plugins:pretix_mapplugin:event.settings.salesmap.retry', kwargs={'organizer': organizer.slug, 'event': event.slug, 'order': entry.order.pk}),
241
+ 'success_level': entry.success_level or 'failed'
237
242
  })
238
243
 
239
244
  avg_dist_val = round(sum(distances) / len(distances), 1) if distances else 0
@@ -291,6 +296,10 @@ class SalesMapView(EventSettingsViewMixin, TemplateView):
291
296
  'https://cdn.jsdelivr.net',
292
297
  "'unsafe-eval'" # Needed for some charting libs
293
298
  ],
299
+ 'connect-src': [
300
+ 'https://cdn.jsdelivr.net',
301
+ 'https://*.tile.openstreetmap.org'
302
+ ],
294
303
  'style-src': ["'unsafe-inline'"]
295
304
  }
296
305