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:
dwindown
2025-12-26 17:07:56 +07:00
parent 81bbafcff0
commit d089fcc769
2 changed files with 133 additions and 17 deletions

124
src/lib/statusHelpers.ts Normal file
View 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';
};

View File

@@ -15,6 +15,7 @@ import { Textarea } from "@/components/ui/textarea";
import { formatIDR, formatDateTime } from "@/lib/format";
import { Eye, CheckCircle, XCircle, Video, ExternalLink, Trash2, AlertTriangle, RefreshCw, Link as LinkIcon } from "lucide-react";
import { toast } from "@/hooks/use-toast";
import { getPaymentStatusLabel, getPaymentStatusColor, canRefundOrder, canCancelOrder, canMarkAsPaid } from "@/lib/statusHelpers";
interface Order {
id: string;
@@ -272,20 +273,11 @@ export default function AdminOrders() {
};
const getStatusBadge = (status: string | null) => {
switch (status) {
case "paid":
return <Badge className="bg-brand-accent text-white rounded-full">Lunas</Badge>;
case "refunded":
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>;
}
return (
<Badge className={`${getPaymentStatusColor(status)} rounded-full`}>
{getPaymentStatusLabel(status)}
</Badge>
);
};
if (authLoading || loading) {
@@ -499,7 +491,7 @@ export default function AdminOrders() {
)}
<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
variant="outline"
onClick={openRefundDialog}
@@ -509,13 +501,13 @@ export default function AdminOrders() {
Refund
</Button>
)}
{selectedOrder.payment_status !== "paid" && !selectedOrder.refunded_at && (
{canMarkAsPaid(selectedOrder.payment_status, selectedOrder.refunded_at) && (
<Button onClick={() => updateOrderStatus(selectedOrder.id, "paid")} className="flex-1">
<CheckCircle className="w-4 h-4 mr-2" />
Tandai Lunas
</Button>
)}
{selectedOrder.payment_status !== "cancelled" && !selectedOrder.refunded_at && (
{canCancelOrder(selectedOrder.payment_status, selectedOrder.refunded_at) && (
<Button
variant="outline"
onClick={() => updateOrderStatus(selectedOrder.id, "cancelled")}