feat(admin): archive tabs for business inquiries and reviews
Inquiries: completed and rejected inquiries move to an "Archiv" tab; resetting the status brings them back. Reviews: new "Ablehnen" action (Review.rejectedAt) instead of only deleting; the archive shows decided reviews in two columns, accepted and rejected, and either decision can be reversed. Pending counters in the sidebar and dashboard exclude rejected reviews, and the product page tells the author their review was not published instead of "still being checked". Co-Authored-By: Claude Opus 5.5 (1M context) <[email protected]>
This commit is contained in:
1 parent
866d3726d1
commit
f1c0da4673
16 files changed
+460
-114
No files matched your search
@@ -0,0 +1,24 @@
|
||||
/**
|
||||
* Aktiv/Archiv fuer Firmenanfragen.
|
||||
*
|
||||
* Abgeschlossene und abgelehnte Anfragen wandern ins Archiv, sobald der
|
||||
* Status gesetzt ist. Wird der Status wieder auf "Neu" oder "In Bearbeitung"
|
||||
* gestellt, steht die Anfrage automatisch wieder in der aktiven Liste — es
|
||||
* gibt keine eigene Archiv-Markierung, die man vergessen koennte.
|
||||
*/
|
||||
import type { Prisma } from "@/generated/prisma/client";
|
||||
|
||||
export const INQUIRY_STATUSES = ["NEU", "IN_BEARBEITUNG", "ABGESCHLOSSEN", "ABGELEHNT"] as const;
|
||||
export type InquiryStatusValue = (typeof INQUIRY_STATUSES)[number];
|
||||
|
||||
export const INQUIRY_DONE_STATUSES = ["ABGESCHLOSSEN", "ABGELEHNT"] as const satisfies readonly InquiryStatusValue[];
|
||||
|
||||
export function isInquiryStatus(value: string): value is InquiryStatusValue {
|
||||
return (INQUIRY_STATUSES as readonly string[]).includes(value);
|
||||
}
|
||||
|
||||
export function inquiryViewWhere(isArchive: boolean) {
|
||||
return {
|
||||
status: isArchive ? { in: [...INQUIRY_DONE_STATUSES] } : { notIn: [...INQUIRY_DONE_STATUSES] },
|
||||
} satisfies Prisma.BusinessInquiryWhereInput;
|
||||
}
|
||||
@@ -0,0 +1,51 @@
|
||||
import { describe, it, expect } from "vitest";
|
||||
import {
|
||||
reviewDecision,
|
||||
pendingReviewWhere,
|
||||
approvedReviewWhere,
|
||||
rejectedReviewWhere,
|
||||
} from "./review-status";
|
||||
import { inquiryViewWhere, isInquiryStatus } from "./inquiry-status";
|
||||
|
||||
describe("reviewDecision", () => {
|
||||
it("ist offen, solange weder freigeschaltet noch abgelehnt", () => {
|
||||
expect(reviewDecision({ isApproved: false, rejectedAt: null })).toBe("pending");
|
||||
});
|
||||
|
||||
it("ist abgelehnt, wenn rejectedAt gesetzt ist", () => {
|
||||
expect(reviewDecision({ isApproved: false, rejectedAt: new Date("2026-10-01T00:00:00Z") })).toBe("rejected");
|
||||
});
|
||||
|
||||
it("Freischaltung schlaegt eine fruehere Ablehnung", () => {
|
||||
expect(reviewDecision({ isApproved: true, rejectedAt: new Date("2026-10-01T00:00:00Z") })).toBe("approved");
|
||||
});
|
||||
});
|
||||
|
||||
describe("Bewertungs-Filter", () => {
|
||||
it("offene Bewertungen: weder freigeschaltet noch abgelehnt", () => {
|
||||
expect(pendingReviewWhere()).toEqual({ isApproved: false, rejectedAt: null });
|
||||
});
|
||||
|
||||
it("akzeptiert und abgelehnt ueberschneiden sich nicht", () => {
|
||||
expect(approvedReviewWhere()).toEqual({ isApproved: true });
|
||||
expect(rejectedReviewWhere()).toEqual({ isApproved: false, rejectedAt: { not: null } });
|
||||
});
|
||||
});
|
||||
|
||||
describe("inquiryViewWhere", () => {
|
||||
it("aktive Liste: alles ausser abgeschlossen und abgelehnt", () => {
|
||||
expect(inquiryViewWhere(false)).toEqual({ status: { notIn: ["ABGESCHLOSSEN", "ABGELEHNT"] } });
|
||||
});
|
||||
|
||||
it("Archiv: abgeschlossen und abgelehnt", () => {
|
||||
expect(inquiryViewWhere(true)).toEqual({ status: { in: ["ABGESCHLOSSEN", "ABGELEHNT"] } });
|
||||
});
|
||||
});
|
||||
|
||||
describe("isInquiryStatus", () => {
|
||||
it("akzeptiert nur die vier bekannten Status", () => {
|
||||
expect(isInquiryStatus("NEU")).toBe(true);
|
||||
expect(isInquiryStatus("ABGELEHNT")).toBe(true);
|
||||
expect(isInquiryStatus("GELOESCHT")).toBe(false);
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,33 @@
|
||||
/**
|
||||
* Bearbeitungsstand einer Bewertung im Admin.
|
||||
*
|
||||
* Eine Bewertung ist offen, bis der Admin sie freischaltet (isApproved) oder
|
||||
* ablehnt (rejectedAt). Offene stehen in der aktiven Liste, entschiedene im
|
||||
* Archiv — dort getrennt nach "Akzeptiert" und "Abgelehnt". Der Shop zeigt
|
||||
* weiterhin ausschliesslich isApproved-Bewertungen (lib/reviews.ts).
|
||||
*
|
||||
* Anders als Bestellungen und Retouren (lib/archive.ts) gibt es hier keine
|
||||
* Monatsfrist: eine entschiedene Bewertung braucht keine weitere Arbeit, sie
|
||||
* gehoert sofort ins Archiv.
|
||||
*/
|
||||
import type { Prisma } from "@/generated/prisma/client";
|
||||
|
||||
export type ReviewDecision = "pending" | "approved" | "rejected";
|
||||
|
||||
export function reviewDecision(review: { isApproved: boolean; rejectedAt: Date | null }): ReviewDecision {
|
||||
if (review.isApproved) return "approved";
|
||||
if (review.rejectedAt) return "rejected";
|
||||
return "pending";
|
||||
}
|
||||
|
||||
export function pendingReviewWhere() {
|
||||
return { isApproved: false, rejectedAt: null } satisfies Prisma.ReviewWhereInput;
|
||||
}
|
||||
|
||||
export function approvedReviewWhere() {
|
||||
return { isApproved: true } satisfies Prisma.ReviewWhereInput;
|
||||
}
|
||||
|
||||
export function rejectedReviewWhere() {
|
||||
return { isApproved: false, rejectedAt: { not: null } } satisfies Prisma.ReviewWhereInput;
|
||||
}
|
||||
Reference in new issue
Block a user