Refactor Index page
Refactor `src/pages/Index.tsx` into smaller components.
This commit is contained in:
92
src/hooks/useDashboard.ts
Normal file
92
src/hooks/useDashboard.ts
Normal 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
|
||||
};
|
||||
}
|
147
src/hooks/useDashboardHandlers.ts
Normal file
147
src/hooks/useDashboardHandlers.ts
Normal 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
|
||||
};
|
||||
}
|
@@ -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
30
src/utils/jobFiltering.ts
Normal 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 };
|
||||
};
|
Reference in New Issue
Block a user