diff --git a/src/pages/ServiceOrderDetail.tsx b/src/pages/ServiceOrderDetail.tsx
index 332d22e..845c5b5 100644
--- a/src/pages/ServiceOrderDetail.tsx
+++ b/src/pages/ServiceOrderDetail.tsx
@@ -1,5 +1,9 @@
+import { useState } from 'react';
import { useParams, useNavigate, Link } from 'react-router-dom';
import { useQuery, useMutation, useQueryClient } from '@tanstack/react-query';
+import { useForm } from 'react-hook-form';
+import { zodResolver } from '@hookform/resolvers/zod';
+import { z } from 'zod';
import {
ArrowLeft,
Truck,
@@ -13,13 +17,16 @@ import {
Play,
Pause,
FileText,
- Edit2,
Loader2,
DollarSign,
+ Plus,
+ X,
+ Search,
} from 'lucide-react';
import { serviceOrdersApi } from '../services/api/serviceOrders';
+import { partsApi } from '../services/api/parts';
import type { ServiceOrder, ServiceOrderItem } from '../services/api/serviceOrders';
-import type { ServiceOrderStatus } from '../types';
+import type { ServiceOrderStatus, Part } from '../types';
const STATUS_CONFIG: Record
= {
received: { label: 'Recibido', color: 'text-blue-700', bgColor: 'bg-blue-100' },
@@ -40,6 +47,17 @@ const PRIORITY_CONFIG = {
urgent: { label: 'Urgente', color: 'text-red-600' },
};
+// Status flow for timeline
+const STATUS_FLOW: ServiceOrderStatus[] = [
+ 'received',
+ 'diagnosing',
+ 'quoted',
+ 'approved',
+ 'in_repair',
+ 'ready',
+ 'delivered',
+];
+
// Status transitions
const STATUS_ACTIONS: Record = {
received: [
@@ -69,10 +87,43 @@ const STATUS_ACTIONS: Record;
+
export function ServiceOrderDetailPage() {
const { id } = useParams<{ id: string }>();
const navigate = useNavigate();
const queryClient = useQueryClient();
+ const [showAddItemModal, setShowAddItemModal] = useState(false);
+ const [partSearch, setPartSearch] = useState('');
+ const [selectedPart, setSelectedPart] = useState(null);
+
+ const {
+ register,
+ handleSubmit,
+ setValue,
+ watch,
+ reset,
+ formState: { errors },
+ } = useForm({
+ resolver: zodResolver(addItemSchema),
+ defaultValues: {
+ itemType: 'service',
+ quantity: 1,
+ unitPrice: 0,
+ },
+ });
+
+ const itemType = watch('itemType');
// Fetch order details
const { data: orderData, isLoading, error } = useQuery({
@@ -88,6 +139,13 @@ export function ServiceOrderDetailPage() {
enabled: !!id,
});
+ // Search parts
+ const { data: partsData } = useQuery({
+ queryKey: ['parts-search', partSearch],
+ queryFn: () => partsApi.search(partSearch),
+ enabled: partSearch.length >= 2 && itemType === 'part',
+ });
+
// Status change mutation
const statusMutation = useMutation({
mutationFn: ({ status, notes }: { status: ServiceOrderStatus; notes?: string }) =>
@@ -98,8 +156,52 @@ export function ServiceOrderDetailPage() {
},
});
+ // Add item mutation
+ const addItemMutation = useMutation({
+ mutationFn: (item: Partial) => serviceOrdersApi.addItem(id!, item),
+ onSuccess: () => {
+ queryClient.invalidateQueries({ queryKey: ['service-order-items', id] });
+ queryClient.invalidateQueries({ queryKey: ['service-order', id] });
+ setShowAddItemModal(false);
+ reset();
+ setSelectedPart(null);
+ setPartSearch('');
+ },
+ });
+
+ // Remove item mutation
+ const removeItemMutation = useMutation({
+ mutationFn: (itemId: string) => serviceOrdersApi.removeItem(id!, itemId),
+ onSuccess: () => {
+ queryClient.invalidateQueries({ queryKey: ['service-order-items', id] });
+ queryClient.invalidateQueries({ queryKey: ['service-order', id] });
+ },
+ });
+
const order: ServiceOrder | undefined = orderData?.data;
const items: ServiceOrderItem[] = itemsData?.data || [];
+ const searchParts: Part[] = partsData?.data || [];
+
+ const onAddItem = (data: AddItemForm) => {
+ addItemMutation.mutate({
+ item_type: data.itemType,
+ part_id: data.partId || null,
+ description: data.description,
+ quantity: data.quantity,
+ unit_price: data.unitPrice,
+ actual_hours: data.actualHours || null,
+ discount: 0,
+ subtotal: data.quantity * data.unitPrice,
+ });
+ };
+
+ const handleSelectPart = (part: Part) => {
+ setSelectedPart(part);
+ setValue('partId', part.id);
+ setValue('description', part.name);
+ setValue('unitPrice', part.price);
+ setPartSearch(part.name);
+ };
if (isLoading) {
return (
@@ -128,6 +230,9 @@ export function ServiceOrderDetailPage() {
const laborItems = items.filter(i => i.item_type === 'service');
const partItems = items.filter(i => i.item_type === 'part');
+ // Calculate current status index for timeline
+ const currentStatusIndex = STATUS_FLOW.indexOf(order.status as ServiceOrderStatus);
+
return (
{/* Header */}
@@ -181,6 +286,54 @@ export function ServiceOrderDetailPage() {
)}
+ {/* Status Timeline */}
+
+
Progreso de la Orden
+
+ {STATUS_FLOW.map((status, index) => {
+ const isCompleted = index < currentStatusIndex;
+ const isCurrent = index === currentStatusIndex;
+ const config = STATUS_CONFIG[status];
+
+ return (
+
+
+
+ {isCompleted ? (
+
+ ) : (
+ {index + 1}
+ )}
+
+
+ {config.label}
+
+
+ {index < STATUS_FLOW.length - 1 && (
+
+ )}
+
+ );
+ })}
+
+
+
{/* Main Content */}
@@ -225,9 +378,12 @@ export function ServiceOrderDetailPage() {
Trabajos y Refacciones
-
@@ -245,15 +401,27 @@ export function ServiceOrderDetailPage() {
{laborItems.map((item) => (
-
+
{item.description}
{item.actual_hours && (
{item.actual_hours} hrs
)}
-
- ${item.subtotal.toLocaleString('es-MX', { minimumFractionDigits: 2 })}
-
+
+
+ ${item.subtotal.toLocaleString('es-MX', { minimumFractionDigits: 2 })}
+
+
{
+ if (confirm('Eliminar este item?')) {
+ removeItemMutation.mutate(item.id);
+ }
+ }}
+ className="text-gray-400 hover:text-red-500"
+ >
+
+
+
))}
@@ -270,15 +438,27 @@ export function ServiceOrderDetailPage() {
{partItems.map((item) => (
-
+
{item.description}
{item.quantity} x ${item.unit_price.toLocaleString('es-MX', { minimumFractionDigits: 2 })}
-
- ${item.subtotal.toLocaleString('es-MX', { minimumFractionDigits: 2 })}
-
+
+
+ ${item.subtotal.toLocaleString('es-MX', { minimumFractionDigits: 2 })}
+
+
{
+ if (confirm('Eliminar este item?')) {
+ removeItemMutation.mutate(item.id);
+ }
+ }}
+ className="text-gray-400 hover:text-red-500"
+ >
+
+
+
))}
@@ -380,7 +560,7 @@ export function ServiceOrderDetailPage() {
- {/* Timeline placeholder */}
+ {/* Timeline/History */}
@@ -397,10 +577,207 @@ export function ServiceOrderDetailPage() {
})}
+ {order.updated_at && order.updated_at !== order.created_at && (
+
+
+
Ultima actualizacion
+
+ {new Date(order.updated_at).toLocaleDateString('es-MX', {
+ day: 'numeric',
+ month: 'short',
+ })}
+
+
+ )}