Refactor Index page

Refactor `src/pages/Index.tsx` into smaller components.
This commit is contained in:
gpt-engineer-app[bot]
2025-07-09 19:17:46 +00:00
committed by PhatPhuckDave
parent 81e9a98315
commit 5af53723cc
4 changed files with 317 additions and 167 deletions

92
src/hooks/useDashboard.ts Normal file
View File

@@ -0,0 +1,92 @@
import { useState, useEffect, useRef } from 'react';
import { IndJob } from '@/lib/types';
import { IndTransactionRecordNoId, IndJobRecordNoId } from '@/lib/pbtypes';
import { useJobs } from '@/hooks/useDataService';
export function useDashboard() {
const {
jobs,
loading,
error,
loadingStatuses,
createJob,
updateJob,
deleteJob,
createMultipleTransactions,
createMultipleBillItems,
loadJobsForStatuses
} = useJobs();
const [showJobForm, setShowJobForm] = useState(false);
const [editingJob, setEditingJob] = useState<IndJob | null>(null);
const [showBatchForm, setShowBatchForm] = useState(false);
const [showBatchExpenditureForm, setShowBatchExpenditureForm] = useState(false);
const [searchOpen, setSearchOpen] = useState(false);
const [searchQuery, setSearchQuery] = useState('');
const [totalRevenueChartOpen, setTotalRevenueChartOpen] = useState(false);
const [totalProfitChartOpen, setTotalProfitChartOpen] = useState(false);
const [collapsedGroups, setCollapsedGroups] = useState<Record<string, boolean>>(() => {
const saved = localStorage.getItem('jobGroupsCollapsed');
return saved ? JSON.parse(saved) : {};
});
const scrollPositionRef = useRef<number>(0);
const containerRef = useRef<HTMLDivElement>(null);
useEffect(() => {
const handleKeyDown = (e: KeyboardEvent) => {
if ((e.ctrlKey || e.metaKey) && e.key === 'f') {
e.preventDefault();
setSearchOpen(true);
}
};
window.addEventListener('keydown', handleKeyDown);
return () => window.removeEventListener('keydown', handleKeyDown);
}, []);
useEffect(() => {
const handleScroll = () => {
scrollPositionRef.current = window.scrollY;
};
window.addEventListener('scroll', handleScroll);
return () => window.removeEventListener('scroll', handleScroll);
}, []);
return {
// State
jobs,
loading,
error,
loadingStatuses,
showJobForm,
setShowJobForm,
editingJob,
setEditingJob,
showBatchForm,
setShowBatchForm,
showBatchExpenditureForm,
setShowBatchExpenditureForm,
searchOpen,
setSearchOpen,
searchQuery,
setSearchQuery,
totalRevenueChartOpen,
setTotalRevenueChartOpen,
totalProfitChartOpen,
setTotalProfitChartOpen,
collapsedGroups,
setCollapsedGroups,
scrollPositionRef,
containerRef,
// Methods
createJob,
updateJob,
deleteJob,
createMultipleTransactions,
createMultipleBillItems,
loadJobsForStatuses
};
}

View File

@@ -0,0 +1,147 @@
import { IndJob } from '@/lib/types';
import { IndTransactionRecordNoId, IndJobRecordNoId } from '@/lib/pbtypes';
interface DashboardHandlersProps {
createJob: (jobData: IndJobRecordNoId) => Promise<IndJob>;
updateJob: (id: string, updates: Partial<IndJobRecordNoId>) => Promise<IndJob>;
deleteJob: (id: string) => Promise<void>;
createMultipleTransactions: (jobId: string, transactions: IndTransactionRecordNoId[], type: 'expenditure' | 'income') => Promise<IndJob>;
createMultipleBillItems: (jobId: string, items: { name: string; quantity: number; unitPrice: number }[], type: 'billOfMaterials' | 'consumedMaterials') => Promise<IndJob>;
loadJobsForStatuses: (statuses: string[]) => Promise<void>;
setShowJobForm: (show: boolean) => void;
setEditingJob: (job: IndJob | null) => void;
collapsedGroups: Record<string, boolean>;
setCollapsedGroups: (groups: Record<string, boolean>) => void;
loadingStatuses: Set<string>;
}
export function useDashboardHandlers({
createJob,
updateJob,
deleteJob,
createMultipleTransactions,
createMultipleBillItems,
loadJobsForStatuses,
setShowJobForm,
setEditingJob,
collapsedGroups,
setCollapsedGroups,
loadingStatuses
}: DashboardHandlersProps) {
const handleCreateJob = async (jobData: IndJobRecordNoId, billOfMaterials?: { name: string; quantity: number }[]) => {
try {
const newJob = await createJob(jobData);
if (billOfMaterials && billOfMaterials.length > 0) {
const billItems = billOfMaterials.map(item => ({
name: item.name,
quantity: item.quantity,
unitPrice: 0
}));
await createMultipleBillItems(newJob.id, billItems, 'billOfMaterials');
}
setShowJobForm(false);
} catch (error) {
console.error('Error creating job:', error);
}
};
const handleEditJob = (job: IndJob) => {
setEditingJob(job);
setShowJobForm(true);
};
const handleUpdateJob = async (jobData: IndJobRecordNoId, editingJob: IndJob | null) => {
if (!editingJob) return;
try {
await updateJob(editingJob.id, jobData);
setShowJobForm(false);
setEditingJob(null);
} catch (error) {
console.error('Error updating job:', error);
}
};
const handleDeleteJob = async (jobId: string) => {
if (confirm('Are you sure you want to delete this job?')) {
try {
await deleteJob(jobId);
} catch (error) {
console.error('Error deleting job:', error);
}
}
};
const handleUpdateProduced = async (jobId: string, produced: number) => {
try {
await updateJob(jobId, { produced });
} catch (error) {
console.error('Error updating produced quantity:', error);
}
};
const handleImportBOM = async (jobId: string, items: { name: string; quantity: number }[]) => {
try {
const billItems = items.map(item => ({
name: item.name,
quantity: item.quantity,
unitPrice: 0
}));
await createMultipleBillItems(jobId, billItems, 'billOfMaterials');
} catch (error) {
console.error('Error importing BOM:', error);
}
};
const toggleGroup = async (status: string) => {
const currentScrollY = window.scrollY;
const newState = { ...collapsedGroups, [status]: !collapsedGroups[status] };
setCollapsedGroups(newState);
localStorage.setItem('jobGroupsCollapsed', JSON.stringify(newState));
if (collapsedGroups[status] && !loadingStatuses.has(status)) {
await loadJobsForStatuses([status]);
setTimeout(() => {
window.scrollTo(0, currentScrollY);
}, 50);
}
};
const handleBatchTransactionsAssigned = async (assignments: { jobId: string, transactions: IndTransactionRecordNoId[] }[]) => {
try {
for (const { jobId, transactions } of assignments) {
await createMultipleTransactions(jobId, transactions, 'income');
}
} catch (error) {
console.error('Error assigning batch transactions:', error);
}
};
const handleBatchExpendituresAssigned = async (assignments: { jobId: string, transactions: IndTransactionRecordNoId[] }[]) => {
try {
for (const { jobId, transactions } of assignments) {
await createMultipleTransactions(jobId, transactions, 'expenditure');
}
} catch (error) {
console.error('Error assigning batch expenditures:', error);
}
};
return {
handleCreateJob,
handleEditJob,
handleUpdateJob,
handleDeleteJob,
handleUpdateProduced,
handleImportBOM,
toggleGroup,
handleBatchTransactionsAssigned,
handleBatchExpendituresAssigned
};
}

View File

