This commit is contained in:
parent
5569f1097c
commit
488eeac64c
|
|
@ -3,7 +3,7 @@
|
||||||
*/
|
*/
|
||||||
"use client";
|
"use client";
|
||||||
|
|
||||||
import { useState, useEffect } from "react";
|
import { useState, useEffect, useRef } from "react";
|
||||||
import { useRouter } from "next/navigation";
|
import { useRouter } from "next/navigation";
|
||||||
import { format } from "date-fns";
|
import { format } from "date-fns";
|
||||||
import { zhTW } from "date-fns/locale";
|
import { zhTW } from "date-fns/locale";
|
||||||
|
|
@ -34,7 +34,7 @@ import {
|
||||||
getReportCountByMarketType,
|
getReportCountByMarketType,
|
||||||
type SavedReport,
|
type SavedReport,
|
||||||
} from "@/lib/reports-db";
|
} from "@/lib/reports-db";
|
||||||
import { getCloudReports, deleteCloudReport, isCloudSyncEnabled } from "@/lib/user-api";
|
import { getCloudReports, deleteCloudReport, saveCloudReport, isCloudSyncEnabled } from "@/lib/user-api";
|
||||||
import { LoginPrompt } from "@/components/auth/login-button";
|
import { LoginPrompt } from "@/components/auth/login-button";
|
||||||
import { PendingTaskRecovery } from "@/components/PendingTaskRecovery";
|
import { PendingTaskRecovery } from "@/components/PendingTaskRecovery";
|
||||||
|
|
||||||
|
|
@ -152,6 +152,11 @@ export default function HistoryPage() {
|
||||||
);
|
);
|
||||||
const [deleting, setDeleting] = useState(false);
|
const [deleting, setDeleting] = useState(false);
|
||||||
|
|
||||||
|
// Sync state
|
||||||
|
const [syncing, setSyncing] = useState(false);
|
||||||
|
const [syncResult, setSyncResult] = useState<{ success: number; failed: number } | null>(null);
|
||||||
|
const hasAutoSyncedRef = useRef(false);
|
||||||
|
|
||||||
// Load reports when tab changes or auth state changes
|
// Load reports when tab changes or auth state changes
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
loadReports();
|
loadReports();
|
||||||
|
|
@ -162,6 +167,75 @@ export default function HistoryPage() {
|
||||||
loadCounts();
|
loadCounts();
|
||||||
}, [isAuthenticated]);
|
}, [isAuthenticated]);
|
||||||
|
|
||||||
|
// Auto-sync local reports to cloud when page loads (if authenticated)
|
||||||
|
useEffect(() => {
|
||||||
|
const autoSync = async () => {
|
||||||
|
// Only sync once per session, and only if authenticated
|
||||||
|
if (hasAutoSyncedRef.current || !isAuthenticated || !isCloudSyncEnabled()) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
hasAutoSyncedRef.current = true;
|
||||||
|
|
||||||
|
try {
|
||||||
|
// Get all local reports
|
||||||
|
const [usLocal, twseLocal, tpexLocal] = await Promise.all([
|
||||||
|
getReportsByMarketType("us"),
|
||||||
|
getReportsByMarketType("twse"),
|
||||||
|
getReportsByMarketType("tpex"),
|
||||||
|
]);
|
||||||
|
const allLocal = [...usLocal, ...twseLocal, ...tpexLocal];
|
||||||
|
|
||||||
|
if (allLocal.length === 0) return;
|
||||||
|
|
||||||
|
// Get cloud reports to check for duplicates
|
||||||
|
const cloudReports = await getCloudReports();
|
||||||
|
const cloudKeys = new Set(
|
||||||
|
cloudReports.map(r => `${r.ticker}_${r.analysis_date}`)
|
||||||
|
);
|
||||||
|
|
||||||
|
// Find local-only reports to upload
|
||||||
|
const toUpload = allLocal.filter(
|
||||||
|
r => !cloudKeys.has(`${r.ticker}_${r.analysis_date}`)
|
||||||
|
);
|
||||||
|
|
||||||
|
if (toUpload.length === 0) {
|
||||||
|
console.log("☁️ Auto-sync: All reports already in cloud");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
console.log(`☁️ Auto-sync: Uploading ${toUpload.length} local reports to cloud...`);
|
||||||
|
|
||||||
|
// Upload each report silently
|
||||||
|
let success = 0;
|
||||||
|
for (const report of toUpload) {
|
||||||
|
try {
|
||||||
|
const cloudId = await saveCloudReport({
|
||||||
|
ticker: report.ticker,
|
||||||
|
market_type: report.market_type,
|
||||||
|
analysis_date: report.analysis_date,
|
||||||
|
result: report.result,
|
||||||
|
});
|
||||||
|
if (cloudId) success++;
|
||||||
|
} catch (e) {
|
||||||
|
// Silently continue on error
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (success > 0) {
|
||||||
|
console.log(`☁️ Auto-sync: Successfully uploaded ${success} reports`);
|
||||||
|
// Reload to show updated data
|
||||||
|
await loadReports();
|
||||||
|
await loadCounts();
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error("☁️ Auto-sync failed:", error);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
autoSync();
|
||||||
|
}, [isAuthenticated]);
|
||||||
|
|
||||||
const loadReports = async () => {
|
const loadReports = async () => {
|
||||||
setLoading(true);
|
setLoading(true);
|
||||||
try {
|
try {
|
||||||
|
|
@ -227,26 +301,134 @@ export default function HistoryPage() {
|
||||||
|
|
||||||
const loadCounts = async () => {
|
const loadCounts = async () => {
|
||||||
try {
|
try {
|
||||||
|
// Always get local counts first
|
||||||
|
const localCounts = await getReportCountByMarketType();
|
||||||
|
|
||||||
if (isAuthenticated && isCloudSyncEnabled()) {
|
if (isAuthenticated && isCloudSyncEnabled()) {
|
||||||
const cloudReports = await getCloudReports();
|
const cloudReports = await getCloudReports();
|
||||||
const cloudCounts = {
|
|
||||||
us: cloudReports.filter(r => r.market_type === "us").length,
|
|
||||||
twse: cloudReports.filter(r => r.market_type === "twse").length,
|
|
||||||
tpex: cloudReports.filter(r => r.market_type === "tpex").length,
|
|
||||||
};
|
|
||||||
if (cloudReports.length > 0) {
|
if (cloudReports.length > 0) {
|
||||||
setCounts(cloudCounts);
|
// Get local reports to check for duplicates
|
||||||
|
const [usLocal, twseLocal, tpexLocal] = await Promise.all([
|
||||||
|
getReportsByMarketType("us"),
|
||||||
|
getReportsByMarketType("twse"),
|
||||||
|
getReportsByMarketType("tpex"),
|
||||||
|
]);
|
||||||
|
|
||||||
|
// Cloud report keys for deduplication
|
||||||
|
const cloudKeys = new Set(
|
||||||
|
cloudReports.map(r => `${r.ticker}_${r.analysis_date}_${r.market_type}`)
|
||||||
|
);
|
||||||
|
|
||||||
|
// Count local-only reports (not in cloud)
|
||||||
|
const usLocalOnly = usLocal.filter(
|
||||||
|
r => !cloudKeys.has(`${r.ticker}_${r.analysis_date}_us`)
|
||||||
|
).length;
|
||||||
|
const twseLocalOnly = twseLocal.filter(
|
||||||
|
r => !cloudKeys.has(`${r.ticker}_${r.analysis_date}_twse`)
|
||||||
|
).length;
|
||||||
|
const tpexLocalOnly = tpexLocal.filter(
|
||||||
|
r => !cloudKeys.has(`${r.ticker}_${r.analysis_date}_tpex`)
|
||||||
|
).length;
|
||||||
|
|
||||||
|
// Cloud counts
|
||||||
|
const usCoud = cloudReports.filter(r => r.market_type === "us").length;
|
||||||
|
const twseCloud = cloudReports.filter(r => r.market_type === "twse").length;
|
||||||
|
const tpexCloud = cloudReports.filter(r => r.market_type === "tpex").length;
|
||||||
|
|
||||||
|
// Merged counts: cloud + local-only
|
||||||
|
setCounts({
|
||||||
|
us: usCoud + usLocalOnly,
|
||||||
|
twse: twseCloud + twseLocalOnly,
|
||||||
|
tpex: tpexCloud + tpexLocalOnly,
|
||||||
|
});
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const data = await getReportCountByMarketType();
|
setCounts(localCounts);
|
||||||
setCounts(data);
|
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error("Failed to load counts:", error);
|
console.error("Failed to load counts:", error);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
// Sync local reports to cloud
|
||||||
|
const handleSyncToCloud = async () => {
|
||||||
|
if (!isAuthenticated || !isCloudSyncEnabled()) {
|
||||||
|
alert("請先登入以啟用雲端同步");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
setSyncing(true);
|
||||||
|
setSyncResult(null);
|
||||||
|
|
||||||
|
try {
|
||||||
|
// Get all local reports
|
||||||
|
const [usLocal, twseLocal, tpexLocal] = await Promise.all([
|
||||||
|
getReportsByMarketType("us"),
|
||||||
|
getReportsByMarketType("twse"),
|
||||||
|
getReportsByMarketType("tpex"),
|
||||||
|
]);
|
||||||
|
const allLocal = [...usLocal, ...twseLocal, ...tpexLocal];
|
||||||
|
|
||||||
|
// Get cloud reports to check for duplicates
|
||||||
|
const cloudReports = await getCloudReports();
|
||||||
|
const cloudKeys = new Set(
|
||||||
|
cloudReports.map(r => `${r.ticker}_${r.analysis_date}`)
|
||||||
|
);
|
||||||
|
|
||||||
|
// Find local-only reports to upload
|
||||||
|
const toUpload = allLocal.filter(
|
||||||
|
r => !cloudKeys.has(`${r.ticker}_${r.analysis_date}`)
|
||||||
|
);
|
||||||
|
|
||||||
|
if (toUpload.length === 0) {
|
||||||
|
setSyncResult({ success: 0, failed: 0 });
|
||||||
|
alert("所有報告已同步到雲端!");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Upload each report
|
||||||
|
let success = 0;
|
||||||
|
let failed = 0;
|
||||||
|
|
||||||
|
for (const report of toUpload) {
|
||||||
|
try {
|
||||||
|
const cloudId = await saveCloudReport({
|
||||||
|
ticker: report.ticker,
|
||||||
|
market_type: report.market_type,
|
||||||
|
analysis_date: report.analysis_date,
|
||||||
|
result: report.result,
|
||||||
|
});
|
||||||
|
if (cloudId) {
|
||||||
|
success++;
|
||||||
|
} else {
|
||||||
|
failed++;
|
||||||
|
}
|
||||||
|
} catch (e) {
|
||||||
|
failed++;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
setSyncResult({ success, failed });
|
||||||
|
|
||||||
|
// Reload data after sync
|
||||||
|
await loadReports();
|
||||||
|
await loadCounts();
|
||||||
|
|
||||||
|
if (failed === 0) {
|
||||||
|
alert(`成功同步 ${success} 份報告到雲端!`);
|
||||||
|
} else {
|
||||||
|
alert(`同步完成:${success} 成功,${failed} 失敗`);
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error("Sync failed:", error);
|
||||||
|
alert("同步失敗,請稍後再試");
|
||||||
|
} finally {
|
||||||
|
setSyncing(false);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
const handleViewReport = (report: SavedReport) => {
|
const handleViewReport = (report: SavedReport) => {
|
||||||
// Set the context with the saved report data
|
// Set the context with the saved report data
|
||||||
setAnalysisResult(report.result);
|
setAnalysisResult(report.result);
|
||||||
|
|
@ -410,8 +592,25 @@ export default function HistoryPage() {
|
||||||
(marketType) => (
|
(marketType) => (
|
||||||
<TabsContent key={marketType} value={marketType} className="mt-6">
|
<TabsContent key={marketType} value={marketType} className="mt-6">
|
||||||
<div className="space-y-4">
|
<div className="space-y-4">
|
||||||
{/* Refresh button */}
|
{/* Action buttons */}
|
||||||
<div className="flex justify-end">
|
<div className="flex justify-end gap-2">
|
||||||
|
{/* Sync to Cloud button - only show when authenticated */}
|
||||||
|
{isAuthenticated && (
|
||||||
|
<Button
|
||||||
|
variant="outline"
|
||||||
|
size="sm"
|
||||||
|
onClick={handleSyncToCloud}
|
||||||
|
disabled={syncing || loading}
|
||||||
|
className="gap-2"
|
||||||
|
>
|
||||||
|
<Cloud
|
||||||
|
className={`h-4 w-4 ${syncing ? "animate-pulse" : ""}`}
|
||||||
|
/>
|
||||||
|
{syncing ? "同步中..." : "同步到雲端"}
|
||||||
|
</Button>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{/* Refresh button */}
|
||||||
<Button
|
<Button
|
||||||
variant="outline"
|
variant="outline"
|
||||||
size="sm"
|
size="sm"
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue