feat: billing period for subscriptions, costs in redundancy analysis

This commit is contained in:
root
2026-07-29 23:46:58 +02:00
parent deabe36727
commit 44cc29b8fe
6 changed files with 109 additions and 14 deletions
+40 -7
View File
@@ -4,7 +4,7 @@ import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card";
import { Badge } from "@/components/ui/badge";
import { Skeleton } from "@/components/ui/skeleton";
import { Progress } from "@/components/ui/progress";
import { Star, AlertTriangle } from "lucide-react";
import { Star, AlertTriangle, DollarSign } from "lucide-react";
import { Link } from "wouter";
import { customFetch } from "@workspace/api-client-react";
@@ -38,16 +38,24 @@ export default function RedundancyPage() {
<div className="space-y-8">
{data.map((group) => (
<div key={group.category}>
<h2 className="text-xl font-semibold mb-4 capitalize">{group.category}</h2>
<div className="flex items-center justify-between mb-4">
<h2 className="text-xl font-semibold capitalize">{group.category}</h2>
{group.totalMonthlyCost > 0 && (
<span className="text-xs text-muted-foreground flex items-center gap-1">
<DollarSign className="w-3 h-3" />
{group.totalMonthlyCost.toFixed(2)}/mo total
</span>
)}
</div>
<div className="grid grid-cols-1 lg:grid-cols-2 gap-4 mb-4">
{group.tools.map((tool: any) => (
<Link key={tool.id} href={`/tools/${tool.id}`}>
<Card className="hover-elevate transition-all cursor-pointer hover:border-primary/50">
<CardContent className="p-4">
<div className="flex justify-between items-start">
<div>
<div className="flex items-start gap-2 justify-between">
<div className="min-w-0">
<span className="font-medium">{tool.name}</span>
<div className="flex items-center gap-2 mt-1 text-xs text-muted-foreground">
<div className="flex items-center gap-2 mt-1 text-xs text-muted-foreground flex-wrap">
<span>{tool.ratingCount} reviews</span>
{tool.avgCombined != null && (
<>
@@ -58,9 +66,27 @@ export default function RedundancyPage() {
</span>
</>
)}
{tool.totalMonthly > 0 && (
<>
<span>·</span>
<span className="flex items-center gap-0.5 text-amber-600">
<DollarSign className="w-3 h-3" />
{tool.totalMonthly.toFixed(2)}/mo
</span>
</>
)}
</div>
{tool.costs?.length > 0 && (
<div className="flex flex-wrap gap-1 mt-2">
{tool.costs.map((c: any, i: number) => (
<Badge key={i} variant="outline" className="text-[10px] px-1 py-0">
{c.licenseType}{c.billingPeriod ? ` (${c.billingPeriod})` : ""}
</Badge>
))}
</div>
)}
</div>
<Badge variant="outline">{tool.features.length} features</Badge>
<Badge variant="outline" className="shrink-0">{tool.features.length} features</Badge>
</div>
</CardContent>
</Card>
@@ -79,7 +105,7 @@ export default function RedundancyPage() {
<span className="text-muted-foreground text-xs shrink-0">vs</span>
<span className="font-medium text-sm truncate">{pair.b.name}</span>
</div>
<div className="flex items-center gap-4 shrink-0">
<div className="flex items-center gap-3 shrink-0">
<div className="flex items-center gap-2">
<Progress value={pair.overlap} className="w-16 h-2" />
<span className="text-xs text-muted-foreground w-8">{pair.overlap}%</span>
@@ -89,6 +115,13 @@ export default function RedundancyPage() {
{pair.scoreDiff > 0 ? `${pair.b.name} +${pair.scoreDiff.toFixed(1)}` : `${pair.a.name} +${Math.abs(pair.scoreDiff).toFixed(1)}`}
</Badge>
)}
{pair.costDiff !== 0 && (
<span className="text-[10px] text-muted-foreground">
{pair.costDiff > 0
? `${pair.b.name} +${pair.costDiff.toFixed(2)}/mo`
: `${pair.a.name} +${Math.abs(pair.costDiff).toFixed(2)}/mo`}
</span>
)}
</div>
</CardContent>
</Card>
+21 -1
View File
@@ -84,6 +84,7 @@ export default function ToolDetail() {
const [costDialogOpen, setCostDialogOpen] = useState(false);
const [editCost, setEditCost] = useState<any | null>(null);
const [costLicenseType, setCostLicenseType] = useState("subscription");
const [costBillingPeriod, setCostBillingPeriod] = useState("monthly");
const [costAmount, setCostAmount] = useState("");
const [costCurrency, setCostCurrency] = useState("EUR");
const [costRenewal, setCostRenewal] = useState("");
@@ -140,6 +141,7 @@ export default function ToolDetail() {
const method = editCost ? "PATCH" : "POST";
const body: Record<string, unknown> = {
licenseType: costLicenseType,
billingPeriod: costLicenseType === "subscription" ? costBillingPeriod : null,
currency: costCurrency,
notes: costNotes || null,
};
@@ -159,6 +161,7 @@ export default function ToolDetail() {
function resetCostForm() {
setEditCost(null);
setCostLicenseType("subscription");
setCostBillingPeriod("monthly");
setCostAmount("");
setCostCurrency("EUR");
setCostRenewal("");
@@ -168,6 +171,7 @@ export default function ToolDetail() {
function openEditCost(c: any) {
setEditCost(c);
setCostLicenseType(c.licenseType);
setCostBillingPeriod(c.billingPeriod ?? "monthly");
setCostAmount(c.cost ?? "");
setCostCurrency(c.currency ?? "EUR");
setCostRenewal(c.renewalDate ?? "");
@@ -550,7 +554,10 @@ export default function ToolDetail() {
<CardContent className="p-4">
<div className="flex justify-between items-start">
<div>
<Badge variant="outline" className="text-xs mb-1">{c.licenseType}</Badge>
<div className="flex items-center gap-1.5 mb-1">
<Badge variant="outline" className="text-xs">{c.licenseType}</Badge>
{c.billingPeriod && <span className="text-[10px] text-muted-foreground uppercase">{c.billingPeriod}</span>}
</div>
<div className="text-lg font-bold">
{c.cost != null ? `${c.cost} ${c.currency ?? ""}` : "Free"}
</div>
@@ -602,6 +609,19 @@ export default function ToolDetail() {
</SelectContent>
</Select>
</div>
{costLicenseType === "subscription" && (
<div className="space-y-2">
<label className="text-sm font-medium">Billing Period</label>
<Select value={costBillingPeriod} onValueChange={setCostBillingPeriod}>
<SelectTrigger><SelectValue /></SelectTrigger>
<SelectContent>
<SelectItem value="monthly">Monthly</SelectItem>
<SelectItem value="quarterly">Quarterly</SelectItem>
<SelectItem value="yearly">Yearly</SelectItem>
</SelectContent>
</Select>
</div>
)}
<div className="grid grid-cols-2 gap-4">
<div className="space-y-2">
<label className="text-sm font-medium">Cost</label>