Implement post-implementation refinements

Features implemented:
1. Expired QRIS order handling with dual-path approach
   - Product orders: QR regeneration button
   - Consulting orders: Immediate cancellation with slot release
2. Standardized status badge wording to "Pending"
3. Fixed TypeScript error in MemberDashboard
4. Dynamic badge colors from branding settings
5. Dynamic page title from branding settings
6. Logo/favicon file upload with auto-delete

🤖 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-24 11:42:20 +07:00
parent 4b8765885b
commit fb24e77e42
15 changed files with 779 additions and 149 deletions

View File

@@ -11,6 +11,7 @@ import { formatIDR } from "@/lib/format";
import { Video, Calendar, BookOpen, ArrowRight, Package, Receipt, ShoppingBag } from "lucide-react";
import { WhatsAppBanner } from "@/components/WhatsAppBanner";
import { ConsultingHistory } from "@/components/reviews/ConsultingHistory";
import { UnpaidOrderAlert } from "@/components/UnpaidOrderAlert";
interface UserAccess {
id: string;
@@ -31,11 +32,17 @@ interface Order {
created_at: string;
}
interface UnpaidConsultingOrder {
order_id: string;
qr_expires_at: string;
}
export default function MemberDashboard() {
const { user, loading: authLoading } = useAuth();
const navigate = useNavigate();
const [access, setAccess] = useState<UserAccess[]>([]);
const [recentOrders, setRecentOrders] = useState<Order[]>([]);
const [unpaidConsultingOrders, setUnpaidConsultingOrders] = useState<UnpaidConsultingOrder[]>([]);
const [loading, setLoading] = useState(true);
const [hasWhatsApp, setHasWhatsApp] = useState(true);
@@ -44,6 +51,57 @@ export default function MemberDashboard() {
else if (user) fetchData();
}, [user, authLoading]);
// Fetch unpaid consulting orders
useEffect(() => {
if (!user) return;
const fetchUnpaidOrders = async () => {
const { data, error } = await supabase
.from('consulting_slots')
.select(`
order_id,
orders (
id,
payment_status,
qr_expires_at
)
`)
.eq('orders.payment_status', 'pending')
.eq('status', 'pending_payment')
.gt('orders.qr_expires_at', new Date().toISOString())
.order('created_at', { ascending: false });
if (!error && data) {
// Get unique order IDs
const uniqueOrders = Array.from(
new Set(data.map((item: any) => item.order_id))
).map((orderId) => {
// Find the corresponding order data
const orderData = data.find((item: any) => item.order_id === orderId);
return {
order_id: orderId,
qr_expires_at: (orderData as any)?.orders?.qr_expires_at || ''
};
});
setUnpaidConsultingOrders(uniqueOrders);
}
};
fetchUnpaidOrders();
}, [user]);
// Auto-hide expired orders every 30 seconds
useEffect(() => {
const checkExpiry = () => {
setUnpaidConsultingOrders(prev =>
prev.filter(order => new Date(order.qr_expires_at) > new Date())
);
};
const interval = setInterval(checkExpiry, 30000); // Check every 30s
return () => clearInterval(interval);
}, []);
const fetchData = async () => {
const [accessRes, ordersRes, paidOrdersRes, profileRes] = await Promise.all([
supabase
@@ -123,6 +181,16 @@ export default function MemberDashboard() {
<h1 className="text-4xl font-bold mb-2">Dashboard</h1>
<p className="text-muted-foreground mb-8">Selamat datang kembali!</p>
{/* Unpaid Order Alert - shown when user has unpaid consulting orders */}
{unpaidConsultingOrders.length > 0 && (
<div className="mb-6">
<UnpaidOrderAlert
orderId={unpaidConsultingOrders[0].order_id}
expiresAt={unpaidConsultingOrders[0].qr_expires_at}
/>
</div>
)}
{!hasWhatsApp && <WhatsAppBanner />}
<div className="grid gap-6 md:grid-cols-2 lg:grid-cols-3 mb-8">
@@ -225,7 +293,7 @@ export default function MemberDashboard() {
</p>
</div>
<div className="flex items-center gap-4">
<Badge className={order.payment_status === "paid" ? "bg-accent" : "bg-muted"}>
<Badge className={order.payment_status === "paid" ? "bg-brand-accent text-white" : "bg-muted text-primary"}>
{order.payment_status === "paid" ? "Lunas" : "Pending"}
</Badge>
<span className="font-bold">{formatIDR(order.total_amount)}</span>