@@ -1,18 +1,16 @@
import { useState, useEffect, useRef } from 'react';
import { IndTransactionRecordNoId, IndJobRecordNoId } from '@/lib/pbtypes';
import { getStatusPriority } from '@/utils/jobStatusUtils';
import JobForm from '@/components/JobForm';
import { IndJob } from '@/lib/types';
import BatchTransactionForm from '@/components/BatchTransactionForm';
import BatchExpenditureForm from '@/components/BatchExpenditureForm';
import { useJobs } from '@/hooks/useDataService';
import { useJobMetrics } from '@/hooks/useJobMetrics';
import SearchOverlay from '@/components/SearchOverlay';
import TransactionChart from '@/components/TransactionChart';
import DashboardStats from '@/components/DashboardStats';
import JobsToolbar from '@/components/JobsToolbar';
import JobsSection from '@/components/JobsSection';
import { useDashboard } from '@/hooks/useDashboard';
import { useDashboardHandlers } from '@/hooks/useDashboardHandlers';
import { useJobMetrics } from '@/hooks/useJobMetrics';
import { categorizeJobs } from '@/utils/jobFiltering';
const Index = () => {
const {
@@ -20,52 +18,57 @@ const Index = () => {
loading,
error,
loadingStatuses,
showJobForm,
setShowJobForm,
editingJob,
setEditingJob,
showBatchForm,
setShowBatchForm,
showBatchExpenditureForm,
setShowBatchExpenditureForm,
searchOpen,
setSearchOpen,
searchQuery,
setSearchQuery,
totalRevenueChartOpen,
setTotalRevenueChartOpen,
totalProfitChartOpen,
setTotalProfitChartOpen,
collapsedGroups,
setCollapsedGroups,
containerRef,
createJob,
updateJob,
deleteJob,
createMultipleTransactions,
createMultipleBillItems,
loadJobsForStatuses
} = useJobs();
} = useDashboard();
const [showJobForm, setShowJobForm] = useState(false);
const [editingJob, setEditingJob] = useState<IndJob | null>(null);
const [showBatchForm, setShowBatchForm] = useState(false);
const [showBatchExpenditureForm, setShowBatchExpenditureForm] = useState(false);
const [searchOpen, setSearchOpen] = useState(false);
const [searchQuery, setSearchQuery] = useState('');
const [totalRevenueChartOpen, setTotalRevenueChartOpen] = useState(false);
const [totalProfitChartOpen, setTotalProfitChartOpen] = useState(false);
const [collapsedGroups, setCollapsedGroups] = useState<Record<string, boolean>>(() => {
const saved = localStorage.getItem('jobGroupsCollapsed');
return saved ? JSON.parse(saved) : {};
const {
handleCreateJob,
handleEditJob,
handleUpdateJob,
handleDeleteJob,
handleUpdateProduced,
handleImportBOM,
toggleGroup,
handleBatchTransactionsAssigned,
handleBatchExpendituresAssigned
} = useDashboardHandlers({
createJob,
updateJob,
deleteJob,
createMultipleTransactions,
createMultipleBillItems,
loadJobsForStatuses,
setShowJobForm,
setEditingJob,
collapsedGroups,
setCollapsedGroups,
loadingStatuses
});
// Track scroll position to prevent jarring jumps
const scrollPositionRef = useRef<number>(0);
const containerRef = useRef<HTMLDivElement>(null);
useEffect(() => {
const handleKeyDown = (e: KeyboardEvent) => {
if ((e.ctrlKey || e.metaKey) && e.key === 'f') {
e.preventDefault();
setSearchOpen(true);
}
};
window.addEventListener('keydown', handleKeyDown);
return () => window.removeEventListener('keydown', handleKeyDown);
}, []);
useEffect(() => {
const handleScroll = () => {
scrollPositionRef.current = window.scrollY;
};
window.addEventListener('scroll', handleScroll);
return () => window.removeEventListener('scroll', handleScroll);
}, []);
if (loading) {
return (
<div className="min-h-screen bg-gray-950 p-6 flex items-center justify-center">
@@ -82,138 +85,16 @@ const Index = () => {
);
}
const filterJobs = (jobs: IndJob[]) => {
if (!searchQuery) return jobs;
const query = searchQuery.toLowerCase();
return jobs.filter(job =>
job.outputItem.toLowerCase().includes(query)
);
};
const sortedJobs = [...jobs].sort((a, b) => {
const priorityA = getStatusPriority(a.status);
const priorityB = getStatusPriority(b.status);
if (priorityA === priorityB) {
return new Date(b.created || '').getTime() - new Date(a.created || '').getTime();
}
return priorityA - priorityB;
});
const regularJobs = filterJobs(sortedJobs.filter(job => job.status !== 'Tracked'));
const trackedJobs = filterJobs(sortedJobs.filter(job => job.status === 'Tracked'));
const { regularJobs, trackedJobs } = categorizeJobs(jobs, searchQuery);
const { totalJobs, totalProfit, totalRevenue, calculateJobRevenue, calculateJobProfit } = useJobMetrics(regularJobs);
const handleCreateJob = async (jobData: IndJobRecordNoId, billOfMaterials?: { name: string; quantity: number }[]) => {
try {
const newJob = await createJob(jobData);
if (billOfMaterials && billOfMaterials.length > 0) {
const billItems = billOfMaterials.map(item => ({
name: item.name,
quantity: item.quantity,
unitPrice: 0
}));
await createMultipleBillItems(newJob.id, billItems, 'billOfMaterials');
}
setShowJobForm(false);
} catch (error) {
console.error('Error creating job:', error);
}
};
const handleEditJob = (job: IndJob) => {
setEditingJob(job);
setShowJobForm(true);
};
const handleUpdateJob = async (jobData: IndJobRecordNoId) => {
if (!editingJob) return;
try {
await updateJob(editingJob.id, jobData);
setShowJobForm(false);
setEditingJob(null);
} catch (error) {
console.error('Error updating job:', error);
}
};
const handleDeleteJob = async (jobId: string) => {
if (confirm('Are you sure you want to delete this job?')) {
try {
await deleteJob(jobId);
} catch (error) {
console.error('Error deleting job:', error);
}
}
};
const handleUpdateProduced = async (jobId: string, produced: number) => {
try {
await updateJob(jobId, { produced });
} catch (error) {
console.error('Error updating produced quantity:', error);
}
};
const handleImportBOM = async (jobId: string, items: { name: string; quantity: number }[]) => {
try {
const billItems = items.map(item => ({
name: item.name,
quantity: item.quantity,
unitPrice: 0
}));
await createMultipleBillItems(jobId, billItems, 'billOfMaterials');
} catch (error) {
console.error('Error importing BOM:', error);
}
};
const toggleGroup = async (status: string) => {
const currentScrollY = window.scrollY;
const newState = { ...collapsedGroups, [status]: !collapsedGroups[status] };
setCollapsedGroups(newState);
localStorage.setItem('jobGroupsCollapsed', JSON.stringify(newState));
if (collapsedGroups[status] && !loadingStatuses.has(status)) {
await loadJobsForStatuses([status]);
setTimeout(() => {
window.scrollTo(0, currentScrollY);
}, 50);
}
};
const handleBatchTransactionsAssigned = async (assignments: { jobId: string, transactions: IndTransactionRecordNoId[] }[]) => {
try {
for (const { jobId, transactions } of assignments) {
await createMultipleTransactions(jobId, transactions, 'income');
}
} catch (error) {
console.error('Error assigning batch transactions:', error);
}
};
const handleBatchExpendituresAssigned = async (assignments: { jobId: string, transactions: IndTransactionRecordNoId[] }[]) => {
try {
for (const { jobId, transactions } of assignments) {
await createMultipleTransactions(jobId, transactions, 'expenditure');
}
} catch (error) {
console.error('Error assigning batch expenditures:', error);
}
};
if (showJobForm) {
return (
<div className="min-h-screen bg-gray-950 p-6">
<div className="max-w-4xl mx-auto">
<JobForm
job={editingJob || undefined}
onSubmit={editingJob ? handleUpdateJob : handleCreateJob}
onSubmit={editingJob ? (jobData) => handleUpdateJob(jobData, editingJob) : handleCreateJob}
onCancel={() => {
setShowJobForm(false);
setEditingJob(null);

30
src/utils/jobFiltering.ts Normal file
View File

@@ -0,0 +1,30 @@
import { IndJob } from '@/lib/types';
import { getStatusPriority } from '@/utils/jobStatusUtils';
export const filterJobs = (jobs: IndJob[], searchQuery: string) => {
if (!searchQuery) return jobs;
const query = searchQuery.toLowerCase();
return jobs.filter(job =>
job.outputItem.toLowerCase().includes(query)
);
};
export const sortJobs = (jobs: IndJob[]) => {
return [...jobs].sort((a, b) => {
const priorityA = getStatusPriority(a.status);
const priorityB = getStatusPriority(b.status);
if (priorityA === priorityB) {
return new Date(b.created || '').getTime() - new Date(a.created || '').getTime();
}
return priorityA - priorityB;
});
};
export const categorizeJobs = (jobs: IndJob[], searchQuery: string) => {
const sortedJobs = sortJobs(jobs);
const regularJobs = filterJobs(sortedJobs.filter(job => job.status !== 'Tracked'), searchQuery);
const trackedJobs = filterJobs(sortedJobs.filter(job => job.status === 'Tracked'), searchQuery);
return { regularJobs, trackedJobs };
};