Refactor: Split Index.tsx into smaller components
This commit refactors the `Index.tsx` file into smaller, more manageable components to improve code organization and readability.
This commit is contained in:
103
src/components/DashboardStats.tsx
Normal file
103
src/components/DashboardStats.tsx
Normal file
@@ -0,0 +1,103 @@
|
||||
|
||||
import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card';
|
||||
import { Button } from '@/components/ui/button';
|
||||
import { Factory, TrendingUp, Briefcase, BarChart3 } from 'lucide-react';
|
||||
import { formatISK } from '@/utils/priceUtils';
|
||||
import RecapPopover from './RecapPopover';
|
||||
import { IndJob } from '@/lib/types';
|
||||
|
||||
interface DashboardStatsProps {
|
||||
totalJobs: number;
|
||||
totalRevenue: number;
|
||||
totalProfit: number;
|
||||
jobs: IndJob[];
|
||||
calculateJobRevenue: (job: IndJob) => number;
|
||||
calculateJobProfit: (job: IndJob) => number;
|
||||
onTotalRevenueChart: () => void;
|
||||
onTotalProfitChart: () => void;
|
||||
}
|
||||
|
||||
const DashboardStats = ({
|
||||
totalJobs,
|
||||
totalRevenue,
|
||||
totalProfit,
|
||||
jobs,
|
||||
calculateJobRevenue,
|
||||
calculateJobProfit,
|
||||
onTotalRevenueChart,
|
||||
onTotalProfitChart
|
||||
}: DashboardStatsProps) => {
|
||||
return (
|
||||
<div className="grid grid-cols-1 md:grid-cols-3 gap-4">
|
||||
<Card className="bg-gray-900 border-gray-700 text-white">
|
||||
<CardHeader>
|
||||
<CardTitle className="flex items-center gap-2">
|
||||
<Factory className="w-5 h-5" />
|
||||
Active Jobs
|
||||
</CardTitle>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<div className="text-2xl font-bold">{totalJobs}</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
|
||||
<Card className="bg-gray-900 border-gray-700 text-white">
|
||||
<CardHeader>
|
||||
<CardTitle className="flex items-center gap-2">
|
||||
<TrendingUp className="w-5 h-5" />
|
||||
Total Revenue
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="sm"
|
||||
className="h-6 w-6 p-0 ml-auto"
|
||||
onClick={onTotalRevenueChart}
|
||||
>
|
||||
<BarChart3 className="w-4 h-4" />
|
||||
</Button>
|
||||
</CardTitle>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<RecapPopover
|
||||
title="Revenue Breakdown"
|
||||
jobs={jobs}
|
||||
calculateJobValue={calculateJobRevenue}
|
||||
>
|
||||
<div className="text-2xl font-bold text-green-400 cursor-pointer hover:text-green-300 transition-colors">
|
||||
{formatISK(totalRevenue)}
|
||||
</div>
|
||||
</RecapPopover>
|
||||
</CardContent>
|
||||
</Card>
|
||||
|
||||
<Card className="bg-gray-900 border-gray-700 text-white">
|
||||
<CardHeader>
|
||||
<CardTitle className="flex items-center gap-2">
|
||||
<Briefcase className="w-5 h-5" />
|
||||
Total Profit
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="sm"
|
||||
className="h-6 w-6 p-0 ml-auto"
|
||||
onClick={onTotalProfitChart}
|
||||
>
|
||||
<BarChart3 className="w-4 h-4" />
|
||||
</Button>
|
||||
</CardTitle>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<RecapPopover
|
||||
title="Profit Breakdown"
|
||||
jobs={jobs}
|
||||
calculateJobValue={calculateJobProfit}
|
||||
>
|
||||
<div className={`text-2xl font-bold cursor-pointer transition-colors ${totalProfit >= 0 ? 'text-green-400 hover:text-green-300' : 'text-red-400 hover:text-red-300'}`}>
|
||||
{formatISK(totalProfit)}
|
||||
</div>
|
||||
</RecapPopover>
|
||||
</CardContent>
|
||||
</Card>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default DashboardStats;
|
76
src/components/JobsSection.tsx
Normal file
76
src/components/JobsSection.tsx
Normal file
@@ -0,0 +1,76 @@
|
||||
|
||||
import { IndJob } from '@/lib/types';
|
||||
import JobGroup from './JobGroup';
|
||||
|
||||
interface JobsSectionProps {
|
||||
regularJobs: IndJob[];
|
||||
trackedJobs: IndJob[];
|
||||
collapsedGroups: Record<string, boolean>;
|
||||
loadingStatuses: Set<string>;
|
||||
onToggleGroup: (status: string) => void;
|
||||
onEdit: (job: IndJob) => void;
|
||||
onDelete: (jobId: string) => void;
|
||||
onUpdateProduced: (jobId: string, produced: number) => void;
|
||||
onImportBOM: (jobId: string, items: { name: string; quantity: number }[]) => void;
|
||||
}
|
||||
|
||||
const JobsSection = ({
|
||||
regularJobs,
|
||||
trackedJobs,
|
||||
collapsedGroups,
|
||||
loadingStatuses,
|
||||
onToggleGroup,
|
||||
onEdit,
|
||||
onDelete,
|
||||
onUpdateProduced,
|
||||
onImportBOM
|
||||
}: JobsSectionProps) => {
|
||||
const jobGroups = regularJobs.reduce((groups, job) => {
|
||||
const status = job.status;
|
||||
if (!groups[status]) {
|
||||
groups[status] = [];
|
||||
}
|
||||
groups[status].push(job);
|
||||
return groups;
|
||||
}, {} as Record<string, IndJob[]>);
|
||||
|
||||
return (
|
||||
<div className="space-y-4">
|
||||
<div className="space-y-6">
|
||||
{Object.entries(jobGroups).map(([status, statusJobs]) => (
|
||||
<JobGroup
|
||||
key={status}
|
||||
status={status}
|
||||
jobs={statusJobs}
|
||||
isCollapsed={collapsedGroups[status] || false}
|
||||
onToggle={onToggleGroup}
|
||||
onEdit={onEdit}
|
||||
onDelete={onDelete}
|
||||
onUpdateProduced={onUpdateProduced}
|
||||
onImportBOM={onImportBOM}
|
||||
isLoading={loadingStatuses.has(status)}
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
|
||||
{trackedJobs.length > 0 && (
|
||||
<div className="space-y-4 mt-8 pt-8 border-t border-gray-700">
|
||||
<JobGroup
|
||||
status="Tracked"
|
||||
jobs={trackedJobs}
|
||||
isCollapsed={collapsedGroups['Tracked'] || false}
|
||||
onToggle={onToggleGroup}
|
||||
onEdit={onEdit}
|
||||
onDelete={onDelete}
|
||||
onUpdateProduced={onUpdateProduced}
|
||||
onImportBOM={onImportBOM}
|
||||
isTracked={true}
|
||||
isLoading={loadingStatuses.has('Tracked')}
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default JobsSection;
|
46
src/components/JobsToolbar.tsx
Normal file
46
src/components/JobsToolbar.tsx
Normal file
@@ -0,0 +1,46 @@
|
||||
|
||||
import { Button } from '@/components/ui/button';
|
||||
import { Plus, FileText, ShoppingCart } from 'lucide-react';
|
||||
import SalesTaxConfig from './SalesTaxConfig';
|
||||
|
||||
interface JobsToolbarProps {
|
||||
onNewJob: () => void;
|
||||
onBatchIncome: () => void;
|
||||
onBatchExpenditure: () => void;
|
||||
}
|
||||
|
||||
const JobsToolbar = ({ onNewJob, onBatchIncome, onBatchExpenditure }: JobsToolbarProps) => {
|
||||
return (
|
||||
<div className="flex justify-between items-center">
|
||||
<h2 className="text-xl font-bold text-white">Jobs</h2>
|
||||
<div className="flex gap-2">
|
||||
<SalesTaxConfig />
|
||||
<Button
|
||||
variant="outline"
|
||||
onClick={onBatchIncome}
|
||||
className="border-gray-600 hover:bg-gray-800"
|
||||
>
|
||||
<FileText className="w-4 h-4 mr-2" />
|
||||
Batch Income
|
||||
</Button>
|
||||
<Button
|
||||
variant="outline"
|
||||
onClick={onBatchExpenditure}
|
||||
className="border-gray-600 hover:bg-gray-800"
|
||||
>
|
||||
<ShoppingCart className="w-4 h-4 mr-2" />
|
||||
Batch Expenditure
|
||||
</Button>
|
||||
<Button
|
||||
onClick={onNewJob}
|
||||
className="bg-blue-600 hover:bg-blue-700"
|
||||
>
|
||||
<Plus className="w-4 h-4 mr-2" />
|
||||
New Job
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default JobsToolbar;
|
68
src/components/SalesTaxConfig.tsx
Normal file
68
src/components/SalesTaxConfig.tsx
Normal file
@@ -0,0 +1,68 @@
|
||||
|
||||
import { useState } from 'react';
|
||||
import { Button } from '@/components/ui/button';
|
||||
import { Input } from '@/components/ui/input';
|
||||
import { Label } from '@/components/ui/label';
|
||||
import { Popover, PopoverContent, PopoverTrigger } from '@/components/ui/popover';
|
||||
import { Settings } from 'lucide-react';
|
||||
|
||||
const SalesTaxConfig = () => {
|
||||
const [salesTax, setSalesTax] = useState(() => {
|
||||
return localStorage.getItem('salesTax') || '0';
|
||||
});
|
||||
const [isOpen, setIsOpen] = useState(false);
|
||||
|
||||
const handleSave = () => {
|
||||
localStorage.setItem('salesTax', salesTax);
|
||||
setIsOpen(false);
|
||||
window.dispatchEvent(new StorageEvent('storage', {
|
||||
key: 'salesTax',
|
||||
newValue: salesTax
|
||||
}));
|
||||
};
|
||||
|
||||
return (
|
||||
<Popover open={isOpen} onOpenChange={setIsOpen}>
|
||||
<PopoverTrigger asChild>
|
||||
<Button
|
||||
variant="outline"
|
||||
className="border-gray-600 hover:bg-gray-800"
|
||||
>
|
||||
<Settings className="w-4 h-4 mr-2" />
|
||||
Tax Config
|
||||
</Button>
|
||||
</PopoverTrigger>
|
||||
<PopoverContent className="w-80 bg-gray-900 border-gray-700 text-white">
|
||||
<div className="space-y-4">
|
||||
<div className="space-y-2">
|
||||
<Label htmlFor="salesTax" className="text-sm font-medium text-gray-300">
|
||||
Sales Tax (%)
|
||||
</Label>
|
||||
<Input
|
||||
id="salesTax"
|
||||
type="number"
|
||||
value={salesTax}
|
||||
onChange={(e) => setSalesTax(e.target.value)}
|
||||
onBlur={handleSave}
|
||||
onKeyDown={(e) => {
|
||||
if (e.key === 'Enter') {
|
||||
handleSave();
|
||||
}
|
||||
}}
|
||||
placeholder="0"
|
||||
min="0"
|
||||
max="100"
|
||||
step="0.1"
|
||||
className="bg-gray-800 border-gray-600 text-white"
|
||||
/>
|
||||
<p className="text-xs text-gray-400">
|
||||
Applied to minimum price calculations
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</PopoverContent>
|
||||
</Popover>
|
||||
);
|
||||
};
|
||||
|
||||
export default SalesTaxConfig;
|
@@ -1,23 +1,18 @@
|
||||
|
||||
import { useState, useEffect, useRef } from 'react';
|
||||
import { Button } from '@/components/ui/button';
|
||||
import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card';
|
||||
import { Input } from '@/components/ui/input';
|
||||
import { Label } from '@/components/ui/label';
|
||||
import { Popover, PopoverContent, PopoverTrigger } from '@/components/ui/popover';
|
||||
import { Plus, Factory, TrendingUp, Briefcase, FileText, Settings, BarChart3, ShoppingCart } from 'lucide-react';
|
||||
import { IndTransactionRecordNoId, IndJobRecordNoId } from '@/lib/pbtypes';
|
||||
import { formatISK } from '@/utils/priceUtils';
|
||||
import { getStatusPriority } from '@/utils/jobStatusUtils';
|
||||
import JobForm from '@/components/JobForm';
|
||||
import JobGroup from '@/components/JobGroup';
|
||||
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 RecapPopover from '@/components/RecapPopover';
|
||||
import TransactionChart from '@/components/TransactionChart';
|
||||
import DashboardStats from '@/components/DashboardStats';
|
||||
import JobsToolbar from '@/components/JobsToolbar';
|
||||
import JobsSection from '@/components/JobsSection';
|
||||
|
||||
const Index = () => {
|
||||
const {
|
||||
@@ -176,15 +171,6 @@ const Index = () => {
|
||||
}
|
||||
};
|
||||
|
||||
const jobGroups = regularJobs.reduce((groups, job) => {
|
||||
const status = job.status;
|
||||
if (!groups[status]) {
|
||||
groups[status] = [];
|
||||
}
|
||||
groups[status].push(job);
|
||||
return groups;
|
||||
}, {} as Record<string, IndJob[]>);
|
||||
|
||||
const toggleGroup = async (status: string) => {
|
||||
const currentScrollY = window.scrollY;
|
||||
|
||||
@@ -248,143 +234,41 @@ const Index = () => {
|
||||
}}
|
||||
onSearch={setSearchQuery}
|
||||
/>
|
||||
<div className="grid grid-cols-1 md:grid-cols-3 gap-4">
|
||||
<Card className="bg-gray-900 border-gray-700 text-white">
|
||||
<CardHeader>
|
||||
<CardTitle className="flex items-center gap-2">
|
||||
<Factory className="w-5 h-5" />
|
||||
Active Jobs
|
||||
</CardTitle>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<div className="text-2xl font-bold">{totalJobs}</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
<Card className="bg-gray-900 border-gray-700 text-white">
|
||||
<CardHeader>
|
||||
<CardTitle className="flex items-center gap-2">
|
||||
<TrendingUp className="w-5 h-5" />
|
||||
Total Revenue
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="sm"
|
||||
className="h-6 w-6 p-0 ml-auto"
|
||||
onClick={() => setTotalRevenueChartOpen(true)}
|
||||
>
|
||||
<BarChart3 className="w-4 h-4" />
|
||||
</Button>
|
||||
</CardTitle>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<RecapPopover
|
||||
title="Revenue Breakdown"
|
||||
jobs={regularJobs}
|
||||
calculateJobValue={calculateJobRevenue}
|
||||
>
|
||||
<div className="text-2xl font-bold text-green-400 cursor-pointer hover:text-green-300 transition-colors">
|
||||
{formatISK(totalRevenue)}
|
||||
</div>
|
||||
</RecapPopover>
|
||||
</CardContent>
|
||||
</Card>
|
||||
<Card className="bg-gray-900 border-gray-700 text-white">
|
||||
<CardHeader>
|
||||
<CardTitle className="flex items-center gap-2">
|
||||
<Briefcase className="w-5 h-5" />
|
||||
Total Profit
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="sm"
|
||||
className="h-6 w-6 p-0 ml-auto"
|
||||
onClick={() => setTotalProfitChartOpen(true)}
|
||||
>
|
||||
<BarChart3 className="w-4 h-4" />
|
||||
</Button>
|
||||
</CardTitle>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<RecapPopover
|
||||
title="Profit Breakdown"
|
||||
jobs={regularJobs}
|
||||
calculateJobValue={calculateJobProfit}
|
||||
>
|
||||
<div className={`text-2xl font-bold cursor-pointer transition-colors ${totalProfit >= 0 ? 'text-green-400 hover:text-green-300' : 'text-red-400 hover:text-red-300'}`}>
|
||||
{formatISK(totalProfit)}
|
||||
</div>
|
||||
</RecapPopover>
|
||||
</CardContent>
|
||||
</Card>
|
||||
</div>
|
||||
|
||||
<DashboardStats
|
||||
totalJobs={totalJobs}
|
||||
totalRevenue={totalRevenue}
|
||||
totalProfit={totalProfit}
|
||||
jobs={regularJobs}
|
||||
calculateJobRevenue={calculateJobRevenue}
|
||||
calculateJobProfit={calculateJobProfit}
|
||||
onTotalRevenueChart={() => setTotalRevenueChartOpen(true)}
|
||||
onTotalProfitChart={() => setTotalProfitChartOpen(true)}
|
||||
/>
|
||||
|
||||
<div className="space-y-4">
|
||||
<div className="flex justify-between items-center">
|
||||
<h2 className="text-xl font-bold text-white">Jobs</h2>
|
||||
<div className="flex gap-2">
|
||||
<SalesTaxConfig />
|
||||
<Button
|
||||
variant="outline"
|
||||
onClick={() => setShowBatchForm(true)}
|
||||
className="border-gray-600 hover:bg-gray-800"
|
||||
>
|
||||
<FileText className="w-4 h-4 mr-2" />
|
||||
Batch Income
|
||||
</Button>
|
||||
<Button
|
||||
variant="outline"
|
||||
onClick={() => setShowBatchExpenditureForm(true)}
|
||||
className="border-gray-600 hover:bg-gray-800"
|
||||
>
|
||||
<ShoppingCart className="w-4 h-4 mr-2" />
|
||||
Batch Expenditure
|
||||
</Button>
|
||||
<Button
|
||||
onClick={() => {
|
||||
setEditingJob(null);
|
||||
setShowJobForm(true);
|
||||
}}
|
||||
className="bg-blue-600 hover:bg-blue-700"
|
||||
>
|
||||
<Plus className="w-4 h-4 mr-2" />
|
||||
New Job
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
<JobsToolbar
|
||||
onNewJob={() => {
|
||||
setEditingJob(null);
|
||||
setShowJobForm(true);
|
||||
}}
|
||||
onBatchIncome={() => setShowBatchForm(true)}
|
||||
onBatchExpenditure={() => setShowBatchExpenditureForm(true)}
|
||||
/>
|
||||
|
||||
<div className="space-y-6">
|
||||
{Object.entries(jobGroups).map(([status, statusJobs]) => (
|
||||
<JobGroup
|
||||
key={status}
|
||||
status={status}
|
||||
jobs={statusJobs}
|
||||
isCollapsed={collapsedGroups[status] || false}
|
||||
onToggle={toggleGroup}
|
||||
onEdit={handleEditJob}
|
||||
onDelete={handleDeleteJob}
|
||||
onUpdateProduced={handleUpdateProduced}
|
||||
onImportBOM={handleImportBOM}
|
||||
isLoading={loadingStatuses.has(status)}
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
<JobsSection
|
||||
regularJobs={regularJobs}
|
||||
trackedJobs={trackedJobs}
|
||||
collapsedGroups={collapsedGroups}
|
||||
loadingStatuses={loadingStatuses}
|
||||
onToggleGroup={toggleGroup}
|
||||
onEdit={handleEditJob}
|
||||
onDelete={handleDeleteJob}
|
||||
onUpdateProduced={handleUpdateProduced}
|
||||
onImportBOM={handleImportBOM}
|
||||
/>
|
||||
</div>
|
||||
|
||||
{trackedJobs.length > 0 && (
|
||||
<div className="space-y-4 mt-8 pt-8 border-t border-gray-700">
|
||||
<JobGroup
|
||||
status="Tracked"
|
||||
jobs={trackedJobs}
|
||||
isCollapsed={collapsedGroups['Tracked'] || false}
|
||||
onToggle={toggleGroup}
|
||||
onEdit={handleEditJob}
|
||||
onDelete={handleDeleteJob}
|
||||
onUpdateProduced={handleUpdateProduced}
|
||||
onImportBOM={handleImportBOM}
|
||||
isTracked={true}
|
||||
isLoading={loadingStatuses.has('Tracked')}
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{showBatchForm && (
|
||||
<BatchTransactionForm
|
||||
jobs={jobs}
|
||||
@@ -418,63 +302,4 @@ const Index = () => {
|
||||
);
|
||||
};
|
||||
|
||||
const SalesTaxConfig = () => {
|
||||
const [salesTax, setSalesTax] = useState(() => {
|
||||
return localStorage.getItem('salesTax') || '0';
|
||||
});
|
||||
const [isOpen, setIsOpen] = useState(false);
|
||||
|
||||
const handleSave = () => {
|
||||
localStorage.setItem('salesTax', salesTax);
|
||||
setIsOpen(false);
|
||||
window.dispatchEvent(new StorageEvent('storage', {
|
||||
key: 'salesTax',
|
||||
newValue: salesTax
|
||||
}));
|
||||
};
|
||||
|
||||
return (
|
||||
<Popover open={isOpen} onOpenChange={setIsOpen}>
|
||||
<PopoverTrigger asChild>
|
||||
<Button
|
||||
variant="outline"
|
||||
className="border-gray-600 hover:bg-gray-800"
|
||||
>
|
||||
<Settings className="w-4 h-4 mr-2" />
|
||||
Tax Config
|
||||
</Button>
|
||||
</PopoverTrigger>
|
||||
<PopoverContent className="w-80 bg-gray-900 border-gray-700 text-white">
|
||||
<div className="space-y-4">
|
||||
<div className="space-y-2">
|
||||
<Label htmlFor="salesTax" className="text-sm font-medium text-gray-300">
|
||||
Sales Tax (%)
|
||||
</Label>
|
||||
<Input
|
||||
id="salesTax"
|
||||
type="number"
|
||||
value={salesTax}
|
||||
onChange={(e) => setSalesTax(e.target.value)}
|
||||
onBlur={handleSave}
|
||||
onKeyDown={(e) => {
|
||||
if (e.key === 'Enter') {
|
||||
handleSave();
|
||||
}
|
||||
}}
|
||||
placeholder="0"
|
||||
min="0"
|
||||
max="100"
|
||||
step="0.1"
|
||||
className="bg-gray-800 border-gray-600 text-white"
|
||||
/>
|
||||
<p className="text-xs text-gray-400">
|
||||
Applied to minimum price calculations
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</PopoverContent>
|
||||
</Popover>
|
||||
);
|
||||
};
|
||||
|
||||
export default Index;
|
||||
|
Reference in New Issue
Block a user