fix: resolve all remaining type safety errors

- Create types.ts with proper TypeScript interfaces for dashboard data
- Replace all 'any' types in dashboard/page.tsx with DashboardStats and RecentInvoice
- Fix type safety in invoice-form.tsx:
  - Replace 'any' in updateItem with proper union type
  - Add generic type parameter to updateField for type safety
  - Fix status type assertion (any -> proper union type)
  - Replace || with ?? for safer null handling
- All TypeScript compilation errors resolved
- Lint down to 1 warning (false positive for 'loading' variable)
This commit is contained in:
2025-12-11 20:15:29 -05:00
parent cf4ef928b8
commit 75c4362d97
3 changed files with 29 additions and 13 deletions

View File

@@ -24,12 +24,13 @@ import { RevenueChart } from "~/app/dashboard/_components/revenue-chart";
import { InvoiceStatusChart } from "~/app/dashboard/_components/invoice-status-chart";
import { MonthlyMetricsChart } from "~/app/dashboard/_components/monthly-metrics-chart";
import { AnimatedStatsCard } from "~/app/dashboard/_components/animated-stats-card";
import type { DashboardStats, RecentInvoice } from "./types";
// Hero section with clean mono design
// Enhanced stats cards with better visuals
function DashboardStats({ stats }: { stats: any }) { // TODO: Import RouterOutput type
function DashboardStats({ stats }: { stats: DashboardStats }) { // TODO: Import RouterOutput type
const formatTrend = (value: number, isCount = false) => {
if (isCount) {
return value > 0 ? `+${value}` : value.toString();
@@ -101,7 +102,7 @@ function DashboardStats({ stats }: { stats: any }) { // TODO: Import RouterOutpu
}
// Charts section
async function ChartsSection({ stats }: { stats: any }) {
async function ChartsSection({ stats }: { stats: DashboardStats }) {
// We still fetch all invoices for the status chart for now, or we could aggregate that too.
// For now, let's keep status chart as is (fetching all) but use aggregated for revenue.
// Actually, let's fetch invoices here for the status chart to keep it working.
@@ -309,7 +310,7 @@ async function CurrentWork() {
}
// Enhanced recent activity
async function RecentActivity({ recentInvoices }: { recentInvoices: any[] }) {
async function RecentActivity({ recentInvoices }: { recentInvoices: RecentInvoice[] }) {
// Use passed recentInvoices instead of fetching all
const getStatusStyle = (status: string) => {

View File

@@ -0,0 +1,13 @@
import { type RouterOutputs } from "~/trpc/react";
// Dashboard stats type from the dashboard router
export type DashboardStats = RouterOutputs["dashboard"]["getStats"];
// Individual invoice type from the invoices router
export type Invoice = RouterOutputs["invoices"]["getAll"][number];
// Recent invoice type (includes client relation)
export type RecentInvoice = DashboardStats["recentInvoices"][number];
// Revenue chart data point
export type RevenueChartDataPoint = DashboardStats["revenueChartData"][number];

View File

@@ -111,7 +111,7 @@ export default function InvoiceForm({ invoiceId }: InvoiceFormProps) {
clientId: existingInvoice.clientId,
issueDate: new Date(existingInvoice.issueDate),
dueDate: new Date(existingInvoice.dueDate),
status: existingInvoice.status as any,
status: existingInvoice.status as "draft" | "sent" | "paid",
notes: existingInvoice.notes ?? "",
taxRate: existingInvoice.taxRate,
defaultHourlyRate: null,
@@ -119,7 +119,7 @@ export default function InvoiceForm({ invoiceId }: InvoiceFormProps) {
});
setInitialized(true);
} else if ((!invoiceId || invoiceId === "new") && businesses && !initialized) {
const defaultBusiness = businesses.find((b) => b.isDefault) || businesses[0];
const defaultBusiness = businesses.find((b) => b.isDefault) ?? businesses[0];
if (defaultBusiness) setFormData((prev) => ({ ...prev, businessId: defaultBusiness.id }));
setInitialized(true);
}
@@ -140,14 +140,16 @@ export default function InvoiceForm({ invoiceId }: InvoiceFormProps) {
}));
};
const removeItem = (idx: number) => { if (formData.items.length > 1) setFormData((prev) => ({ ...prev, items: prev.items.filter((_, i) => i !== idx) })); };
const updateItem = (idx: number, field: string, value: any) => {
const updateItem = (idx: number, field: string, value: string | number | Date) => {
setFormData((prev) => ({
...prev,
items: prev.items.map((item, i) => {
if (i === idx) {
const u: any = { ...item, [field]: value };
if (field === "hours" || field === "rate") u.amount = u.hours * u.rate;
return u;
const updated = { ...item, [field]: value };
if (field === "hours" || field === "rate") {
updated.amount = updated.hours * updated.rate;
}
return updated;
}
return item;
})
@@ -238,7 +240,7 @@ export default function InvoiceForm({ invoiceId }: InvoiceFormProps) {
} finally { setLoading(false); }
};
const updateField = (field: keyof InvoiceFormData, value: any) => setFormData(p => ({ ...p, [field]: value }));
const updateField = <K extends keyof InvoiceFormData>(field: K, value: InvoiceFormData[K]) => setFormData(p => ({ ...p, [field]: value }));
const handleDelete = () => setDeleteDialogOpen(true);
const confirmDelete = () => { if (invoiceId) deleteInvoice.mutate({ id: invoiceId }); };
@@ -277,7 +279,7 @@ export default function InvoiceForm({ invoiceId }: InvoiceFormProps) {
// Explicitly prioritize client rate, then business rate, then 0
const clientRate = selectedClient && 'defaultHourlyRate' in selectedClient ? selectedClient.defaultHourlyRate : null;
const businessRate = currentBusiness && 'defaultHourlyRate' in currentBusiness ? currentBusiness.defaultHourlyRate : null;
const rateToSet = clientRate ?? businessRate ?? 0;
const rateToSet: number = (clientRate ?? businessRate ?? 0) as number;
updateField("defaultHourlyRate", rateToSet);
}}
@@ -294,8 +296,8 @@ export default function InvoiceForm({ invoiceId }: InvoiceFormProps) {
<CardHeader><CardTitle className="flex gap-2 text-base"><Tag className="w-4 h-4" /> Invoice Config</CardTitle></CardHeader>
<CardContent className="space-y-4">
<div className="grid grid-cols-2 gap-4">
<div className="space-y-2"><Label>Issue Date</Label><DatePicker date={formData.issueDate} onDateChange={(d) => updateField("issueDate", d || new Date())} className="w-full" /></div>
<div className="space-y-2"><Label>Due Date</Label><DatePicker date={formData.dueDate} onDateChange={(d) => updateField("dueDate", d || new Date())} className="w-full" /></div>
<div className="space-y-2"><Label>Issue Date</Label><DatePicker date={formData.issueDate} onDateChange={(d) => updateField("issueDate", d ?? new Date())} className="w-full" /></div>
<div className="space-y-2"><Label>Due Date</Label><DatePicker date={formData.dueDate} onDateChange={(d) => updateField("dueDate", d ?? new Date())} className="w-full" /></div>
</div>
<div className="grid grid-cols-2 gap-4">
<div className="space-y-2"><Label>Tax Rate</Label><NumberInput value={formData.taxRate} onChange={(v) => updateField("taxRate", v)} suffix="%" className="w-full" /></div>