Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
53 changes: 33 additions & 20 deletions app/blocks/__global/app-sidebar.tsx
Original file line number Diff line number Diff line change
@@ -1,13 +1,14 @@
import { Link, NavLink, useSubmit } from "react-router";
import {
IconDashboard,
IconBox,
IconChartLine,
IconReceipt,
IconWallet,
IconBrain,
import {
IconDashboard,
IconBox,
IconChartLine,
IconReceipt,
IconWallet,
IconBrain,
IconSettings,
IconLogout
IconLogout,
IconTrendingUp,
} from "@tabler/icons-react";
import styles from "./app-sidebar.module.css";

Expand All @@ -33,9 +34,9 @@ export function AppSidebar({ user }: Props) {
{ to: "/app/sales", label: "Sales Log", icon: <IconReceipt size={20} /> },
{ to: "/app/expenses", label: "Expenses", icon: <IconWallet size={20} /> },
{ to: "/app/ai-insights", label: "AI Insights", icon: <IconBrain size={20} /> },
{ to: "/app/analytics", label: "Analytics & Insights", icon: <IconTrendingUp size={20} /> },
{ to: "/app/settings", label: "Settings", icon: <IconSettings size={20} /> },
];

return (
<aside className={styles.sidebar}>
<div className={styles.header}>
Expand All @@ -49,7 +50,7 @@ export function AppSidebar({ user }: Props) {
<NavLink
key={link.to}
to={link.to}
className={({ isActive }) =>
className={({ isActive }) =>
[styles.navItem, isActive ? styles.navItemActive : ""].filter(Boolean).join(" ")
}
>
Expand All @@ -69,19 +70,31 @@ export function AppSidebar({ user }: Props) {
<span className={styles.userPlan}>{user.plan || "FREE PLAN"}</span>
</div>
</div>
<div style={{ display: 'flex', gap: '8px' }}>
<button
<div style={{ display: "flex", gap: "8px" }}>
<button
onClick={() => {
const current = document.documentElement.getAttribute('data-theme') || 'dark';
const next = current === 'dark' ? 'light' : 'dark';
document.documentElement.setAttribute('data-theme', next);
localStorage.setItem('fliptrack-theme', next);
}}
className={styles.logoutBtn}
style={{ flex: 1, justifyContent: 'center' }}
const current = document.documentElement.getAttribute("data-theme") || "dark";
const next = current === "dark" ? "light" : "dark";
document.documentElement.setAttribute("data-theme", next);
localStorage.setItem("fliptrack-theme", next);
}}
className={styles.logoutBtn}
style={{ flex: 1, justifyContent: "center" }}
title="Toggle Theme"
>
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round"><path d="M12 3a6 6 0 0 0 9 9 9 9 0 1 1-9-9Z"/></svg>
<svg
xmlns="http://www.w3.org/2000/svg"
width="16"
height="16"
viewBox="0 0 24 24"
fill="none"
stroke="currentColor"
strokeWidth="2"
strokeLinecap="round"
strokeLinejoin="round"
>
<path d="M12 3a6 6 0 0 0 9 9 9 9 0 1 1-9-9Z" />
</svg>
</button>
<button onClick={handleLogout} className={styles.logoutBtn} style={{ flex: 3 }}>
<IconLogout size={16} />
Expand Down
65 changes: 65 additions & 0 deletions app/blocks/analytics/ProfitForecast.module.css
Original file line number Diff line number Diff line change
@@ -0,0 +1,65 @@
.card {
background: var(--color-bg-card);
border: 1px solid var(--color-border);
border-radius: var(--radius-md);
padding: var(--space-4);
}

.title {
font-size: 1.1rem;
margin-bottom: var(--space-4);
color: var(--color-text);
}

.row {
display: grid;
grid-template-columns: repeat(3, 1fr);
gap: var(--space-4);
}

.statCard {
background: var(--color-bg-elevated);
border: 1px solid var(--color-border);
border-radius: var(--radius-sm);
padding: var(--space-4);
}

.label {
font-size: 11px;
color: var(--color-text-subtle);
text-transform: uppercase;
letter-spacing: 0.06em;
margin-bottom: var(--space-2);
}

.value {
font-family: var(--family-mono);
font-size: 1.3rem;
font-weight: 700;
color: var(--color-text);
}

.sub {
font-size: 12px;
color: var(--color-text-subtle);
margin-top: var(--space-1);
}

.positive {
color: var(--color-success);
}

.disclaimer {
margin-top: var(--space-4);
font-size: 12px;
color: var(--color-warning);
background: var(--color-warning-bg);
border-radius: var(--radius-sm);
padding: var(--space-2) var(--space-3);
}

@media (max-width: 768px) {
.row {
grid-template-columns: 1fr;
}
}
53 changes: 53 additions & 0 deletions app/blocks/analytics/ProfitForecast.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,53 @@
import styles from "./ProfitForecast.module.css";

interface Props {
className?: string;
forecast?: {
totalInventoryCost: number;
totalEstimatedMarketValue: number;
projectedProfit: number;
projectedMarginPct: number;
itemsMissingMarketData: number;
};
}

export function ProfitForecast({ className, forecast }: Props) {
if (!forecast) return null;
const isProfit = forecast.projectedProfit >= 0;

const cards = [
{ label: "Inventory Cost", value: `$${forecast.totalInventoryCost.toFixed(2)}`, sub: "All in-stock items" },
{
label: "Est. Market Value",
value: `$${forecast.totalEstimatedMarketValue.toFixed(2)}`,
sub: "Based on latest price data",
},
{
label: "Projected Profit",
value: `${isProfit ? "+" : ""}$${forecast.projectedProfit.toFixed(2)} (${forecast.projectedMarginPct.toFixed(1)}%)`,
sub: "If sold at current market value",
positive: isProfit,
},
];

return (
<div className={[styles.card, className].filter(Boolean).join(" ")}>
<h2 className={styles.title}>Profit Forecast</h2>
<div className={styles.row}>
{cards.map((c) => (
<div key={c.label} className={styles.statCard}>
<div className={styles.label}>{c.label}</div>
<div className={[styles.value, c.positive ? styles.positive : ""].join(" ")}>{c.value}</div>
<div className={styles.sub}>{c.sub}</div>
</div>
))}
</div>
{forecast.itemsMissingMarketData > 0 && (
<div className={styles.disclaimer}>
{forecast.itemsMissingMarketData} item(s) had no scraped market price yet — purchase or asking price was used
as a placeholder.
</div>
)}
</div>
);
}
109 changes: 109 additions & 0 deletions app/blocks/analytics/StaleInventoryCard.module.css
Original file line number Diff line number Diff line change
@@ -0,0 +1,109 @@
.card {
background: var(--color-bg-card);
border: 1px solid var(--color-border);
border-radius: var(--radius-md);
padding: var(--space-4);
}

.headerRow {
display: flex;
align-items: center;
justify-content: space-between;
margin-bottom: var(--space-1);
}

.title {
font-size: 1.1rem;
color: var(--color-text);
}

.badge {
font-size: 11px;
font-family: var(--family-mono);
color: var(--color-warning);
background: var(--color-warning-bg);
border-radius: var(--radius-full);
padding: var(--space-1) var(--space-3);
}

.sub {
font-size: 12px;
color: var(--color-text-subtle);
margin-bottom: var(--space-4);
}

.empty {
font-size: 13px;
color: var(--color-text-muted);
padding: var(--space-6) 0;
text-align: center;
}

.list {
display: flex;
flex-direction: column;
gap: var(--space-3);
}

.row {
display: grid;
grid-template-columns: 1fr 120px 80px;
align-items: center;
gap: var(--space-3);
padding: var(--space-2) 0;
border-bottom: 1px solid var(--color-border);
}

.row:last-child {
border-bottom: none;
}

.rowInfo {
display: flex;
flex-direction: column;
gap: 2px;
min-width: 0;
}

.itemName {
font-size: 13px;
color: var(--color-text);
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
}

.itemMeta {
font-size: 11px;
color: var(--color-text-subtle);
}

.progressTrack {
height: 6px;
background: var(--color-bg-elevated);
border-radius: var(--radius-full);
overflow: hidden;
}

.progressFill {
height: 100%;
background: var(--color-warning);
border-radius: var(--radius-full);
}

.itemValue {
font-family: var(--family-mono);
font-size: 13px;
text-align: right;
color: var(--color-text);
}

@media (max-width: 768px) {
.row {
grid-template-columns: 1fr;
gap: var(--space-2);
}
.itemValue {
text-align: left;
}
}
47 changes: 47 additions & 0 deletions app/blocks/analytics/StaleInventoryCard.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
import styles from "./StaleInventoryCard.module.css";

interface Props {
className?: string;
items?: any[];
}

export function StaleInventoryCard({ className, items = [] }: Props) {
const totalTiedUpCapital = items.reduce((acc, i) => acc + Number(i.purchasePrice), 0);
const maxDays = Math.max(...items.map((i) => i.daysInStock), 1);

return (
<div className={[styles.card, className].filter(Boolean).join(" ")}>
<div className={styles.headerRow}>
<h2 className={styles.title}>Stale Inventory</h2>
<span className={styles.badge}>{items.length} items &bull; 60+ days</span>
</div>
<div className={styles.sub}>${totalTiedUpCapital.toFixed(2)} in capital tied up</div>

{items.length === 0 ? (
<div className={styles.empty}>Nothing stale — everything is moving.</div>
) : (
<div className={styles.list}>
{items.slice(0, 8).map((item) => (
<div key={item.id} className={styles.row}>
<div className={styles.rowInfo}>
<span className={styles.itemName}>
{item.name} {item.size ? `(${item.size})` : ""}
</span>
<span className={styles.itemMeta}>
{item.sku} &bull; {item.daysInStock} days in stock
</span>
</div>
<div className={styles.progressTrack}>
<div
className={styles.progressFill}
style={{ width: `${Math.min((item.daysInStock / maxDays) * 100, 100)}%` }}
/>
</div>
<span className={styles.itemValue}>${Number(item.purchasePrice).toFixed(2)}</span>
</div>
))}
</div>
)}
</div>
);
}
9 changes: 5 additions & 4 deletions app/routes.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,14 +12,15 @@ export default [
route("/changelog", "routes/changelog-page.tsx"),
route("/privacy", "routes/privacy-policy.tsx"),
route("/terms", "routes/terms-of-service.tsx"),

...prefix("/app", [
layout("routes/app-layout.tsx", [
route("dashboard", "routes/dashboard.tsx"),
route("inventory", "routes/inventory-management.tsx"),
route("inventory/:id", "routes/inventory-item-detail.tsx"),
route("market-prices", "routes/market-prices.tsx"),
route("sales", "routes/sales-log.tsx"),
route("analytics", "routes/analytics.tsx"),
route("expenses", "routes/expenses-tracker.tsx"),
route("income-statement", "routes/income-statement.tsx"),
route("alerts", "routes/price-alerts.tsx"),
Expand All @@ -43,11 +44,11 @@ export default [
route("/api/ai/ocr", "routes/api.ai.ocr.ts"),
route("/api/insights", "routes/api.insights.ts"),
route("/api/export/tax", "routes/api.export.tax.ts"),

// 🌟 Kept from main branch merge (Placed safely above dynamic route)
route("/api/inventory/search", "routes/api.inventory.search.ts"),
route("/api/integrations", "routes/api.integrations.ts"),

// 🌟 Your dynamic showroom route (Must stay at the very end as a catch-all)
route("/:username", "routes/$username.tsx"),
] satisfies RouteConfig;
] satisfies RouteConfig;
Loading
Loading