From 00f7a21581ada6f808280428a8207565d59bf520 Mon Sep 17 00:00:00 2001 From: "google-labs-jules[bot]" <161369871+google-labs-jules[bot]@users.noreply.github.com> Date: Sat, 13 Dec 2025 23:27:11 +0000 Subject: [PATCH] Refactor Kanban Board to use Sheet, Tagging, and Batch Actions - Replaced `RequisicaoModal` with `RequisicaoSheet` for a slide-over experience. - Implemented item tagging ("Separar" vs "Comprar") in the request details. - Added status lock validation: requests cannot be moved to "Em Atendimento" until all items are tagged. - Added visual indicators (icons) on Kanban cards based on item tags. - Implemented Batch Selection (Shift + Click) and bulk status update. - Fixed a typo in `login-form.tsx` causing inputs to be uncontrolled. - Updated `mock-data.ts` to support item tags. --- components/auth/login-form.tsx | 8 +- components/ordi/kanban-board.tsx | 176 ++++++++++++++-- components/ordi/requisicao-modal.tsx | 271 ------------------------ components/ordi/requisicao-sheet.tsx | 301 +++++++++++++++++++++++++++ components/ordi/requisicoes-list.tsx | 15 +- lib/mock-data.ts | 3 + 6 files changed, 473 insertions(+), 301 deletions(-) delete mode 100644 components/ordi/requisicao-modal.tsx create mode 100644 components/ordi/requisicao-sheet.tsx diff --git a/components/auth/login-form.tsx b/components/auth/login-form.tsx index 97445bf..aa38b92 100644 --- a/components/auth/login-form.tsx +++ b/components/auth/login-form.tsx @@ -35,8 +35,8 @@ export function LoginForm() { id="email" type="email" placeholder="seu@email.com" - Stockue={email} - onChange={(e) => setEmail(e.target.Stockue)} + value={email} + onChange={(e) => setEmail(e.target.value)} required className="mobile-optimized" /> @@ -47,8 +47,8 @@ export function LoginForm() { id="password" type="password" placeholder="••••••••" - Stockue={password} - onChange={(e) => setPassword(e.target.Stockue)} + value={password} + onChange={(e) => setPassword(e.target.value)} required className="mobile-optimized" /> diff --git a/components/ordi/kanban-board.tsx b/components/ordi/kanban-board.tsx index 7560072..30d6c31 100644 --- a/components/ordi/kanban-board.tsx +++ b/components/ordi/kanban-board.tsx @@ -11,10 +11,13 @@ import { Trash2, Archive, Building2, + ShoppingCart, + Box, + CheckSquare, } from "lucide-react"; import { Button } from "@/components/ui/button"; import { useRequisicoesStore } from "@/lib/requisicoes-store"; -import { RequisicaoModal } from "./requisicao-modal"; +import { RequisicaoSheet } from "./requisicao-sheet"; import { TrashSheet } from "@/components/ordi/trash/trash-sheet"; import { useToast } from "@/hooks/use-toast"; import { @@ -72,20 +75,99 @@ export function KanbanBoard({ requisicoes }: KanbanBoardProps) { const [selectedRequisicao, setSelectedRequisicao] = useState(null); const [isTrashOpen, setIsTrashOpen] = useState(false); + const [selectedCards, setSelectedCards] = useState([]); const { toast } = useToast(); - const handleNextStatus = (id: string, currentStatus: StatusRequisicao) => { + const handleNextStatus = (req: Requisicao, currentStatus: StatusRequisicao) => { + // Status Lock Validation + if (currentStatus === "nova") { + const allItemsTagged = req.itens.every( + (item) => item.tag === "separar" || item.tag === "comprar" + ); + + if (!allItemsTagged) { + toast({ + title: "Ação Bloqueada", + description: "Todos os itens devem ser classificados como 'Separar' ou 'Comprar' antes de mover para Em Atendimento.", + variant: "destructive", + }); + return; + } + } + let next: StatusRequisicao = "nova"; if (currentStatus === "nova") next = "em_atendimento"; else if (currentStatus === "em_atendimento") next = "concluida"; - updateStatus(id, next); + updateStatus(req.id, next); toast({ title: "Status atualizado", description: `Pedido movido para ${statusLabels[next]}.`, }); }; + const handleBatchMove = () => { + // For simplicity, we only assume moving forward one step for all selected + // In a real app we might need to check individual status compatibility + // But usually batch actions apply when they are in the same column? + // The requirement says: "show a floating action bar to move them all to the next status at once." + + // We should only move those that are valid. + + const processed = []; + const failed = []; + + selectedCards.forEach(id => { + const req = requisicoes.find(r => r.id === id); + if(!req) return; + + // Logic for next status + let next: StatusRequisicao | null = null; + if (req.status === "nova") { + const allItemsTagged = req.itens.every( + (item) => item.tag === "separar" || item.tag === "comprar" + ); + if (allItemsTagged) next = "em_atendimento"; + else failed.push(id); + } + else if (req.status === "em_atendimento") next = "concluida"; + + if (next) { + updateStatus(id, next); + processed.push(id); + } + }); + + if (processed.length > 0) { + toast({ + title: "Lote processado", + description: `${processed.length} itens movidos com sucesso.`, + }); + setSelectedCards([]); + } + + if (failed.length > 0) { + toast({ + title: "Alguns itens não foram movidos", + description: `${failed.length} itens precisam ser classificados primeiro.`, + variant: "destructive", + }); + } + }; + + const toggleSelectCard = (id: string) => { + setSelectedCards(prev => + prev.includes(id) ? prev.filter(c => c !== id) : [...prev, id] + ); + }; + + const handleCardClick = (e: React.MouseEvent, req: Requisicao) => { + if (e.shiftKey) { + e.preventDefault(); + toggleSelectCard(req.id); + } + }; + const handleDeny = (id: string) => { updateStatus(id, "negada"); toast({ @@ -141,12 +223,22 @@ export function KanbanBoard({ requisicoes }: KanbanBoardProps) { .filter((r) => r.status === col.status) .map((req) => { const setor = getSetorById(req.setorId); + const isSelected = selectedCards.includes(req.id); + const hasComprar = req.itens.some(i => i.tag === "comprar"); + const allSeparar = req.itens.length > 0 && req.itens.every(i => i.tag === "separar"); return (
handleCardClick(e, req)} + className={`bg-background p-3 rounded-lg border shadow-sm hover:shadow-md transition-all group relative cursor-pointer ${isSelected ? "ring-2 ring-primary border-primary" : ""}`} > + {isSelected && ( +
+ +
+ )} + {/* Topo: Setor e Data */}
@@ -195,17 +287,33 @@ export function KanbanBoard({ requisicoes }: KanbanBoardProps) { Detalhes - {/* Botões de Ação Condicionais */} - {(col.status === "nova" || + {/* Visual Indicators */} +
+ {hasComprar && ( +
+ +
+ )} + {allSeparar && ( +
+ +
+ )} +
+
+ + {/* Botões de Ação Condicionais */} + {(col.status === "nova" || col.status === "em_atendimento") && ( <>
- {selectedRequisicao && ( - setSelectedRequisicao(null)} - /> - )} + !open && setSelectedRequisicao(null)} + /> + + {/* Floating Action Bar for Batch Selection */} + {selectedCards.length > 0 && ( +
+ {selectedCards.length} selecionado(s) +
+ + +
+ )}
); } diff --git a/components/ordi/requisicao-modal.tsx b/components/ordi/requisicao-modal.tsx deleted file mode 100644 index 975c806..0000000 --- a/components/ordi/requisicao-modal.tsx +++ /dev/null @@ -1,271 +0,0 @@ -// app/components/ordi/requisicao-modal.tsx -"use client"; - -import { useState, useEffect } from "react"; -import { X, Users, Calendar, Package, Check, Loader2 } from "lucide-react"; -import { Button } from "@/components/ui/button"; -import { Input } from "@/components/ui/input"; -import { - type Requisicao, - type ItemRequisicao, - getFuncionarioById, - getSetorById, -} from "@/lib/mock-data"; -import { useRequisicoesStore } from "@/lib/requisicoes-store"; - -interface RequisicaoModalProps { - requisicao: Requisicao; - onClose: () => void; -} - -interface ModalItemState extends ItemRequisicao { - isApproved: boolean; - quantidadeAprovada: number; -} - -export function RequisicaoModal({ requisicao, onClose }: RequisicaoModalProps) { - // Usando a nova ação updateItens - const { updateItens } = useRequisicoesStore(); - const setor = getSetorById(requisicao.setorId); - - // Estado inicial dos itens - let initialItemsState = requisicao.itens.map((item) => ({ - ...item, - isApproved: true, - quantidadeAprovada: item.quantidade, - })); - - const [itemsState, setItemsState] = - useState(initialItemsState); - const [isLoading, setIsLoading] = useState(false); - const [showSuccess, setShowSuccess] = useState(false); - const [hasChanges, setHasChanges] = useState(false); - - // Verifica se há alterações nos itens - useEffect(() => { - const hasItemChanges = itemsState.some((item, index) => { - const originalItem = initialItemsState[index]; - return ( - item.isApproved !== originalItem.isApproved || - item.quantidadeAprovada !== originalItem.quantidadeAprovada - ); - }); - setHasChanges(hasItemChanges); - }, [itemsState, initialItemsState]); - - // Adiciona suporte à tecla ESC para fechar o modal - useEffect(() => { - const handleKeyDown = (event: KeyboardEvent) => { - if (event.key === "Escape") { - onClose(); - } - }; - - window.addEventListener("keydown", handleKeyDown); - return () => window.removeEventListener("keydown", handleKeyDown); - }, [onClose]); - - const handleQuantityChange = (index: number, value: string) => { - const newQuantity = parseInt(value, 10) || 0; - setItemsState((current) => - current.map((item, i) => - i === index ? { ...item, quantidadeAprovada: newQuantity } : item - ) - ); - }; - - const handleDoubleClick = (index: number) => { - setItemsState((current) => - current.map((item, i) => - i === index ? { ...item, isApproved: !item.isApproved } : item - ) - ); - }; - - // NOVA FUNÇÃO: Apenas confirma e salva os itens, sem mudar o status - const handleConfirmChanges = () => { - setIsLoading(true); - - // Simula um tempo de processamento - setTimeout(() => { - const finalItems = itemsState - .filter((item) => item.isApproved && item.quantidadeAprovada > 0) - .map(({ isApproved, quantidadeAprovada, ...rest }) => ({ - ...rest, - quantidade: quantidadeAprovada, - })); - - // Chama a nova ação do store para atualizar apenas os itens - updateItens(requisicao.id, finalItems); - - // Atualiza o estado inicial para refletir as alterações salvas - const newInitialState = itemsState.map((item) => ({ - ...item, - quantidade: item.quantidadeAprovada, - })); - initialItemsState = newInitialState; - - setIsLoading(false); - setShowSuccess(true); - - // Esconde a mensagem de sucesso após 3 segundos - setTimeout(() => { - setShowSuccess(false); - }, 3000); - }, 1000); - }; - - return ( -
-
-
- {/* Header */} -
-
-

- Detalhes da Requisição -

-

- #{requisicao.id.toUpperCase()} -

-
- -
- - {/* Content */} -
- {/* Informações Principais */} -
-
-
- - Setor que requisitou -
-

- {setor?.nome} -

-
-
-
- - Data da Requisição -
-

- {new Date(requisicao.dataCriacao).toLocaleString("pt-BR")} -

-
-
- - {/* Itens com Controle */} -
-
- -

- Aprovar Itens ({itemsState.filter((i) => i.isApproved).length}{" "} - de {requisicao.itens.length}) -

-
- {itemsState.map((item, index) => { - const itemFuncionario = getFuncionarioById( - item.funcionarioId || requisicao.funcionarioId - ); - return ( -
handleDoubleClick(index)} - > -
-
-

- {item.nome} -

-

- Solicitado por: {itemFuncionario?.nome} -

-
-
-
- - - handleQuantityChange(index, e.target.value) - } - disabled={!item.isApproved} - className="w-20 h-8 text-center" - /> -
-
-
-
- ); - })} -
- - {/* Observações gerais */} - {requisicao.observacoesGerais && ( -
-

- Observações gerais -

-
-

- {requisicao.observacoesGerais} -

-
-
- )} -
- - {/* Footer com o Botão Confirmar */} -
-
- -
- - {/* Mensagem de sucesso */} - {showSuccess && ( -
- Alterações salvas com sucesso! -
- )} -
-
-
- ); -} diff --git a/components/ordi/requisicao-sheet.tsx b/components/ordi/requisicao-sheet.tsx new file mode 100644 index 0000000..c3981c1 --- /dev/null +++ b/components/ordi/requisicao-sheet.tsx @@ -0,0 +1,301 @@ +"use client"; + +import { useState, useEffect } from "react"; +import { + X, + Users, + Calendar, + Package, + Check, + Loader2, + ShoppingCart, + Box, +} from "lucide-react"; +import { Button } from "@/components/ui/button"; +import { Input } from "@/components/ui/input"; +import { + Sheet, + SheetContent, + SheetHeader, + SheetTitle, + SheetDescription, + SheetFooter, + SheetClose, +} from "@/components/ui/sheet"; +import { + type Requisicao, + type ItemRequisicao, + type ItemTag, + getFuncionarioById, + getSetorById, +} from "@/lib/mock-data"; +import { useRequisicoesStore } from "@/lib/requisicoes-store"; +import { ToggleGroup, ToggleGroupItem } from "@/components/ui/toggle-group"; + +interface RequisicaoSheetProps { + requisicao: Requisicao | null; + open: boolean; + onOpenChange: (open: boolean) => void; +} + +interface SheetItemState extends ItemRequisicao { + isApproved: boolean; + quantidadeAprovada: number; +} + +export function RequisicaoSheet({ + requisicao, + open, + onOpenChange, +}: RequisicaoSheetProps) { + const { updateItens } = useRequisicoesStore(); + + const [itemsState, setItemsState] = useState([]); + const [isLoading, setIsLoading] = useState(false); + const [showSuccess, setShowSuccess] = useState(false); + const [hasChanges, setHasChanges] = useState(false); + const [initialItemsState, setInitialItemsState] = useState( + [] + ); + + const setor = requisicao ? getSetorById(requisicao.setorId) : undefined; + + // Initialize state when requisicao changes + useEffect(() => { + if (requisicao) { + const state = requisicao.itens.map((item) => ({ + ...item, + isApproved: true, + quantidadeAprovada: item.quantidade, + })); + setItemsState(state); + setInitialItemsState(state); + } + }, [requisicao]); + + // Check for changes + useEffect(() => { + if (!requisicao) return; + + const hasItemChanges = itemsState.some((item, index) => { + const originalItem = initialItemsState[index]; + if (!originalItem) return true; + + return ( + item.isApproved !== originalItem.isApproved || + item.quantidadeAprovada !== originalItem.quantidadeAprovada || + item.tag !== originalItem.tag + ); + }); + setHasChanges(hasItemChanges); + }, [itemsState, initialItemsState, requisicao]); + + const handleQuantityChange = (index: number, value: string) => { + const newQuantity = parseInt(value, 10) || 0; + setItemsState((current) => + current.map((item, i) => + i === index ? { ...item, quantidadeAprovada: newQuantity } : item + ) + ); + }; + + const handleTagChange = (index: number, value: ItemTag) => { + setItemsState((current) => + current.map((item, i) => (i === index ? { ...item, tag: value } : item)) + ); + }; + + const handleConfirmChanges = () => { + if (!requisicao) return; + setIsLoading(true); + + setTimeout(() => { + const finalItems = itemsState + .filter((item) => item.isApproved && item.quantidadeAprovada > 0) + .map(({ isApproved, quantidadeAprovada, ...rest }) => ({ + ...rest, + quantidade: quantidadeAprovada, + })); + + updateItens(requisicao.id, finalItems); + + const newInitialState = itemsState.map((item) => ({ + ...item, + quantidade: item.quantidadeAprovada, + })); + setInitialItemsState(newInitialState); + + setIsLoading(false); + setShowSuccess(true); + + setTimeout(() => { + setShowSuccess(false); + }, 3000); + }, 1000); + }; + + if (!requisicao) return null; + + return ( + + + + Detalhes da Requisição + #{requisicao.id.toUpperCase()} + + + {/* Scrollable Content */} +
+ {/* Main Info */} +
+
+
+ + Setor +
+

+ {setor?.nome} +

+
+
+
+ + Data +
+

+ {new Date(requisicao.dataCriacao).toLocaleString("pt-BR")} +

+
+
+ + {/* Items */} +
+
+ +

Itens ({itemsState.length})

+
+ + {itemsState.map((item, index) => { + const itemFuncionario = getFuncionarioById( + item.funcionarioId || requisicao.funcionarioId + ); + return ( +
+
+ {/* Item Header */} +
+
+

+ {item.nome} +

+

+ Solicitado por: {itemFuncionario?.nome} +

+
+
+ + + handleQuantityChange(index, e.target.value) + } + disabled={!item.isApproved} + className="w-16 h-8 text-center" + /> +
+
+ + {/* Tagging Section */} +
+ + Ação Necessária: + + handleTagChange(index, val as ItemTag || null)} + className="justify-start" + > + + + Separar + + + + Comprar + + +
+
+
+ ); + })} +
+ + {/* Observations */} + {requisicao.observacoesGerais && ( +
+

+ Observações gerais +

+
+

{requisicao.observacoesGerais}

+
+
+ )} +
+ + {/* Footer */} + + + {showSuccess && ( +
+ Alterações salvas com sucesso! +
+ )} +
+
+
+ ); +} diff --git a/components/ordi/requisicoes-list.tsx b/components/ordi/requisicoes-list.tsx index d2ba770..cf0ee3e 100644 --- a/components/ordi/requisicoes-list.tsx +++ b/components/ordi/requisicoes-list.tsx @@ -5,7 +5,7 @@ import { useState, useMemo } from "react"; import { Eye, ArrowRight, X } from "lucide-react"; import { Button } from "@/components/ui/button"; import { useRequisicoesStore } from "@/lib/requisicoes-store"; -import { RequisicaoModal } from "./requisicao-modal"; +import { RequisicaoSheet } from "./requisicao-sheet"; import { type Requisicao, type StatusRequisicao, @@ -268,13 +268,12 @@ export function RequisicoesList({ requisicoes }: RequisicoesListProps) { })}
- {/* Modal de detalhes (sem alterações) */} - {selectedRequisicao && ( - setSelectedRequisicao(null)} - /> - )} + {/* Sheet de detalhes */} + !open && setSelectedRequisicao(null)} + /> ); } diff --git a/lib/mock-data.ts b/lib/mock-data.ts index 77436dd..7955e00 100644 --- a/lib/mock-data.ts +++ b/lib/mock-data.ts @@ -95,6 +95,8 @@ export type StatusRequisicao = | "concluida" | "negada"; +export type ItemTag = "separar" | "comprar" | null; + // ALTERAÇÃO 1: Adicionado o campo opcional 'funcionarioId' à interface export interface ItemRequisicao { nome: string; @@ -102,6 +104,7 @@ export interface ItemRequisicao { observacoes?: string; // novo: id do funcionário que solicitou ESSE item funcionarioId?: string; + tag?: ItemTag; } export interface Requisicao {