login fix
This commit is contained in:
@@ -53,7 +53,8 @@ chrome.runtime.onMessage.addListener((msg, sender, sendResponse) => {
|
|||||||
|
|
||||||
// eBay Parsing Response (from eBay content script)
|
// eBay Parsing Response (from eBay content script)
|
||||||
if (msg?.action === "PARSE_COMPLETE") {
|
if (msg?.action === "PARSE_COMPLETE") {
|
||||||
handleParseComplete(sender.tab?.id, msg.data);
|
const tabId = sender?.tab?.id;
|
||||||
|
handleParseComplete(tabId, msg.data);
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
@@ -100,7 +101,10 @@ async function handleParseRequest(url, sendResponse) {
|
|||||||
url: url,
|
url: url,
|
||||||
active: false
|
active: false
|
||||||
});
|
});
|
||||||
|
if (!tab?.id) {
|
||||||
|
sendResponse({ ok: false, error: "Tab could not be created" });
|
||||||
|
return;
|
||||||
|
}
|
||||||
const tabId = tab.id;
|
const tabId = tab.id;
|
||||||
console.log("[BACKGROUND] Tab created:", tabId);
|
console.log("[BACKGROUND] Tab created:", tabId);
|
||||||
|
|
||||||
@@ -330,6 +334,7 @@ function handleParseComplete(tabId, data) {
|
|||||||
* Cleans up parse request: closes tab, clears timeout, sends response
|
* Cleans up parse request: closes tab, clears timeout, sends response
|
||||||
*/
|
*/
|
||||||
async function cleanupParseRequest(tabId, data, error) {
|
async function cleanupParseRequest(tabId, data, error) {
|
||||||
|
if (tabId == null) return;
|
||||||
const request = activeParseRequests.get(tabId);
|
const request = activeParseRequests.get(tabId);
|
||||||
if (!request) return;
|
if (!request) return;
|
||||||
|
|
||||||
@@ -410,7 +415,10 @@ async function handleScanProductsRequest(url, accountId, sendResponse) {
|
|||||||
url: targetUrl,
|
url: targetUrl,
|
||||||
active: false
|
active: false
|
||||||
});
|
});
|
||||||
|
if (!tab?.id) {
|
||||||
|
sendResponse({ ok: false, error: "Tab could not be created" });
|
||||||
|
return;
|
||||||
|
}
|
||||||
const tabId = tab.id;
|
const tabId = tab.id;
|
||||||
|
|
||||||
// Set up timeout
|
// Set up timeout
|
||||||
@@ -562,7 +570,10 @@ async function loadAndParseEachItemTab(items) {
|
|||||||
url: item.url,
|
url: item.url,
|
||||||
active: false
|
active: false
|
||||||
});
|
});
|
||||||
|
if (!tab?.id) {
|
||||||
|
console.warn(`[BACKGROUND] Tab could not be created for item ${i + 1}, skipping`);
|
||||||
|
continue;
|
||||||
|
}
|
||||||
await new Promise((resolve) => {
|
await new Promise((resolve) => {
|
||||||
const listener = (tabId, changeInfo) => {
|
const listener = (tabId, changeInfo) => {
|
||||||
if (tabId === tab.id && changeInfo.status === "complete") {
|
if (tabId === tab.id && changeInfo.status === "complete") {
|
||||||
@@ -647,6 +658,7 @@ async function handleScanComplete(tabId, data) {
|
|||||||
* Cleans up scan request: closes tab, clears timeout, sends response
|
* Cleans up scan request: closes tab, clears timeout, sends response
|
||||||
*/
|
*/
|
||||||
async function cleanupScanRequest(tabId, data, error) {
|
async function cleanupScanRequest(tabId, data, error) {
|
||||||
|
if (tabId == null) return;
|
||||||
const request = activeScanRequests.get(tabId);
|
const request = activeScanRequests.get(tabId);
|
||||||
if (!request) return;
|
if (!request) return;
|
||||||
|
|
||||||
@@ -751,7 +763,9 @@ async function handleParseAccountExtendedRequest(url, sendResponse) {
|
|||||||
url: url,
|
url: url,
|
||||||
active: false
|
active: false
|
||||||
});
|
});
|
||||||
|
if (!baseTab?.id) {
|
||||||
|
throw new Error("Tab could not be created");
|
||||||
|
}
|
||||||
await new Promise((resolve) => {
|
await new Promise((resolve) => {
|
||||||
const checkLoaded = (tabId, changeInfo) => {
|
const checkLoaded = (tabId, changeInfo) => {
|
||||||
if (tabId === baseTab.id && changeInfo.status === 'complete') {
|
if (tabId === baseTab.id && changeInfo.status === 'complete') {
|
||||||
@@ -849,6 +863,10 @@ async function parseSingleTab(url, action, timeoutMs) {
|
|||||||
url: url,
|
url: url,
|
||||||
active: false
|
active: false
|
||||||
});
|
});
|
||||||
|
if (!tab?.id) {
|
||||||
|
reject(new Error("Tab could not be created"));
|
||||||
|
return;
|
||||||
|
}
|
||||||
tabId = tab.id;
|
tabId = tab.id;
|
||||||
|
|
||||||
// Set up timeout
|
// Set up timeout
|
||||||
|
|||||||
@@ -9,6 +9,7 @@ import {
|
|||||||
IconSettings,
|
IconSettings,
|
||||||
IconUserBolt,
|
IconUserBolt,
|
||||||
IconShoppingBag,
|
IconShoppingBag,
|
||||||
|
IconRobot,
|
||||||
} from "@tabler/icons-react";
|
} from "@tabler/icons-react";
|
||||||
import { motion } from "motion/react";
|
import { motion } from "motion/react";
|
||||||
import { cn } from "./lib/utils";
|
import { cn } from "./lib/utils";
|
||||||
@@ -440,6 +441,17 @@ export default function App() {
|
|||||||
navigate("/blacklist");
|
navigate("/blacklist");
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
label: "KI",
|
||||||
|
href: "#/ki",
|
||||||
|
icon: (
|
||||||
|
<IconRobot className="h-5 w-5 shrink-0 text-neutral-700 dark:text-neutral-200" />
|
||||||
|
),
|
||||||
|
onClick: (e) => {
|
||||||
|
e.preventDefault();
|
||||||
|
navigate("/ki");
|
||||||
|
},
|
||||||
|
},
|
||||||
{
|
{
|
||||||
label: "Settings",
|
label: "Settings",
|
||||||
href: "#",
|
href: "#",
|
||||||
@@ -463,6 +475,17 @@ export default function App() {
|
|||||||
if (route === "/analysis") {
|
if (route === "/analysis") {
|
||||||
return <AnalysisPage />;
|
return <AnalysisPage />;
|
||||||
}
|
}
|
||||||
|
if (route === "/ki") {
|
||||||
|
return (
|
||||||
|
<div className="flex items-center justify-center h-full w-full">
|
||||||
|
<div className="text-center">
|
||||||
|
<IconRobot className="h-16 w-16 mx-auto mb-4 text-neutral-700 dark:text-neutral-200" />
|
||||||
|
<h1 className="text-2xl font-bold text-neutral-800 dark:text-neutral-100 mb-2">KI</h1>
|
||||||
|
<p className="text-neutral-600 dark:text-neutral-400">KI-Seite wird hier angezeigt</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
// Default: Dashboard
|
// Default: Dashboard
|
||||||
return <Dashboard />;
|
return <Dashboard />;
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -161,17 +161,20 @@ export const OverviewSection = ({ onJumpToSection, activeAccountId }) => {
|
|||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
<div className="relative">
|
<div className="relative">
|
||||||
<p className="mb-2 text-xs text-[var(--muted)]">Product overview</p>
|
<p className="mb-2 text-xs text-[var(--muted)]">News</p>
|
||||||
|
{loading ? (
|
||||||
|
<div className="mb-3 text-xs text-[var(--muted)]">Loading...</div>
|
||||||
|
) : (
|
||||||
<div className="mb-3 text-xs text-[var(--muted)]">
|
<div className="mb-3 text-xs text-[var(--muted)]">
|
||||||
{loading ? "Loading..." : kpis ? `${kpis.totalProducts} total products` : "No data"}
|
<ul className="list-none space-y-2 p-0 m-0">
|
||||||
|
<li className="text-[var(--muted)]">- System update available</li>
|
||||||
|
<li className="text-[var(--muted)]">- New features released</li>
|
||||||
|
<li className="text-[var(--muted)]">- Dashboard improvements</li>
|
||||||
|
</ul>
|
||||||
</div>
|
</div>
|
||||||
|
)}
|
||||||
<div className="flex items-center justify-between border-t border-[var(--line)] pt-3">
|
<div className="flex items-center justify-between border-t border-[var(--line)] pt-3">
|
||||||
<button
|
<div className="text-xs text-[var(--muted)]">Latest updates</div>
|
||||||
onClick={() => onJumpToSection("s3")}
|
|
||||||
className="rounded-xl border border-[var(--line)] bg-white/3 px-3 py-2 text-xs text-[var(--text)] transition-all hover:border-[rgba(106,166,255,0.55)] hover:bg-[rgba(106,166,255,0.08)] active:translate-y-[1px]"
|
|
||||||
>
|
|
||||||
Explore products
|
|
||||||
</button>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -206,6 +209,31 @@ export const OverviewSection = ({ onJumpToSection, activeAccountId }) => {
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
{activeAccountId && !loading && !error && kpis && (
|
||||||
|
<div className="relative overflow-hidden rounded-[18px] border border-[var(--line)] bg-gradient-to-b from-white/4 to-white/2 p-4 shadow-[0_10px_30px_rgba(0,0,0,0.35)]">
|
||||||
|
<div
|
||||||
|
className="pointer-events-none absolute inset-[-1px] opacity-55"
|
||||||
|
style={{
|
||||||
|
background: "radial-gradient(600px 280px at 20% 0%, rgba(106,166,255,0.14), transparent 60%)",
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
<div className="relative">
|
||||||
|
<p className="mb-2 text-xs text-[var(--muted)]">Product overview</p>
|
||||||
|
<div className="mb-3 text-xs text-[var(--muted)]">
|
||||||
|
{loading ? "Loading..." : kpis ? `${kpis.totalProducts} total products` : "No data"}
|
||||||
|
</div>
|
||||||
|
<div className="flex items-center justify-between border-t border-[var(--line)] pt-3">
|
||||||
|
<button
|
||||||
|
onClick={() => onJumpToSection("s3")}
|
||||||
|
className="rounded-xl border border-[var(--line)] bg-white/3 px-3 py-2 text-xs text-[var(--text)] transition-all hover:border-[rgba(106,166,255,0.55)] hover:bg-[rgba(106,166,255,0.08)] active:translate-y-[1px]"
|
||||||
|
>
|
||||||
|
Explore products
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
</section>
|
</section>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -328,11 +328,15 @@ export const ProductsSection = ({ onJumpToSection, activeAccountId }) => {
|
|||||||
<DataTable
|
<DataTable
|
||||||
columns={["Title", "Price", "Status", "Category", "Action"]}
|
columns={["Title", "Price", "Status", "Category", "Action"]}
|
||||||
data={products}
|
data={products}
|
||||||
|
onRowClick={(row) => handleOpenProduct(row.$id)}
|
||||||
renderCell={(col, row) => {
|
renderCell={(col, row) => {
|
||||||
if (col === "Action") {
|
if (col === "Action") {
|
||||||
return (
|
return (
|
||||||
<button
|
<button
|
||||||
onClick={() => handleOpenProduct(row.$id)}
|
onClick={(e) => {
|
||||||
|
e.stopPropagation();
|
||||||
|
handleOpenProduct(row.$id);
|
||||||
|
}}
|
||||||
className="rounded-xl border border-[var(--line)] bg-white/3 px-3 py-2 text-xs text-[var(--text)] transition-all hover:border-[rgba(106,166,255,0.55)] hover:bg-[rgba(106,166,255,0.08)] active:translate-y-[1px]"
|
className="rounded-xl border border-[var(--line)] bg-white/3 px-3 py-2 text-xs text-[var(--text)] transition-all hover:border-[rgba(106,166,255,0.55)] hover:bg-[rgba(106,166,255,0.08)] active:translate-y-[1px]"
|
||||||
>
|
>
|
||||||
Open
|
Open
|
||||||
@@ -379,34 +383,130 @@ export const ProductsSection = ({ onJumpToSection, activeAccountId }) => {
|
|||||||
{previewLoading ? (
|
{previewLoading ? (
|
||||||
<div className="mb-4 text-xs text-[var(--muted)]">Loading preview...</div>
|
<div className="mb-4 text-xs text-[var(--muted)]">Loading preview...</div>
|
||||||
) : selectedProduct ? (
|
) : selectedProduct ? (
|
||||||
<div className="mb-4 space-y-2 text-xs text-[var(--muted)]">
|
<div className="mb-4 space-y-3 text-xs text-[var(--muted)]">
|
||||||
<div>
|
<div>
|
||||||
<strong>Title:</strong> {selectedProduct.product_title || selectedProduct.$id}
|
<strong className="text-[var(--text)]">Title:</strong>{" "}
|
||||||
|
<span className="text-[var(--text)]">
|
||||||
|
{selectedProduct.product_title || selectedProduct.$id}
|
||||||
|
</span>
|
||||||
</div>
|
</div>
|
||||||
{selectedProduct.product_price && (
|
|
||||||
<div>
|
<div>
|
||||||
<strong>Price:</strong> EUR {selectedProduct.product_price}
|
<strong className="text-[var(--text)]">Price:</strong>{" "}
|
||||||
|
<span className="text-[var(--text)]">
|
||||||
|
{selectedProduct.product_price
|
||||||
|
? `${selectedProduct.product_currency || "EUR"} ${selectedProduct.product_price}`
|
||||||
|
: "N/A"}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<strong className="text-[var(--text)]">Status:</strong>{" "}
|
||||||
|
<span className="text-[var(--text)]">
|
||||||
|
{selectedProduct.product_status || "unknown"}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<strong className="text-[var(--text)]">Category:</strong>{" "}
|
||||||
|
<span className="text-[var(--text)]">
|
||||||
|
{selectedProduct.product_category || "-"}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<strong className="text-[var(--text)]">Condition:</strong>{" "}
|
||||||
|
<span className="text-[var(--text)]">
|
||||||
|
{selectedProduct.product_condition || "-"}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
{selectedProduct.product_url && (
|
||||||
|
<div>
|
||||||
|
<strong className="text-[var(--text)]">URL:</strong>{" "}
|
||||||
|
<a
|
||||||
|
href={selectedProduct.product_url}
|
||||||
|
target="_blank"
|
||||||
|
rel="noopener noreferrer"
|
||||||
|
className="text-blue-500 hover:underline dark:text-blue-400"
|
||||||
|
>
|
||||||
|
{selectedProduct.product_url.length > 50
|
||||||
|
? `${selectedProduct.product_url.substring(0, 50)}...`
|
||||||
|
: selectedProduct.product_url}
|
||||||
|
</a>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
{selectedProduct.product_status && (
|
|
||||||
<div>
|
<div>
|
||||||
<strong>Status:</strong> {selectedProduct.product_status}
|
<strong className="text-[var(--text)]">Platform:</strong>{" "}
|
||||||
|
<span className="text-[var(--text)]">
|
||||||
|
{selectedProduct.product_platform || "-"}
|
||||||
|
</span>
|
||||||
|
{selectedProduct.product_platform_market && (
|
||||||
|
<span className="text-[var(--text)]">
|
||||||
|
{" "}
|
||||||
|
({selectedProduct.product_platform_market})
|
||||||
|
</span>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
{selectedProduct.product_platform_product_id && (
|
||||||
|
<div>
|
||||||
|
<strong className="text-[var(--text)]">Platform Product ID:</strong>{" "}
|
||||||
|
<span className="text-[var(--text)]">
|
||||||
|
{selectedProduct.product_platform_product_id}
|
||||||
|
</span>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
{selectedProduct.product_category && (
|
{(selectedProduct.product_quantity_available != null ||
|
||||||
|
selectedProduct.product_quantity_sold != null) && (
|
||||||
|
<div className="mt-2 border-t border-[var(--line)] pt-2">
|
||||||
|
<strong className="text-[var(--text)]">Quantity:</strong>
|
||||||
|
{selectedProduct.product_quantity_available != null && (
|
||||||
|
<div className="ml-4 text-[var(--text)]">
|
||||||
|
Available: {selectedProduct.product_quantity_available}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
{selectedProduct.product_quantity_sold != null && (
|
||||||
|
<div className="ml-4 text-[var(--text)]">
|
||||||
|
Sold: {selectedProduct.product_quantity_sold}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
{(selectedProduct.product_watch_count != null ||
|
||||||
|
selectedProduct.product_in_carts_count != null) && (
|
||||||
|
<div className="border-t border-[var(--line)] pt-2">
|
||||||
|
<strong className="text-[var(--text)]">Engagement:</strong>
|
||||||
|
{selectedProduct.product_watch_count != null && (
|
||||||
|
<div className="ml-4 text-[var(--text)]">
|
||||||
|
Watches: {selectedProduct.product_watch_count}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
{selectedProduct.product_in_carts_count != null && (
|
||||||
|
<div className="ml-4 text-[var(--text)]">
|
||||||
|
In Carts: {selectedProduct.product_in_carts_count}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
{selectedProduct.product_last_seen_at && (
|
||||||
<div>
|
<div>
|
||||||
<strong>Category:</strong> {selectedProduct.product_category}
|
<strong className="text-[var(--text)]">Last Seen:</strong>{" "}
|
||||||
|
<span className="text-[var(--text)]">
|
||||||
|
{new Date(selectedProduct.product_last_seen_at).toLocaleString("de-DE", {
|
||||||
|
day: "2-digit",
|
||||||
|
month: "2-digit",
|
||||||
|
year: "numeric",
|
||||||
|
hour: "2-digit",
|
||||||
|
minute: "2-digit",
|
||||||
|
})}
|
||||||
|
</span>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
{selectedProduct.details && (
|
{selectedProduct.details && (
|
||||||
<div className="mt-3 border-t border-[var(--line)] pt-2">
|
<div className="mt-3 border-t border-[var(--line)] pt-2">
|
||||||
<strong>Details available</strong>
|
<strong className="text-[var(--text)]">Additional Details:</strong>
|
||||||
|
<div className="mt-1 text-[var(--text)]">Available</div>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
) : (
|
) : (
|
||||||
<div className="mb-4 text-xs text-[var(--muted)]">
|
<div className="mb-4 text-xs text-[var(--muted)]">
|
||||||
Click "Open" on a product to preview details.
|
Click on a product row or "Open" button to preview details.
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
<div className="flex items-center justify-between border-t border-[var(--line)] pt-3">
|
<div className="flex items-center justify-between border-t border-[var(--line)] pt-3">
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
import React from "react";
|
import React from "react";
|
||||||
|
|
||||||
export const DataTable = ({ columns, data, renderCell }) => {
|
export const DataTable = ({ columns, data, renderCell, onRowClick }) => {
|
||||||
return (
|
return (
|
||||||
<div className="overflow-hidden rounded-[18px]">
|
<div className="overflow-hidden rounded-[18px]">
|
||||||
<table className="w-full border-separate border-spacing-0">
|
<table className="w-full border-separate border-spacing-0">
|
||||||
@@ -18,7 +18,11 @@ export const DataTable = ({ columns, data, renderCell }) => {
|
|||||||
</thead>
|
</thead>
|
||||||
<tbody>
|
<tbody>
|
||||||
{data.map((row, rowIdx) => (
|
{data.map((row, rowIdx) => (
|
||||||
<tr key={rowIdx}>
|
<tr
|
||||||
|
key={rowIdx}
|
||||||
|
onClick={() => onRowClick && onRowClick(row, rowIdx)}
|
||||||
|
className={onRowClick ? "cursor-pointer transition-colors hover:bg-white/5" : ""}
|
||||||
|
>
|
||||||
{columns.map((col, colIdx) => (
|
{columns.map((col, colIdx) => (
|
||||||
<td
|
<td
|
||||||
key={colIdx}
|
key={colIdx}
|
||||||
|
|||||||
@@ -1,6 +1,10 @@
|
|||||||
/**
|
/**
|
||||||
* Zentrales Appwrite Client Setup
|
* Zentrales Appwrite Client Setup
|
||||||
* Stellt Client, Account, Databases Instanzen und Helper-Funktionen bereit
|
* Stellt Client, Account, Databases Instanzen und Helper-Funktionen bereit
|
||||||
|
*
|
||||||
|
* CORS: Wenn die App unter einer anderen Origin läuft (z. B. https://www.eship.pro),
|
||||||
|
* muss im Appwrite-Dashboard unter "Settings" → "Platforms" die entsprechende
|
||||||
|
* Origin (z. B. https://www.eship.pro) hinzugefügt werden, sonst blockiert CORS die Requests.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import { Client, Account, Databases } from "appwrite";
|
import { Client, Account, Databases } from "appwrite";
|
||||||
|
|||||||
@@ -15,7 +15,7 @@ import {
|
|||||||
getAccountDisplayName,
|
getAccountDisplayName,
|
||||||
} from "../services/accountService";
|
} from "../services/accountService";
|
||||||
import { fetchManagedAccounts, createManagedAccount, updateManagedAccount, determineRefreshStatus, calculateDataFreshness, getLastSuccessfulAccountMetric } from "../services/accountsService";
|
import { fetchManagedAccounts, createManagedAccount, updateManagedAccount, determineRefreshStatus, calculateDataFreshness, getLastSuccessfulAccountMetric } from "../services/accountsService";
|
||||||
import { upsertAccountMetric, fetchAccountMetricsForMonth } from "../services/accountMetricsService";
|
import { upsertAccountMetric } from "../services/accountMetricsService";
|
||||||
import { getAuthUser, databases, databaseId } from "../lib/appwrite";
|
import { getAuthUser, databases, databaseId } from "../lib/appwrite";
|
||||||
import { parseEbayAccount, parseViaExtensionExtended } from "../services/ebayParserService";
|
import { parseEbayAccount, parseViaExtensionExtended } from "../services/ebayParserService";
|
||||||
import { BentoGrid, BentoGridItem } from "../components/ui/bento-grid";
|
import { BentoGrid, BentoGridItem } from "../components/ui/bento-grid";
|
||||||
@@ -242,174 +242,6 @@ function RangCard({ rank, className }) {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
function RefreshActivityCard({ monthMetrics = new Map(), className }) {
|
|
||||||
// monthMetrics: Map von date (yyyy-mm-dd) -> metric document
|
|
||||||
|
|
||||||
const today = new Date();
|
|
||||||
const currentMonth = today.getMonth();
|
|
||||||
const currentYear = today.getFullYear();
|
|
||||||
const currentDate = today.getDate();
|
|
||||||
|
|
||||||
// Erstelle Kalenderstruktur für aktuellen Monat
|
|
||||||
const getMonthCalendar = () => {
|
|
||||||
const firstDay = new Date(currentYear, currentMonth, 1);
|
|
||||||
const lastDay = new Date(currentYear, currentMonth + 1, 0);
|
|
||||||
const daysInMonth = lastDay.getDate();
|
|
||||||
const startDayOfWeek = firstDay.getDay(); // 0 = Sonntag, 1 = Montag, etc.
|
|
||||||
|
|
||||||
// Wochen beginnen mit Montag (1) statt Sonntag (0)
|
|
||||||
const adjustedStartDay = startDayOfWeek === 0 ? 6 : startDayOfWeek - 1;
|
|
||||||
|
|
||||||
const calendar = [];
|
|
||||||
let currentWeek = [];
|
|
||||||
|
|
||||||
// Leere Felder für Tage vor Monatsbeginn
|
|
||||||
for (let i = 0; i < adjustedStartDay; i++) {
|
|
||||||
currentWeek.push(null);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Tage des Monats
|
|
||||||
for (let day = 1; day <= daysInMonth; day++) {
|
|
||||||
const date = new Date(currentYear, currentMonth, day);
|
|
||||||
currentWeek.push(date);
|
|
||||||
|
|
||||||
if (currentWeek.length === 7) {
|
|
||||||
calendar.push(currentWeek);
|
|
||||||
currentWeek = [];
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Leere Felder für restliche Woche
|
|
||||||
if (currentWeek.length > 0) {
|
|
||||||
while (currentWeek.length < 7) {
|
|
||||||
currentWeek.push(null);
|
|
||||||
}
|
|
||||||
calendar.push(currentWeek);
|
|
||||||
}
|
|
||||||
|
|
||||||
return calendar;
|
|
||||||
};
|
|
||||||
|
|
||||||
const getDayData = (date) => {
|
|
||||||
if (!date) return { refreshStatus: null, salesBucket: null };
|
|
||||||
|
|
||||||
const dateStr = date.toISOString().split('T')[0];
|
|
||||||
const metric = monthMetrics.get(dateStr);
|
|
||||||
|
|
||||||
if (!metric) {
|
|
||||||
return {
|
|
||||||
refreshStatus: 'not-refreshed',
|
|
||||||
salesBucket: null
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
// Bestimme refreshStatus aus metric
|
|
||||||
let refreshStatus = 'not-refreshed';
|
|
||||||
if (metric.account_metrics_refresh_status === 'failed') {
|
|
||||||
refreshStatus = 'failed';
|
|
||||||
} else if (metric.account_metrics_refreshed === true && metric.account_metrics_refresh_status === 'success') {
|
|
||||||
refreshStatus = 'refreshed';
|
|
||||||
}
|
|
||||||
|
|
||||||
return {
|
|
||||||
refreshStatus: refreshStatus,
|
|
||||||
salesBucket: metric.account_metrics_sales_bucket || null
|
|
||||||
};
|
|
||||||
};
|
|
||||||
|
|
||||||
const getStatusColor = (status, isToday) => {
|
|
||||||
if (isToday) {
|
|
||||||
// Aktueller Tag: dezente Umrandung
|
|
||||||
switch (status) {
|
|
||||||
case 'refreshed':
|
|
||||||
return 'bg-green-500/30 border-2 border-green-600 dark:bg-green-500/20 dark:border-green-500';
|
|
||||||
case 'failed':
|
|
||||||
return 'bg-red-500/20 border-2 border-red-600 dark:bg-red-500/10 dark:border-red-500';
|
|
||||||
default:
|
|
||||||
return 'bg-neutral-100 border-2 border-neutral-400 dark:bg-neutral-800 dark:border-neutral-500';
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
switch (status) {
|
|
||||||
case 'refreshed':
|
|
||||||
return 'bg-green-500/30 border border-green-500/50 dark:bg-green-500/20 dark:border-green-500/40';
|
|
||||||
case 'failed':
|
|
||||||
return 'bg-red-500/20 border border-red-500/30 dark:bg-red-500/10 dark:border-red-500/20';
|
|
||||||
default:
|
|
||||||
return 'bg-neutral-100 border border-neutral-200 dark:bg-neutral-800 dark:border-neutral-700';
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
const calendar = getMonthCalendar();
|
|
||||||
const isTodayDate = (date) => {
|
|
||||||
if (!date) return false;
|
|
||||||
return date.getDate() === currentDate &&
|
|
||||||
date.getMonth() === currentMonth &&
|
|
||||||
date.getFullYear() === currentYear;
|
|
||||||
};
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div
|
|
||||||
className={cn(
|
|
||||||
"row-span-1 flex h-full min-h-[12rem] flex-col rounded-xl border border-neutral-200 bg-white p-4 shadow-md transition duration-200 hover:shadow-xl dark:border-white/[0.2] dark:bg-neutral-900 dark:shadow-none md:min-h-[18rem]",
|
|
||||||
className
|
|
||||||
)}
|
|
||||||
>
|
|
||||||
<div className="flex h-full flex-col">
|
|
||||||
{/* Titel oben links */}
|
|
||||||
<div className="mb-2 flex items-baseline gap-2">
|
|
||||||
<div className="text-xs font-medium text-[var(--muted)]">Refresh Activity</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{/* Wochenraster: 7 Spalten x 5 Zeilen */}
|
|
||||||
<div className="grid h-full grid-cols-7 grid-rows-5 gap-0.5 flex-1">
|
|
||||||
{calendar.map((week, weekIndex) => (
|
|
||||||
<React.Fragment key={weekIndex}>
|
|
||||||
{week.map((date, dayIndex) => {
|
|
||||||
if (!date) {
|
|
||||||
return (
|
|
||||||
<div
|
|
||||||
key={`empty-${weekIndex}-${dayIndex}`}
|
|
||||||
className="border border-transparent"
|
|
||||||
/>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
const dayData = getDayData(date);
|
|
||||||
const isToday = isTodayDate(date);
|
|
||||||
const dayNumber = date.getDate();
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div
|
|
||||||
key={`${weekIndex}-${dayIndex}`}
|
|
||||||
className={cn(
|
|
||||||
"relative flex items-center justify-center rounded-sm min-h-[20px]",
|
|
||||||
getStatusColor(dayData.refreshStatus, isToday),
|
|
||||||
"transition-colors"
|
|
||||||
)}
|
|
||||||
title={`${date.toLocaleDateString('de-DE')}: ${dayData.refreshStatus === 'not-refreshed' ? 'Not refreshed' : dayData.refreshStatus === 'refreshed' ? 'Refreshed' : 'Refresh failed'}${dayData.salesBucket ? `, Sales: ${dayData.salesBucket}` : ''}`}
|
|
||||||
>
|
|
||||||
{/* Tag-Nummer (klein, oben links) */}
|
|
||||||
<div className="absolute top-0.5 left-0.5 text-[8px] text-[var(--muted)] leading-none">
|
|
||||||
{dayNumber}
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{/* Sales-Bucket (zentriert) */}
|
|
||||||
{dayData.salesBucket && (
|
|
||||||
<div className="text-[10px] font-semibold text-[var(--text)] leading-none">
|
|
||||||
{dayData.salesBucket}
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
})}
|
|
||||||
</React.Fragment>
|
|
||||||
))}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
function AccountRefreshCard({
|
function AccountRefreshCard({
|
||||||
onRefresh,
|
onRefresh,
|
||||||
@@ -461,7 +293,14 @@ function AccountRefreshCard({
|
|||||||
{/* Bereich B (Zeilen 2-3, Spalten 1-5) */}
|
{/* Bereich B (Zeilen 2-3, Spalten 1-5) */}
|
||||||
<div className="col-span-5 row-span-2 flex items-center justify-center">
|
<div className="col-span-5 row-span-2 flex items-center justify-center">
|
||||||
<button
|
<button
|
||||||
onClick={onRefresh}
|
onClick={() => {
|
||||||
|
// #region agent log
|
||||||
|
fetch('http://127.0.0.1:7243/ingest/2cdae91e-9f0b-48c7-8e02-a970375bdaff',{method:'POST',headers:{'Content-Type':'application/json'},body:JSON.stringify({sessionId:'debug-session',runId:'run1',hypothesisId:'H1',location:'AccountsPage.jsx:297',message:'AccountRefreshCard click',data:{isRefreshing,hasOnRefresh:!!onRefresh},timestamp:Date.now()})}).catch(()=>{});
|
||||||
|
// #endregion
|
||||||
|
if (onRefresh) {
|
||||||
|
onRefresh();
|
||||||
|
}
|
||||||
|
}}
|
||||||
disabled={isRefreshing}
|
disabled={isRefreshing}
|
||||||
className={cn(
|
className={cn(
|
||||||
"w-full h-full rounded-xl border transition-all active:translate-y-[1px] disabled:opacity-50 disabled:cursor-not-allowed",
|
"w-full h-full rounded-xl border transition-all active:translate-y-[1px] disabled:opacity-50 disabled:cursor-not-allowed",
|
||||||
@@ -631,8 +470,6 @@ export const AccountsPage = () => {
|
|||||||
// Nur ein Account wird angezeigt; Wechsel über Dropdown
|
// Nur ein Account wird angezeigt; Wechsel über Dropdown
|
||||||
const [displayedAccountId, setDisplayedAccountId] = useState(null);
|
const [displayedAccountId, setDisplayedAccountId] = useState(null);
|
||||||
|
|
||||||
// Monats-Metriken für Kalender (Map: date -> metric)
|
|
||||||
const [monthMetrics, setMonthMetrics] = useState(new Map());
|
|
||||||
|
|
||||||
// Form-Felder (nur noch URL)
|
// Form-Felder (nur noch URL)
|
||||||
const [formData, setFormData] = useState({
|
const [formData, setFormData] = useState({
|
||||||
@@ -659,14 +496,6 @@ export const AccountsPage = () => {
|
|||||||
}
|
}
|
||||||
}, [accounts]);
|
}, [accounts]);
|
||||||
|
|
||||||
// Lade Monats-Metriken wenn displayedAccountId sich ändert
|
|
||||||
useEffect(() => {
|
|
||||||
if (displayedAccountId) {
|
|
||||||
loadMonthMetrics(displayedAccountId);
|
|
||||||
} else {
|
|
||||||
setMonthMetrics(new Map());
|
|
||||||
}
|
|
||||||
}, [displayedAccountId]);
|
|
||||||
|
|
||||||
async function loadAccounts() {
|
async function loadAccounts() {
|
||||||
setLoading(true);
|
setLoading(true);
|
||||||
@@ -687,24 +516,6 @@ export const AccountsPage = () => {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
async function loadMonthMetrics(accountId) {
|
|
||||||
if (!accountId) {
|
|
||||||
setMonthMetrics(new Map());
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
try {
|
|
||||||
const today = new Date();
|
|
||||||
const year = today.getFullYear();
|
|
||||||
const month = today.getMonth() + 1; // 1-12
|
|
||||||
|
|
||||||
const metrics = await fetchAccountMetricsForMonth(accountId, year, month);
|
|
||||||
setMonthMetrics(metrics);
|
|
||||||
} catch (e) {
|
|
||||||
console.error("Fehler beim Laden der Monats-Metriken:", e);
|
|
||||||
setMonthMetrics(new Map());
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
const handleDisplayedAccountChange = (accountId) => {
|
const handleDisplayedAccountChange = (accountId) => {
|
||||||
setDisplayedAccountId(accountId);
|
setDisplayedAccountId(accountId);
|
||||||
@@ -1087,10 +898,6 @@ export const AccountsPage = () => {
|
|||||||
streak={account.account_refresh_streak || null}
|
streak={account.account_refresh_streak || null}
|
||||||
dataFreshness={calculateDataFreshness(account.account_last_refresh_at) || 'Aging'}
|
dataFreshness={calculateDataFreshness(account.account_last_refresh_at) || 'Aging'}
|
||||||
/>
|
/>
|
||||||
<RefreshActivityCard
|
|
||||||
monthMetrics={monthMetrics}
|
|
||||||
className="md:col-span-3"
|
|
||||||
/>
|
|
||||||
</BentoGrid>
|
</BentoGrid>
|
||||||
);
|
);
|
||||||
})()
|
})()
|
||||||
|
|||||||
BIN
bilder/Bildschirmfoto 2026-01-26 um 17.29.55.png
Normal file
BIN
bilder/Bildschirmfoto 2026-01-26 um 17.29.55.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 2.5 MiB |
BIN
bilder/Bildschirmfoto 2026-01-26 um 17.30.20.png
Normal file
BIN
bilder/Bildschirmfoto 2026-01-26 um 17.30.20.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 1.5 MiB |
BIN
bilder/Bildschirmfoto 2026-01-26 um 17.31.18.png
Normal file
BIN
bilder/Bildschirmfoto 2026-01-26 um 17.31.18.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 3.4 MiB |
Reference in New Issue
Block a user