feat(campaigns): delete campaign from the list
DELETE /campaigns/:id removes the JSON from /opt/targo-hub/data/campaigns/. The Giftbit shortlinks already issued for that campaign live on Giftbit's side and are unaffected — this is purely about clearing internal tracking records (typically test runs cluttering the list). Refuses (409) while the send worker is active for that id so we never yank the file out from under saveCampaign(). Defensive id regex (in campaignPath) blocks path-traversal attempts before unlink runs. UI: red trash icon on each row, disabled while status=sending. Confirmation dialog spells out what survives the deletion (Giftbit links) vs what's lost (tracking, opens/clicks, CSV report) so the operator isn't surprised. 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
This commit is contained in:
parent
4babb403e8
commit
9450dd34db
|
|
@ -69,6 +69,14 @@ export function sendCampaign (id) {
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Permanent deletion — removes the JSON on the hub. Used for clearing
|
||||||
|
// test campaigns from the list. Giftbit shortlinks are unaffected.
|
||||||
|
export function deleteCampaign (id) {
|
||||||
|
return hubFetch(`/campaigns/${encodeURIComponent(id)}`, {
|
||||||
|
method: 'DELETE',
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
// Build the URL the browser hits to download the per-recipient CSV report
|
// Build the URL the browser hits to download the per-recipient CSV report
|
||||||
// (giftbit shortlink ↔ email ↔ name ↔ status). The hub serves it with the
|
// (giftbit shortlink ↔ email ↔ name ↔ status). The hub serves it with the
|
||||||
// proper Content-Disposition so an <a download> click triggers a save.
|
// proper Content-Disposition so an <a download> click triggers a save.
|
||||||
|
|
|
||||||
|
|
@ -49,7 +49,14 @@
|
||||||
</template>
|
</template>
|
||||||
<template v-slot:body-cell-actions="props">
|
<template v-slot:body-cell-actions="props">
|
||||||
<q-td :props="props" class="text-right">
|
<q-td :props="props" class="text-right">
|
||||||
<q-btn flat dense color="primary" icon="visibility" :to="`/campaigns/${props.row.id}`" />
|
<q-btn flat dense color="primary" icon="visibility" :to="`/campaigns/${props.row.id}`">
|
||||||
|
<q-tooltip>Voir la campagne</q-tooltip>
|
||||||
|
</q-btn>
|
||||||
|
<q-btn flat dense color="negative" icon="delete"
|
||||||
|
:disable="props.row.status === 'sending'"
|
||||||
|
@click="confirmDelete(props.row)">
|
||||||
|
<q-tooltip>{{ props.row.status === 'sending' ? "Envoi en cours — impossible de supprimer" : "Supprimer la campagne" }}</q-tooltip>
|
||||||
|
</q-btn>
|
||||||
</q-td>
|
</q-td>
|
||||||
</template>
|
</template>
|
||||||
</q-table>
|
</q-table>
|
||||||
|
|
@ -58,8 +65,10 @@
|
||||||
|
|
||||||
<script setup>
|
<script setup>
|
||||||
import { ref, onMounted } from 'vue'
|
import { ref, onMounted } from 'vue'
|
||||||
import { listCampaigns } from 'src/api/campaigns'
|
import { useQuasar } from 'quasar'
|
||||||
|
import { listCampaigns, deleteCampaign } from 'src/api/campaigns'
|
||||||
|
|
||||||
|
const $q = useQuasar()
|
||||||
const campaigns = ref([])
|
const campaigns = ref([])
|
||||||
const loading = ref(true)
|
const loading = ref(true)
|
||||||
|
|
||||||
|
|
@ -86,6 +95,29 @@ function statusLabel (s) {
|
||||||
return { draft: 'Brouillon', sending: 'En cours', completed: 'Terminée', failed: 'Échec' }[s] || s
|
return { draft: 'Brouillon', sending: 'En cours', completed: 'Terminée', failed: 'Échec' }[s] || s
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Hard-delete: removes the JSON on the hub. Giftbit shortlinks already
|
||||||
|
// issued for this campaign are unaffected (they live on Giftbit's side).
|
||||||
|
// Used mostly to clean up test runs from the list.
|
||||||
|
function confirmDelete (row) {
|
||||||
|
const total = row.total ?? row.counters?.total ?? 0
|
||||||
|
$q.dialog({
|
||||||
|
title: 'Supprimer la campagne ?',
|
||||||
|
message: `<div><strong>${row.name || row.id}</strong> (${total} destinataire${total > 1 ? 's' : ''})<br><br>Cette opération est irréversible — le suivi, les statistiques et le rapport CSV seront perdus. Les liens Giftbit déjà émis restent valides côté Giftbit.</div>`,
|
||||||
|
html: true,
|
||||||
|
cancel: { label: 'Annuler', flat: true },
|
||||||
|
ok: { label: 'Supprimer', color: 'negative', unelevated: true },
|
||||||
|
persistent: true,
|
||||||
|
}).onOk(async () => {
|
||||||
|
try {
|
||||||
|
await deleteCampaign(row.id)
|
||||||
|
campaigns.value = campaigns.value.filter(c => c.id !== row.id)
|
||||||
|
$q.notify({ type: 'positive', message: 'Campagne supprimée' })
|
||||||
|
} catch (e) {
|
||||||
|
$q.notify({ type: 'negative', message: 'Erreur : ' + e.message, timeout: 4000 })
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
async function load () {
|
async function load () {
|
||||||
loading.value = true
|
loading.value = true
|
||||||
try { campaigns.value = await listCampaigns() }
|
try { campaigns.value = await listCampaigns() }
|
||||||
|
|
|
||||||
|
|
@ -1671,6 +1671,30 @@ async function handle (req, res, method, path) {
|
||||||
return json(res, 202, { id, status: 'sending' })
|
return json(res, 202, { id, status: 'sending' })
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// DELETE /campaigns/:id — remove the campaign JSON from disk. Mostly for
|
||||||
|
// cleaning up test/draft runs; the gifts themselves live on Giftbit and
|
||||||
|
// are unaffected. Refuses to delete while the send worker is active for
|
||||||
|
// this id (would crash the worker on the next saveCampaign).
|
||||||
|
if (detailMatch && method === 'DELETE') {
|
||||||
|
const id = detailMatch[1]
|
||||||
|
if (activeWorkers.has(id)) {
|
||||||
|
return json(res, 409, { error: 'cannot delete while campaign is sending — wait for it to finish or fail' })
|
||||||
|
}
|
||||||
|
let filePath
|
||||||
|
try { filePath = campaignPath(id) }
|
||||||
|
catch { return json(res, 400, { error: 'invalid campaign id' }) }
|
||||||
|
if (!fs.existsSync(filePath)) return json(res, 404, { error: 'not found' })
|
||||||
|
try {
|
||||||
|
fs.unlinkSync(filePath)
|
||||||
|
log(`campaign ${id} deleted`)
|
||||||
|
sse.broadcast(`campaign:${id}`, 'campaign-deleted', { id })
|
||||||
|
return json(res, 200, { id, deleted: true })
|
||||||
|
} catch (e) {
|
||||||
|
log(`campaign ${id} delete failed:`, e.message)
|
||||||
|
return json(res, 500, { error: 'delete failed: ' + e.message })
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
return json(res, 404, { error: 'campaigns endpoint not found' })
|
return json(res, 404, { error: 'campaigns endpoint not found' })
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue
Block a user