feat(akquise): Uebernommene Eintraege bleiben in der Liste
Bisher verschwand ein Fund mit der Uebernahme aus Eingang und Archiv. Beim Durchgehen blieb damit offen, ob er schon angefasst wurde. Jetzt bleibt der Eintrag stehen und zeigt statt "Opportunity" den blauen Knopf "Uebernommen" mit Haken; ein Klick springt zur Opportunity. Dafuer entscheidet nicht mehr IS_OPEN ueber die Listen, sondern die neuen Regeln IN_INBOX und RETURNED: ein Rueckfaeller (Opportunity im Papierkorb oder geloescht) gehoert weiter ins Archiv, alles andere bleibt in seiner Liste. Zaehler und Menuezahl folgen derselben Regel. IS_OPEN bleibt fuer den Loeschlauf: was zu einer lebenden Opportunity gehoert, wird nicht weggeraeumt. Solange die Opportunity lebt, sind Bearbeiten, Loeschen und "Zurueck in die Akquise" am Eintrag gesperrt - er ist ihr Herkunftsnachweis, und der Rueckweg von der Opportunity braucht ihn. Das Loeschen weist auch das Backend ab, nicht nur die Oberflaeche. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Opus 5
parent
86aa7468af
commit
3e76a73f2e
@@ -44,18 +44,36 @@ export const IN_TRASH = 'a.deleted_at IS NOT NULL';
|
||||
export const OPPORTUNITY_GONE = '(opp.id IS NULL OR opp.deleted_at IS NOT NULL)';
|
||||
|
||||
/**
|
||||
* Bewerbung, die (wieder) in Eingang oder Archiv gehoert: nie uebernommen, oder
|
||||
* die Opportunity ist weg. Abgeleitet statt gespeichert - wird die Opportunity
|
||||
* zurueckgeholt, verschwindet der Eintrag von selbst wieder.
|
||||
* Bewerbung, die noch nicht abgearbeitet ist: nie uebernommen, oder die
|
||||
* Opportunity ist weg. Abgeleitet statt gespeichert - wird die Opportunity
|
||||
* zurueckgeholt, gilt der Eintrag von selbst wieder als uebernommen.
|
||||
*
|
||||
* Fuer die Listen entscheidet das nichts mehr (uebernommene Eintraege bleiben
|
||||
* stehen und zeigen dort "Uebernommen"), wohl aber fuer den Loeschlauf: was zu
|
||||
* einer lebenden Opportunity gehoert, wird nie weggeraeumt.
|
||||
*/
|
||||
export const IS_OPEN = `(a.converted_at IS NULL OR ${OPPORTUNITY_GONE})`;
|
||||
|
||||
/**
|
||||
* Steht im Archiv: alles Uebernommene, dessen Opportunity weg ist (unabhaengig
|
||||
* vom Alter - es ist abgearbeitet, aber nicht verschwunden), plus alles, was
|
||||
* Rueckfaeller: schon einmal uebernommen, aber die Opportunity ist weg. Ein
|
||||
* solcher Eintrag gehoert ins Archiv - unabhaengig vom Alter, er ist
|
||||
* abgearbeitet, aber nicht verschwunden.
|
||||
*/
|
||||
export const RETURNED = `(a.converted_at IS NOT NULL AND ${OPPORTUNITY_GONE})`;
|
||||
|
||||
/**
|
||||
* Steht im Eingang: alles innerhalb der Archivfrist, ausser Rueckfaellern.
|
||||
* Uebernommene Eintraege bleiben hier stehen: verschwaenden sie sofort, bliebe
|
||||
* beim Durchgehen offen, ob ein Fund schon angefasst wurde. Erwartet die
|
||||
* Archivfrist als Platzhalter.
|
||||
*/
|
||||
export const IN_INBOX = `(NOT ${RETURNED} AND a.found_at >= DATE_SUB(CURDATE(), INTERVAL ? DAY))`;
|
||||
|
||||
/**
|
||||
* Steht im Archiv: jeder Rueckfaeller (unabhaengig vom Alter), plus alles, was
|
||||
* aelter als die Archivfrist ist. Erwartet die Frist als Platzhalter.
|
||||
*/
|
||||
export const IN_ARCHIVE = '(a.converted_at IS NOT NULL OR a.found_at < DATE_SUB(CURDATE(), INTERVAL ? DAY))';
|
||||
export const IN_ARCHIVE = `(${RETURNED} OR a.found_at < DATE_SUB(CURDATE(), INTERVAL ? DAY))`;
|
||||
|
||||
/** Untere Grenze: was schon faellig ist, wird nicht mehr gezeigt. */
|
||||
export const NOT_DUE = 'a.found_at >= DATE_SUB(CURDATE(), INTERVAL ? DAY)';
|
||||
|
||||
@@ -7,6 +7,7 @@ import {
|
||||
CONSULTANT_SEPARATOR,
|
||||
DELETE_AFTER_DAYS,
|
||||
IN_ARCHIVE,
|
||||
IN_INBOX,
|
||||
IN_TRASH,
|
||||
IS_OPEN,
|
||||
NOT_DELETED,
|
||||
@@ -611,9 +612,9 @@ router.delete('/options/:id', async (req, res, next) => {
|
||||
async function listMeta() {
|
||||
const [[counts]] = await pool.query(
|
||||
`SELECT
|
||||
SUM(${IS_OPEN} AND ${NOT_DELETED} AND a.converted_at IS NULL AND ${NOT_DUE}) AS eingang,
|
||||
SUM(${IS_OPEN} AND ${NOT_DELETED} AND ${IN_ARCHIVE} AND ${NOT_DUE}) AS archiv,
|
||||
SUM(${IN_TRASH}) AS papierkorb
|
||||
SUM(${NOT_DELETED} AND ${IN_INBOX}) AS eingang,
|
||||
SUM(${NOT_DELETED} AND ${IN_ARCHIVE} AND ${NOT_DUE}) AS archiv,
|
||||
SUM(${IN_TRASH}) AS papierkorb
|
||||
FROM crm_acquisitions a
|
||||
LEFT JOIN crm_opportunities opp ON opp.id = a.opportunity_id`,
|
||||
// Reihenfolge der Platzhalter: Eingangsfrist, Archivfrist, Loeschfrist.
|
||||
@@ -660,17 +661,17 @@ router.get('/', async (req, res, next) => {
|
||||
});
|
||||
}
|
||||
|
||||
const where = [IS_OPEN, NOT_DELETED];
|
||||
const where = [NOT_DELETED];
|
||||
const params = [];
|
||||
|
||||
/*
|
||||
* Der Eingang zeigt nur, was noch nie uebernommen wurde. Eine Bewerbung,
|
||||
* deren Opportunity geloescht wurde, gehoert ins Archiv - unabhaengig davon,
|
||||
* wie alt sie ist: sie ist abgearbeitet, aber nicht verschwunden.
|
||||
* Der Eingang zeigt auch Uebernommenes: der Eintrag bleibt stehen und ist
|
||||
* dort als uebernommen gekennzeichnet. Nur eine Bewerbung, deren
|
||||
* Opportunity geloescht wurde, gehoert ins Archiv - unabhaengig davon, wie
|
||||
* alt sie ist: sie ist abgearbeitet, aber nicht verschwunden.
|
||||
*/
|
||||
if (view === 'eingang') {
|
||||
where.push('a.converted_at IS NULL');
|
||||
where.push('a.found_at >= DATE_SUB(CURDATE(), INTERVAL ? DAY)');
|
||||
where.push(IN_INBOX);
|
||||
params.push(ARCHIVE_AFTER_DAYS);
|
||||
} else if (view === 'archiv') {
|
||||
// Die untere Grenze steht bewusst auch hier: der Loeschlauf raeumt nur
|
||||
@@ -867,7 +868,7 @@ router.delete('/:id', async (req, res, next) => {
|
||||
|
||||
try {
|
||||
const [rows] = await pool.query(
|
||||
'SELECT created_by, deleted_at FROM crm_acquisitions WHERE id = ? LIMIT 1',
|
||||
'SELECT created_by, deleted_at, converted_at, opportunity_id FROM crm_acquisitions WHERE id = ? LIMIT 1',
|
||||
[id]
|
||||
);
|
||||
const existing = rows[0];
|
||||
@@ -875,6 +876,16 @@ router.delete('/:id', async (req, res, next) => {
|
||||
if (existing.deleted_at !== null) {
|
||||
return res.status(409).json({ error: 'Dieser Eintrag liegt schon im Papierkorb.' });
|
||||
}
|
||||
/*
|
||||
* Ein uebernommener Eintrag bleibt stehen, solange seine Opportunity lebt:
|
||||
* er ist deren Herkunftsnachweis, und "Zurueck in die Akquise" braucht ihn.
|
||||
* Erst wenn die Opportunity weg ist, ist er wieder ein gewoehnlicher Fund.
|
||||
*/
|
||||
if (existing.converted_at !== null && !(await opportunityGone(existing.opportunity_id))) {
|
||||
return res.status(409).json({
|
||||
error: 'Der Eintrag gehört zu einer Opportunity. Erst die Opportunity löschen oder zurücknehmen.',
|
||||
});
|
||||
}
|
||||
if (!mayModify(req.user, existing)) {
|
||||
return res.status(403).json({
|
||||
error: 'Diesen Eintrag darf nur löschen, wer ihn angelegt hat, oder ein Administrator.',
|
||||
|
||||
@@ -8,7 +8,6 @@ import {
|
||||
DELETE_AFTER_DAYS,
|
||||
IN_ARCHIVE,
|
||||
IN_TRASH,
|
||||
IS_OPEN,
|
||||
NOT_DELETED,
|
||||
NOT_DUE,
|
||||
} from '../acquisitionRules.js';
|
||||
@@ -38,7 +37,7 @@ router.get('/', async (req, res, next) => {
|
||||
damit Zahl und Liste nicht auseinanderlaufen. */
|
||||
(SELECT COUNT(*) FROM crm_acquisitions a
|
||||
LEFT JOIN crm_opportunities opp ON opp.id = a.opportunity_id
|
||||
WHERE ${IS_OPEN} AND ${NOT_DELETED} AND ${IN_ARCHIVE}
|
||||
WHERE ${NOT_DELETED} AND ${IN_ARCHIVE}
|
||||
AND ${NOT_DUE}) AS acquisition_archive,
|
||||
(SELECT COUNT(*) FROM crm_acquisitions a WHERE ${IN_TRASH}) AS acquisition_trash,
|
||||
(SELECT COUNT(*) FROM crm_opportunities
|
||||
|
||||
Reference in New Issue
Block a user