Add job status navigation buttons
Adds forward and backward buttons to job cards to change job status. Implements new statuses and highlights jobs needing attention.
This commit is contained in:
@@ -1,3 +1,4 @@
|
||||
|
||||
import { CardTitle } from '@/components/ui/card';
|
||||
import { Button } from '@/components/ui/button';
|
||||
import { Copy, BarChart3 } from 'lucide-react';
|
||||
@@ -6,6 +7,7 @@ import { useClipboard } from '@/hooks/useClipboard';
|
||||
import { useJobs } from '@/hooks/useDataService';
|
||||
import { useState } from 'react';
|
||||
import JobStatusDropdown from './JobStatusDropdown';
|
||||
import JobStatusNavigation from './JobStatusNavigation';
|
||||
import BOMActions from './BOMActions';
|
||||
import EditableProduced from './EditableProduced';
|
||||
import TransactionChart from './TransactionChart';
|
||||
@@ -69,6 +71,7 @@ const JobCardHeader: React.FC<JobCardHeaderProps> = ({
|
||||
</div>
|
||||
<div className="flex flex-col gap-2 flex-shrink-0 items-end">
|
||||
<div className="flex items-center gap-2">
|
||||
<JobStatusNavigation job={job} />
|
||||
<JobStatusDropdown job={job} />
|
||||
<Button
|
||||
variant="outline"
|
||||
@@ -128,4 +131,4 @@ const JobCardHeader: React.FC<JobCardHeaderProps> = ({
|
||||
);
|
||||
};
|
||||
|
||||
export default JobCardHeader;
|
||||
export default JobCardHeader;
|
||||
|
105
src/components/JobStatusNavigation.tsx
Normal file
105
src/components/JobStatusNavigation.tsx
Normal file
@@ -0,0 +1,105 @@
|
||||
|
||||
import { ChevronLeft, ChevronRight } from 'lucide-react';
|
||||
import { Button } from '@/components/ui/button';
|
||||
import { IndJob } from '@/lib/types';
|
||||
import { getNextStatus, getPreviousStatus } from '@/utils/jobStatusUtils';
|
||||
import { useJobs } from '@/hooks/useDataService';
|
||||
import { useToast } from '@/hooks/use-toast';
|
||||
|
||||
interface JobStatusNavigationProps {
|
||||
job: IndJob;
|
||||
}
|
||||
|
||||
const JobStatusNavigation: React.FC<JobStatusNavigationProps> = ({ job }) => {
|
||||
const { updateJob } = useJobs();
|
||||
const { toast } = useToast();
|
||||
|
||||
const nextStatus = getNextStatus(job.status);
|
||||
const previousStatus = getPreviousStatus(job.status);
|
||||
|
||||
const handleStatusChange = async (newStatus: string) => {
|
||||
try {
|
||||
const currentTime = new Date().toISOString();
|
||||
const updates: { status: string; [key: string]: any } = { status: newStatus };
|
||||
|
||||
// Automatically assign dates based on status
|
||||
switch (newStatus) {
|
||||
case 'Running':
|
||||
updates.jobStart = currentTime;
|
||||
break;
|
||||
case 'Done':
|
||||
updates.jobEnd = currentTime;
|
||||
break;
|
||||
case 'Selling':
|
||||
updates.saleStart = currentTime;
|
||||
break;
|
||||
case 'Closed':
|
||||
updates.saleEnd = currentTime;
|
||||
break;
|
||||
}
|
||||
|
||||
await updateJob(job.id, updates);
|
||||
|
||||
const dateMessages = [];
|
||||
if (updates.jobStart) dateMessages.push('job start date set');
|
||||
if (updates.jobEnd) dateMessages.push('job end date set');
|
||||
if (updates.saleStart) dateMessages.push('sale start date set');
|
||||
if (updates.saleEnd) dateMessages.push('sale end date set');
|
||||
|
||||
const description = dateMessages.length > 0
|
||||
? `Job status changed to ${newStatus} and ${dateMessages.join(', ')}`
|
||||
: `Job status changed to ${newStatus}`;
|
||||
|
||||
toast({
|
||||
title: "Status Updated",
|
||||
description,
|
||||
duration: 2000,
|
||||
});
|
||||
} catch (error) {
|
||||
console.error('Error updating status:', error);
|
||||
toast({
|
||||
title: "Error",
|
||||
description: "Failed to update status",
|
||||
variant: "destructive",
|
||||
duration: 2000,
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="flex gap-1">
|
||||
{previousStatus && (
|
||||
<Button
|
||||
variant="outline"
|
||||
size="sm"
|
||||
onClick={(e) => {
|
||||
e.stopPropagation();
|
||||
handleStatusChange(previousStatus);
|
||||
}}
|
||||
className="border-gray-600 hover:bg-gray-800 p-2"
|
||||
data-no-navigate
|
||||
title={`Move to ${previousStatus}`}
|
||||
>
|
||||
<ChevronLeft className="w-4 h-4" />
|
||||
</Button>
|
||||
)}
|
||||
{nextStatus && (
|
||||
<Button
|
||||
variant="outline"
|
||||
size="sm"
|
||||
onClick={(e) => {
|
||||
e.stopPropagation();
|
||||
handleStatusChange(nextStatus);
|
||||
}}
|
||||
className="border-gray-600 hover:bg-gray-800 p-2"
|
||||
data-no-navigate
|
||||
title={`Move to ${nextStatus}`}
|
||||
>
|
||||
<ChevronRight className="w-4 h-4" />
|
||||
</Button>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default JobStatusNavigation;
|
@@ -21,6 +21,7 @@ export type IndJob = {
|
||||
updated?: IsoDateString
|
||||
projectedCost?: number
|
||||
projectedRevenue?: number
|
||||
runtime?: number
|
||||
}
|
||||
|
||||
export type IndTransaction = IndTransactionRecord;
|
||||
|
@@ -33,6 +33,20 @@ export function jobNeedsAttention(job: IndJob): boolean {
|
||||
return allMaterialsSatisfied;
|
||||
}
|
||||
|
||||
// Running jobs need attention when they have finished (start date + runtime > current time)
|
||||
if (job.status === 'Running') {
|
||||
if (!job.jobStart || !job.runtime) {
|
||||
return false;
|
||||
}
|
||||
|
||||
const startTime = new Date(job.jobStart).getTime();
|
||||
const runtimeMs = job.runtime * 1000; // Convert seconds to milliseconds
|
||||
const finishTime = startTime + runtimeMs;
|
||||
const currentTime = Date.now();
|
||||
|
||||
return currentTime >= finishTime;
|
||||
}
|
||||
|
||||
// Selling jobs need attention when sold count reaches produced count
|
||||
if (job.status === 'Selling') {
|
||||
const produced = job.produced || 0;
|
||||
|
@@ -5,9 +5,13 @@ export const getStatusColor = (status: string) => {
|
||||
switch (status) {
|
||||
case 'Planned': return 'bg-gray-600';
|
||||
case 'Acquisition': return 'bg-yellow-600';
|
||||
case 'Staging': return 'bg-amber-600';
|
||||
case 'Inbound': return 'bg-orange-600';
|
||||
case 'Running': return 'bg-blue-600';
|
||||
case 'Done': return 'bg-purple-600';
|
||||
case 'Selling': return 'bg-orange-600';
|
||||
case 'Delivered': return 'bg-indigo-600';
|
||||
case 'Outbound': return 'bg-pink-600';
|
||||
case 'Selling': return 'bg-emerald-600';
|
||||
case 'Closed': return 'bg-green-600';
|
||||
case 'Tracked': return 'bg-cyan-600';
|
||||
default: return 'bg-gray-600';
|
||||
@@ -18,9 +22,13 @@ export const getStatusBackgroundColor = (status: string) => {
|
||||
switch (status) {
|
||||
case 'Planned': return 'bg-gray-600/20';
|
||||
case 'Acquisition': return 'bg-yellow-600/20';
|
||||
case 'Staging': return 'bg-amber-600/20';
|
||||
case 'Inbound': return 'bg-orange-600/20';
|
||||
case 'Running': return 'bg-blue-600/20';
|
||||
case 'Done': return 'bg-purple-600/20';
|
||||
case 'Selling': return 'bg-orange-600/20';
|
||||
case 'Delivered': return 'bg-indigo-600/20';
|
||||
case 'Outbound': return 'bg-pink-600/20';
|
||||
case 'Selling': return 'bg-emerald-600/20';
|
||||
case 'Closed': return 'bg-green-600/20';
|
||||
case 'Tracked': return 'bg-cyan-600/20';
|
||||
default: return 'bg-gray-600/20';
|
||||
@@ -29,15 +37,31 @@ export const getStatusBackgroundColor = (status: string) => {
|
||||
|
||||
export const getStatusPriority = (status: IndJobStatusOptions): number => {
|
||||
switch (status) {
|
||||
case 'Planned': return 6;
|
||||
case 'Acquisition': return 1;
|
||||
case 'Running': return 2;
|
||||
case 'Done': return 3;
|
||||
case 'Selling': return 4;
|
||||
case 'Closed': return 5;
|
||||
case 'Tracked': return 7;
|
||||
case 'Planned': return 1;
|
||||
case 'Acquisition': return 2;
|
||||
case 'Staging': return 3;
|
||||
case 'Inbound': return 4;
|
||||
case 'Running': return 5;
|
||||
case 'Done': return 6;
|
||||
case 'Delivered': return 7;
|
||||
case 'Outbound': return 8;
|
||||
case 'Selling': return 9;
|
||||
case 'Closed': return 10;
|
||||
case 'Tracked': return 11;
|
||||
default: return 0;
|
||||
}
|
||||
};
|
||||
|
||||
export const JOB_STATUSES = ['Planned', 'Acquisition', 'Running', 'Done', 'Selling', 'Closed', 'Tracked'] as const;
|
||||
export const JOB_STATUSES = ['Planned', 'Acquisition', 'Staging', 'Inbound', 'Running', 'Done', 'Delivered', 'Outbound', 'Selling', 'Closed', 'Tracked'] as const;
|
||||
|
||||
export const getNextStatus = (currentStatus: string): string | null => {
|
||||
const currentIndex = JOB_STATUSES.indexOf(currentStatus as any);
|
||||
if (currentIndex === -1 || currentIndex === JOB_STATUSES.length - 1) return null;
|
||||
return JOB_STATUSES[currentIndex + 1];
|
||||
};
|
||||
|
||||
export const getPreviousStatus = (currentStatus: string): string | null => {
|
||||
const currentIndex = JOB_STATUSES.indexOf(currentStatus as any);
|
||||
if (currentIndex <= 0) return null;
|
||||
return JOB_STATUSES[currentIndex - 1];
|
||||
};
|
||||
|
Reference in New Issue
Block a user