| 25 |
25 |
|
{% for item in items %}<option value="{{ item.item_type }}" class="content-type-opt">{{ item.item_type }}</option>{% endfor %}
|
| 26 |
26 |
|
</select>
|
| 27 |
27 |
|
</div>
|
| 28 |
|
- |
<p class="form-hint" style="margin-bottom: 0.75rem;">Select items with checkboxes for bulk publish, unpublish, or delete.</p>
|
| 29 |
|
- |
<div id="bulk-action-bar" style="display: none; padding: 0.75rem 1rem; margin-bottom: 1rem; background: var(--surface-raised); border: 1px solid var(--border); border-radius: 4px; align-items: center; gap: 1rem;">
|
| 30 |
|
- |
<span id="bulk-count" style="font-weight: bold;"></span>
|
| 31 |
|
- |
<button class="secondary small" onclick="bulkAction('publish')">Publish</button>
|
| 32 |
|
- |
<button class="secondary small" onclick="bulkAction('unpublish')">Unpublish</button>
|
| 33 |
|
- |
<button class="secondary small" style="color: var(--danger);" onclick="bulkAction('delete')">Delete</button>
|
| 34 |
|
- |
<button class="secondary small" style="margin-left: auto;" onclick="toggleSelectAll(false)">Deselect All</button>
|
|
28 |
+ |
<div id="bulk-action-bar" style="padding: 0.75rem 1rem; margin-bottom: 1rem; background: var(--surface-raised); border: 1px solid var(--border); border-radius: 4px; display: flex; align-items: center; gap: 0.5rem; flex-wrap: wrap; opacity: 0.4;" data-active="false">
|
|
29 |
+ |
<span id="bulk-count" style="font-weight: bold; min-width: 5rem;">0 selected</span>
|
|
30 |
+ |
<button class="secondary small" onclick="bulkAction('publish')" disabled>Publish</button>
|
|
31 |
+ |
<button class="secondary small" onclick="bulkAction('unpublish')" disabled>Unpublish</button>
|
|
32 |
+ |
<button class="secondary small" onclick="showBulkPrice()" disabled id="bulk-price-btn">Set Price</button>
|
|
33 |
+ |
<button class="secondary small" onclick="showBulkTag()" disabled id="bulk-tag-btn">Add Tag</button>
|
|
34 |
+ |
<button class="secondary small" style="color: var(--danger);" onclick="bulkAction('delete')" disabled>Delete</button>
|
|
35 |
+ |
<button class="secondary small" style="margin-left: auto;" onclick="toggleSelectAll(false)" disabled>Deselect</button>
|
|
36 |
+ |
</div>
|
|
37 |
+ |
<div id="bulk-price-form" style="display: none; margin-bottom: 1rem; padding: 0.75rem 1rem; background: var(--surface-muted); border: 1px solid var(--border); border-radius: 4px;">
|
|
38 |
+ |
<div style="display: flex; gap: 0.75rem; align-items: end;">
|
|
39 |
+ |
<div>
|
|
40 |
+ |
<label style="font-size: 0.85rem; display: block; margin-bottom: 0.25rem;">New price ($)</label>
|
|
41 |
+ |
<input type="number" id="bulk-price-input" min="0" step="0.01" placeholder="0.00" style="width: 120px; padding: 0.3rem;">
|
|
42 |
+ |
</div>
|
|
43 |
+ |
<button class="primary small" onclick="submitBulkPrice()">Apply</button>
|
|
44 |
+ |
<button class="secondary small" onclick="document.getElementById('bulk-price-form').style.display='none'">Cancel</button>
|
|
45 |
+ |
</div>
|
|
46 |
+ |
<p class="form-hint" style="margin-top: 0.5rem;">Enter 0 to make items free.</p>
|
|
47 |
+ |
</div>
|
|
48 |
+ |
<div id="bulk-tag-form" style="display: none; margin-bottom: 1rem; padding: 0.75rem 1rem; background: var(--surface-muted); border: 1px solid var(--border); border-radius: 4px;">
|
|
49 |
+ |
<div style="display: flex; gap: 0.75rem; align-items: end;">
|
|
50 |
+ |
<div>
|
|
51 |
+ |
<label style="font-size: 0.85rem; display: block; margin-bottom: 0.25rem;">Tag name</label>
|
|
52 |
+ |
<input type="text" id="bulk-tag-input" placeholder="e.g. ambient" style="width: 200px; padding: 0.3rem;">
|
|
53 |
+ |
</div>
|
|
54 |
+ |
<button class="primary small" onclick="submitBulkTag()">Apply</button>
|
|
55 |
+ |
<button class="secondary small" onclick="document.getElementById('bulk-tag-form').style.display='none'">Cancel</button>
|
|
56 |
+ |
</div>
|
|
57 |
+ |
<p class="form-hint" style="margin-top: 0.5rem;">Creates the tag if it doesn't exist.</p>
|
| 35 |
58 |
|
</div>
|
| 36 |
59 |
|
<table class="data-table">
|
| 37 |
60 |
|
<thead>
|
| 122 |
145 |
|
</table>
|
| 123 |
146 |
|
{% endif %}
|
| 124 |
147 |
|
|
|
148 |
+ |
{% if !deleted_items.is_empty() %}
|
|
149 |
+ |
<details style="margin-top: 2rem;">
|
|
150 |
+ |
<summary style="cursor: pointer; font-size: 0.95rem; opacity: 0.7;">Recently Deleted ({{ deleted_items.len() }})</summary>
|
|
151 |
+ |
<p class="form-hint" style="margin-top: 0.5rem;">Deleted items are permanently removed after 7 days.</p>
|
|
152 |
+ |
<table class="data-table" style="margin-top: 0.75rem;">
|
|
153 |
+ |
<thead>
|
|
154 |
+ |
<tr>
|
|
155 |
+ |
<th>Title</th>
|
|
156 |
+ |
<th>Deleted</th>
|
|
157 |
+ |
<th></th>
|
|
158 |
+ |
</tr>
|
|
159 |
+ |
</thead>
|
|
160 |
+ |
<tbody>
|
|
161 |
+ |
{% for item in deleted_items %}
|
|
162 |
+ |
<tr id="deleted-{{ item.id }}">
|
|
163 |
+ |
<td style="opacity: 0.6;">{{ item.title }}</td>
|
|
164 |
+ |
<td style="opacity: 0.6;">{{ item.deleted_at }}</td>
|
|
165 |
+ |
<td>
|
|
166 |
+ |
<button class="secondary small"
|
|
167 |
+ |
hx-post="/api/items/{{ item.id }}/restore"
|
|
168 |
+ |
hx-swap="none"
|
|
169 |
+ |
hx-on::after-request="if(event.detail.successful) htmx.ajax('GET', '/dashboard/project/{{ project_slug }}/tabs/content', '#tab-content')">Restore</button>
|
|
170 |
+ |
</td>
|
|
171 |
+ |
</tr>
|
|
172 |
+ |
{% endfor %}
|
|
173 |
+ |
</tbody>
|
|
174 |
+ |
</table>
|
|
175 |
+ |
</details>
|
|
176 |
+ |
{% endif %}
|
|
177 |
+ |
|
| 125 |
178 |
|
<script>
|
| 126 |
179 |
|
function toggleSelectAll(checked) {
|
| 127 |
180 |
|
var boxes = document.querySelectorAll('.bulk-check');
|
| 138 |
191 |
|
var selectAll = document.getElementById('select-all');
|
| 139 |
192 |
|
var allBoxes = document.querySelectorAll('.bulk-check');
|
| 140 |
193 |
|
if (!bar) return;
|
| 141 |
|
- |
if (checked.length > 0) {
|
| 142 |
|
- |
bar.style.display = 'flex';
|
| 143 |
|
- |
countEl.textContent = checked.length + ' selected';
|
| 144 |
|
- |
} else {
|
| 145 |
|
- |
bar.style.display = 'none';
|
| 146 |
|
- |
}
|
|
194 |
+ |
var hasSelection = checked.length > 0;
|
|
195 |
+ |
bar.style.opacity = hasSelection ? '1' : '0.4';
|
|
196 |
+ |
bar.dataset.active = hasSelection ? 'true' : 'false';
|
|
197 |
+ |
countEl.textContent = checked.length + ' selected';
|
|
198 |
+ |
var buttons = bar.querySelectorAll('button');
|
|
199 |
+ |
for (var i = 0; i < buttons.length; i++) buttons[i].disabled = !hasSelection;
|
| 147 |
200 |
|
if (selectAll) selectAll.checked = allBoxes.length > 0 && checked.length === allBoxes.length;
|
|
201 |
+ |
// Hide inline forms when selection changes
|
|
202 |
+ |
if (!hasSelection) {
|
|
203 |
+ |
document.getElementById('bulk-price-form').style.display = 'none';
|
|
204 |
+ |
document.getElementById('bulk-tag-form').style.display = 'none';
|
|
205 |
+ |
}
|
| 148 |
206 |
|
}
|
| 149 |
207 |
|
|
| 150 |
208 |
|
function bulkAction(action) {
|
| 210 |
268 |
|
});
|
| 211 |
269 |
|
}
|
| 212 |
270 |
|
|
|
271 |
+ |
function showBulkPrice() {
|
|
272 |
+ |
document.getElementById('bulk-price-form').style.display = 'block';
|
|
273 |
+ |
document.getElementById('bulk-tag-form').style.display = 'none';
|
|
274 |
+ |
document.getElementById('bulk-price-input').focus();
|
|
275 |
+ |
}
|
|
276 |
+ |
|
|
277 |
+ |
function showBulkTag() {
|
|
278 |
+ |
document.getElementById('bulk-tag-form').style.display = 'block';
|
|
279 |
+ |
document.getElementById('bulk-price-form').style.display = 'none';
|
|
280 |
+ |
document.getElementById('bulk-tag-input').focus();
|
|
281 |
+ |
}
|
|
282 |
+ |
|
|
283 |
+ |
function submitBulkPrice() {
|
|
284 |
+ |
var checked = document.querySelectorAll('.bulk-check:checked');
|
|
285 |
+ |
if (checked.length === 0) return;
|
|
286 |
+ |
var price = document.getElementById('bulk-price-input').value;
|
|
287 |
+ |
if (price === '') { showToast('Enter a price'); return; }
|
|
288 |
+ |
|
|
289 |
+ |
var params = new URLSearchParams();
|
|
290 |
+ |
for (var i = 0; i < checked.length; i++) params.append('item_ids', checked[i].value);
|
|
291 |
+ |
params.append('price_dollars', price);
|
|
292 |
+ |
|
|
293 |
+ |
fetch('/api/items/bulk/price', {
|
|
294 |
+ |
method: 'POST',
|
|
295 |
+ |
headers: Object.assign({'Content-Type': 'application/x-www-form-urlencoded'}, csrfHeaders()),
|
|
296 |
+ |
body: params.toString()
|
|
297 |
+ |
})
|
|
298 |
+ |
.then(function(r) {
|
|
299 |
+ |
if (!r.ok) return r.text().then(function(t) {
|
|
300 |
+ |
var msg = 'Failed';
|
|
301 |
+ |
try { var p = JSON.parse(t); if (p.error) msg = p.error; } catch (_) {}
|
|
302 |
+ |
throw new Error(msg);
|
|
303 |
+ |
});
|
|
304 |
+ |
document.getElementById('bulk-price-form').style.display = 'none';
|
|
305 |
+ |
htmx.ajax('GET', '/dashboard/project/{{ project_slug }}/tabs/content', '#tab-content');
|
|
306 |
+ |
})
|
|
307 |
+ |
.catch(function(err) { showToast(err.message || 'Price update failed'); });
|
|
308 |
+ |
}
|
|
309 |
+ |
|
|
310 |
+ |
function submitBulkTag() {
|
|
311 |
+ |
var checked = document.querySelectorAll('.bulk-check:checked');
|
|
312 |
+ |
if (checked.length === 0) return;
|
|
313 |
+ |
var tag = document.getElementById('bulk-tag-input').value.trim();
|
|
314 |
+ |
if (!tag) { showToast('Enter a tag name'); return; }
|
|
315 |
+ |
|
|
316 |
+ |
var params = new URLSearchParams();
|
|
317 |
+ |
for (var i = 0; i < checked.length; i++) params.append('item_ids', checked[i].value);
|
|
318 |
+ |
params.append('tag_name', tag);
|
|
319 |
+ |
|
|
320 |
+ |
fetch('/api/items/bulk/tag', {
|
|
321 |
+ |
method: 'POST',
|
|
322 |
+ |
headers: Object.assign({'Content-Type': 'application/x-www-form-urlencoded'}, csrfHeaders()),
|
|
323 |
+ |
body: params.toString()
|
|
324 |
+ |
})
|
|
325 |
+ |
.then(function(r) {
|
|
326 |
+ |
if (!r.ok) return r.text().then(function(t) {
|
|
327 |
+ |
var msg = 'Failed';
|
|
328 |
+ |
try { var p = JSON.parse(t); if (p.error) msg = p.error; } catch (_) {}
|
|
329 |
+ |
throw new Error(msg);
|
|
330 |
+ |
});
|
|
331 |
+ |
document.getElementById('bulk-tag-form').style.display = 'none';
|
|
332 |
+ |
document.getElementById('bulk-tag-input').value = '';
|
|
333 |
+ |
htmx.ajax('GET', '/dashboard/project/{{ project_slug }}/tabs/content', '#tab-content');
|
|
334 |
+ |
})
|
|
335 |
+ |
.catch(function(err) { showToast(err.message || 'Tag operation failed'); });
|
|
336 |
+ |
}
|
|
337 |
+ |
|
| 213 |
338 |
|
function toggleBundleChildren(bundleId) {
|
| 214 |
339 |
|
var rows = document.querySelectorAll('.bundle-child-' + bundleId);
|
| 215 |
340 |
|
var btn = document.querySelector('tr:has(.bulk-check[value="' + bundleId + '"]) .bundle-toggle');
|