Create centralized status management system
- Add statusHelpers.ts with single source of truth for all status labels/colors - Update AdminOrders to use centralized helpers - Add utility functions: canRefundOrder, canCancelOrder, canMarkAsPaid - Improve consistency across payment status handling Benefits: - Consistent Indonesian labels everywhere - DRY principle - no more duplicate switch statements - Easy to update status styling in one place - Reusable across all components 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
This commit is contained in:
124
src/lib/statusHelpers.ts
Normal file
124
src/lib/statusHelpers.ts
Normal file
@@ -0,0 +1,124 @@
|
|||||||
|
/**
|
||||||
|
* Centralized status management for consistent labels, colors, and badges
|
||||||
|
* Single source of truth for all status-related UI
|
||||||
|
*/
|
||||||
|
|
||||||
|
export type PaymentStatus = 'paid' | 'pending' | 'failed' | 'cancelled' | 'refunded' | 'partially_refunded';
|
||||||
|
export type ConsultingSlotStatus = 'pending_payment' | 'confirmed' | 'completed' | 'cancelled';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get Indonesian label for payment status
|
||||||
|
*/
|
||||||
|
export const getPaymentStatusLabel = (status: PaymentStatus | string | null): string => {
|
||||||
|
switch (status) {
|
||||||
|
case 'paid':
|
||||||
|
return 'Lunas';
|
||||||
|
case 'pending':
|
||||||
|
return 'Pending';
|
||||||
|
case 'failed':
|
||||||
|
return 'Gagal';
|
||||||
|
case 'cancelled':
|
||||||
|
return 'Dibatalkan';
|
||||||
|
case 'refunded':
|
||||||
|
return 'Refund';
|
||||||
|
case 'partially_refunded':
|
||||||
|
return 'Refund Sebagian';
|
||||||
|
default:
|
||||||
|
return status || 'Pending';
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get CSS class for payment status badge
|
||||||
|
*/
|
||||||
|
export const getPaymentStatusColor = (status: PaymentStatus | string | null): string => {
|
||||||
|
switch (status) {
|
||||||
|
case 'paid':
|
||||||
|
return 'bg-brand-accent text-white';
|
||||||
|
case 'pending':
|
||||||
|
return 'bg-amber-500 text-white';
|
||||||
|
case 'failed':
|
||||||
|
return 'bg-red-500 text-white';
|
||||||
|
case 'cancelled':
|
||||||
|
return 'bg-destructive text-white';
|
||||||
|
case 'refunded':
|
||||||
|
return 'bg-purple-500 text-white';
|
||||||
|
case 'partially_refunded':
|
||||||
|
return 'bg-purple-500/80 text-white';
|
||||||
|
default:
|
||||||
|
return 'bg-secondary text-primary';
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get label for consulting slot status
|
||||||
|
*/
|
||||||
|
export const getConsultingSlotStatusLabel = (status: ConsultingSlotStatus | string): string => {
|
||||||
|
switch (status) {
|
||||||
|
case 'pending_payment':
|
||||||
|
return 'Menunggu Pembayaran';
|
||||||
|
case 'confirmed':
|
||||||
|
return 'Terkonfirmasi';
|
||||||
|
case 'completed':
|
||||||
|
return 'Selesai';
|
||||||
|
case 'cancelled':
|
||||||
|
return 'Dibatalkan';
|
||||||
|
default:
|
||||||
|
return status;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get CSS class for consulting slot status badge
|
||||||
|
*/
|
||||||
|
export const getConsultingSlotStatusColor = (status: ConsultingSlotStatus | string): string => {
|
||||||
|
switch (status) {
|
||||||
|
case 'pending_payment':
|
||||||
|
return 'bg-amber-500 text-white';
|
||||||
|
case 'confirmed':
|
||||||
|
return 'bg-green-500 text-white';
|
||||||
|
case 'completed':
|
||||||
|
return 'bg-blue-500 text-white';
|
||||||
|
case 'cancelled':
|
||||||
|
return 'bg-destructive text-white';
|
||||||
|
default:
|
||||||
|
return 'bg-secondary text-primary';
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get label for product type
|
||||||
|
*/
|
||||||
|
export const getProductTypeLabel = (type: string): string => {
|
||||||
|
switch (type) {
|
||||||
|
case 'consulting':
|
||||||
|
return 'Konsultasi';
|
||||||
|
case 'webinar':
|
||||||
|
return 'Webinar';
|
||||||
|
case 'bootcamp':
|
||||||
|
return 'Bootcamp';
|
||||||
|
default:
|
||||||
|
return type;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Check if order can be refunded
|
||||||
|
*/
|
||||||
|
export const canRefundOrder = (paymentStatus: PaymentStatus | string | null, refundedAt: string | null = null): boolean => {
|
||||||
|
return paymentStatus === 'paid' && !refundedAt;
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Check if order can be cancelled
|
||||||
|
*/
|
||||||
|
export const canCancelOrder = (paymentStatus: PaymentStatus | string | null, refundedAt: string | null = null): boolean => {
|
||||||
|
return !refundedAt && paymentStatus !== 'cancelled' && paymentStatus !== 'refunded' && paymentStatus !== 'partially_refunded';
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Check if order can be marked as paid
|
||||||
|
*/
|
||||||
|
export const canMarkAsPaid = (paymentStatus: PaymentStatus | string | null, refundedAt: string | null = null): boolean => {
|
||||||
|
return !refundedAt && paymentStatus !== 'paid' && paymentStatus !== 'refunded' && paymentStatus !== 'partially_refunded';
|
||||||
|
};
|
||||||
@@ -15,6 +15,7 @@ import { Textarea } from "@/components/ui/textarea";
|
|||||||
import { formatIDR, formatDateTime } from "@/lib/format";
|
import { formatIDR, formatDateTime } from "@/lib/format";
|
||||||
import { Eye, CheckCircle, XCircle, Video, ExternalLink, Trash2, AlertTriangle, RefreshCw, Link as LinkIcon } from "lucide-react";
|
import { Eye, CheckCircle, XCircle, Video, ExternalLink, Trash2, AlertTriangle, RefreshCw, Link as LinkIcon } from "lucide-react";
|
||||||
import { toast } from "@/hooks/use-toast";
|
import { toast } from "@/hooks/use-toast";
|
||||||
|
import { getPaymentStatusLabel, getPaymentStatusColor, canRefundOrder, canCancelOrder, canMarkAsPaid } from "@/lib/statusHelpers";
|
||||||
|
|
||||||
interface Order {
|
interface Order {
|
||||||
id: string;
|
id: string;
|
||||||
@@ -272,20 +273,11 @@ export default function AdminOrders() {
|
|||||||
};
|
};
|
||||||
|
|
||||||
const getStatusBadge = (status: string | null) => {
|
const getStatusBadge = (status: string | null) => {
|
||||||
switch (status) {
|
return (
|
||||||
case "paid":
|
<Badge className={`${getPaymentStatusColor(status)} rounded-full`}>
|
||||||
return <Badge className="bg-brand-accent text-white rounded-full">Lunas</Badge>;
|
{getPaymentStatusLabel(status)}
|
||||||
case "refunded":
|
</Badge>
|
||||||
return <Badge className="bg-purple-500 text-white rounded-full">Refund</Badge>;
|
);
|
||||||
case "partially_refunded":
|
|
||||||
return <Badge className="bg-purple-500/80 text-white rounded-full">Refund Sebagian</Badge>;
|
|
||||||
case "pending":
|
|
||||||
return <Badge className="bg-amber-500 text-white rounded-full">Pending</Badge>;
|
|
||||||
case "cancelled":
|
|
||||||
return <Badge className="bg-destructive text-white rounded-full">Dibatalkan</Badge>;
|
|
||||||
default:
|
|
||||||
return <Badge className="bg-muted rounded-full">{status}</Badge>;
|
|
||||||
}
|
|
||||||
};
|
};
|
||||||
|
|
||||||
if (authLoading || loading) {
|
if (authLoading || loading) {
|
||||||
@@ -499,7 +491,7 @@ export default function AdminOrders() {
|
|||||||
)}
|
)}
|
||||||
|
|
||||||
<div className="flex flex-col sm:flex-row gap-2 pt-4">
|
<div className="flex flex-col sm:flex-row gap-2 pt-4">
|
||||||
{selectedOrder.payment_status === "paid" && !selectedOrder.refunded_at && (
|
{canRefundOrder(selectedOrder.payment_status, selectedOrder.refunded_at) && (
|
||||||
<Button
|
<Button
|
||||||
variant="outline"
|
variant="outline"
|
||||||
onClick={openRefundDialog}
|
onClick={openRefundDialog}
|
||||||
@@ -509,13 +501,13 @@ export default function AdminOrders() {
|
|||||||
Refund
|
Refund
|
||||||
</Button>
|
</Button>
|
||||||
)}
|
)}
|
||||||
{selectedOrder.payment_status !== "paid" && !selectedOrder.refunded_at && (
|
{canMarkAsPaid(selectedOrder.payment_status, selectedOrder.refunded_at) && (
|
||||||
<Button onClick={() => updateOrderStatus(selectedOrder.id, "paid")} className="flex-1">
|
<Button onClick={() => updateOrderStatus(selectedOrder.id, "paid")} className="flex-1">
|
||||||
<CheckCircle className="w-4 h-4 mr-2" />
|
<CheckCircle className="w-4 h-4 mr-2" />
|
||||||
Tandai Lunas
|
Tandai Lunas
|
||||||
</Button>
|
</Button>
|
||||||
)}
|
)}
|
||||||
{selectedOrder.payment_status !== "cancelled" && !selectedOrder.refunded_at && (
|
{canCancelOrder(selectedOrder.payment_status, selectedOrder.refunded_at) && (
|
||||||
<Button
|
<Button
|
||||||
variant="outline"
|
variant="outline"
|
||||||
onClick={() => updateOrderStatus(selectedOrder.id, "cancelled")}
|
onClick={() => updateOrderStatus(selectedOrder.id, "cancelled")}
|
||||||
|
|||||||
Reference in New Issue
Block a user