iloom-flatten/src/pages/washing/PaymentPending.tsx
Chever John b1c2c33aa2
refactor: replace Supabase SDK with self-hosted REST API client layer
WHY: migrating from Supabase BaaS to self-hosted iloom backend
(Go microservices) deployed on K3s. Supabase client and realtime
subscriptions no longer applicable.

HOW:
- add src/api/ layer (client.ts, auth.ts, purchaser.ts, textile.ts,
  washing.ts, websocket.ts) targeting iloom-gateway REST endpoints
- rewrite all hooks (usePlanData, useInventoryData, useDashboardData,
  useRealtime, etc.) to use new API client instead of Supabase queries
- rewrite all page/component data fetching and mutations accordingly
- delete src/supabase/client.ts, remove @supabase/supabase-js dep
- add Dockerfile (multi-stage node build + nginx) and nginx.conf
  with reverse proxy to iloom-gateway for /api/ and /ws/ routes
- adjust webpack.config.js for API_URL environment variable injection

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-06-14 12:38:47 +08:00

147 lines
5.9 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

import React, { useState, useEffect } from 'react';
import { motion } from 'framer-motion';
import { DollarSign, CheckCircle, ArrowLeft } from 'lucide-react';
import { useNavigate } from 'react-router-dom';
import { useAuth } from '../../contexts/AuthContext';
import { washingApi } from '../../api';
import type { Payment, WashingPlan } from '../../types';
interface PaymentWithPlan extends Payment {
plan?: WashingPlan;
}
export function WashingPaymentPending() {
const navigate = useNavigate();
const { auth } = useAuth();
const [payments, setPayments] = useState<PaymentWithPlan[]>([]);
const [loading, setLoading] = useState(true);
const [confirming, setConfirming] = useState<string | null>(null);
useEffect(() => {
if (!auth.company) return;
fetchPayments();
}, [auth.company]);
const fetchPayments = async () => {
const response = await washingApi.listPayments();
const paymentData = response.data || [];
if (paymentData.length === 0) {
setPayments([]);
setLoading(false);
return;
}
const paymentsWithPlans = paymentData.map((p: any) => ({
...p,
plan: p.washing_plan
}));
setPayments(paymentsWithPlans);
setLoading(false);
};
const totalAmount = payments.reduce((sum, item) => sum + item.amount, 0);
const handleConfirmPay = async (paymentId: string) => {
setConfirming(paymentId);
try {
await washingApi.confirmPayment(paymentId);
await fetchPayments();
} catch {
}
setConfirming(null);
};
return (
<div className="min-h-screen bg-gradient-to-br from-slate-50 via-rose-50 to-pink-50 p-3 sm:p-4 md:p-6">
<div className="max-w-4xl mx-auto">
<div className="flex items-center gap-2 sm:gap-3 md:gap-4 mb-4 sm:mb-5 md:mb-6">
<motion.button
whileTap={{ scale: 0.95 }}
onClick={() => navigate('/washing')}
className="p-2 hover:bg-gray-200 rounded-full transition-colors will-change-transform"
>
<ArrowLeft className="w-5 h-5 sm:w-6 sm:h-6 text-gray-600" />
</motion.button>
<h1 className="text-lg sm:text-xl md:text-2xl font-bold text-gray-800"></h1>
</div>
{loading ? (
<div className="flex flex-col items-center justify-center py-12 sm:py-16">
<motion.div
animate={{ rotate: 360 }}
transition={{ duration: 1, repeat: Infinity, ease: 'linear' }}
className="w-6 h-6 sm:w-8 sm:h-8 border-2 border-rose-500 border-t-transparent rounded-full mb-3"
/>
<p className="text-gray-400 text-sm">...</p>
</div>
) : (
<>
<motion.div
initial={{ opacity: 0, y: 10 }}
animate={{ opacity: 1, y: 0 }}
transition={{ duration: 0.3 }}
className="bg-gradient-to-r from-rose-500 to-pink-500 rounded-xl p-4 sm:p-5 md:p-6 text-white mb-4 sm:mb-5 md:mb-6"
>
<div className="flex items-center gap-2 sm:gap-3">
<DollarSign className="w-6 h-6 sm:w-7 sm:h-7 md:w-8 md:h-8" />
<div>
<p className="text-rose-100 text-xs sm:text-sm"></p>
<p className="text-2xl sm:text-3xl md:text-4xl font-bold">¥{totalAmount.toLocaleString()}</p>
</div>
</div>
</motion.div>
{payments.length === 0 ? (
<motion.div
initial={{ opacity: 0, y: 10 }}
animate={{ opacity: 1, y: 0 }}
transition={{ duration: 0.3 }}
className="bg-white rounded-xl shadow-sm p-6 sm:p-8 text-center text-gray-400 text-sm"
>
</motion.div>
) : (
<div className="space-y-3 sm:space-y-4">
{payments.map((item, index) => (
<motion.div
key={item.id}
initial={{ opacity: 0, y: 15 }}
animate={{ opacity: 1, y: 0 }}
transition={{ delay: Math.min(index * 0.03, 0.2), duration: 0.25, ease: [0.25, 0.46, 0.45, 0.94] }}
className="bg-white rounded-xl shadow-sm p-4 sm:p-5 border border-gray-200"
>
<div className="flex items-start justify-between gap-2">
<div className="min-w-0 flex-1">
<div className="flex items-center gap-2 mb-1">
<span className="text-[10px] sm:text-xs text-gray-400">{item.plan?.plan_code || '-'}</span>
</div>
<p className="text-xs sm:text-sm text-gray-500">: {item.quantity} × ¥{item.price_per_meter}/</p>
</div>
<div className="text-right flex-shrink-0">
<p className="text-lg sm:text-xl md:text-2xl font-bold text-rose-600">¥{item.amount.toLocaleString()}</p>
</div>
</div>
<div className="mt-3 sm:mt-4 pt-3 sm:pt-4 border-t border-gray-100 flex justify-end">
<motion.button
whileTap={{ scale: 0.97 }}
onClick={() => handleConfirmPay(item.id)}
disabled={confirming === item.id}
className="flex items-center gap-1.5 sm:gap-2 px-3 sm:px-4 py-1.5 sm:py-2 bg-green-500 text-white rounded-lg hover:bg-green-600 transition-colors disabled:opacity-50 text-xs sm:text-sm will-change-transform"
>
<CheckCircle className="w-3.5 h-3.5 sm:w-4 sm:h-4" />
{confirming === item.id ? '确认中...' : '确认结款'}
</motion.button>
</div>
</motion.div>
))}
</div>
)}
</>
)}
</div>
</div>
);
}