Advertisement
Not a member of Pastebin yet?
Sign Up,
it unlocks many cool features!
- import React, { useState, useMemo } from 'react';
- import { Search, Filter, Eye, AlertCircle, CheckCircle, XCircle, Clock, Building, FileText, Settings } from 'lucide-react';
- const FarmakeepGrid = () => {
- const [selectedCell, setSelectedCell] = useState(null);
- const [searchTerm, setSearchTerm] = useState('');
- const [statusFilter, setStatusFilter] = useState('all');
- const [viewMode, setViewMode] = useState('normal');
- const [hoveredCell, setHoveredCell] = useState(null);
- // Dados simulados
- const establishments = [
- { id: 1, name: 'Farmácia Central', cnpj: '12.345.678/0001-90', city: 'São Paulo', region: 'SP' },
- { id: 2, name: 'Drogaria Saúde', cnpj: '98.765.432/0001-10', city: 'Rio de Janeiro', region: 'RJ' },
- { id: 3, name: 'Farmácia Popular', cnpj: '11.222.333/0001-44', city: 'Belo Horizonte', region: 'MG' },
- { id: 4, name: 'Medicenter', cnpj: '55.666.777/0001-88', city: 'Brasília', region: 'DF' },
- { id: 5, name: 'FarmaVida', cnpj: '33.444.555/0001-22', city: 'Salvador', region: 'BA' },
- { id: 6, name: 'DrugStore Plus', cnpj: '77.888.999/0001-66', city: 'Fortaleza', region: 'CE' },
- ];
- const documents = [
- { id: 1, name: 'Alvará Sanitário', type: 'sanitario', critical: true },
- { id: 2, name: 'Licença ANVISA', type: 'sanitario', critical: true },
- { id: 3, name: 'AFE', type: 'fiscal', critical: false },
- { id: 4, name: 'Responsável Técnico', type: 'tecnico', critical: true },
- { id: 5, name: 'CNPJ Ativo', type: 'fiscal', critical: false },
- { id: 6, name: 'Licença Municipal', type: 'municipal', critical: true },
- { id: 7, name: 'Certificado Digital', type: 'fiscal', critical: false },
- { id: 8, name: 'Plano de Gerenciamento', type: 'ambiental', critical: false },
- ];
- // Status dos documentos (simulado)
- const documentStatus = {
- '1-1': { status: 'regular', daysLeft: 120, lastUpdate: '2024-06-15' },
- '1-2': { status: 'atencao', daysLeft: 25, lastUpdate: '2024-06-10' },
- '1-3': { status: 'vencido', daysLeft: -5, lastUpdate: '2024-06-01' },
- '1-4': { status: 'regular', daysLeft: 90, lastUpdate: '2024-06-20' },
- '1-5': { status: 'protocolo', daysLeft: null, lastUpdate: '2024-06-25' },
- '1-6': { status: 'exigencia', daysLeft: 10, lastUpdate: '2024-06-22' },
- '1-7': { status: 'regular', daysLeft: 200, lastUpdate: '2024-06-18' },
- '1-8': { status: 'atencao', daysLeft: 30, lastUpdate: '2024-06-12' },
- '2-1': { status: 'atencao', daysLeft: 15, lastUpdate: '2024-06-14' },
- '2-2': { status: 'regular', daysLeft: 80, lastUpdate: '2024-06-16' },
- '2-3': { status: 'regular', daysLeft: 150, lastUpdate: '2024-06-20' },
- '2-4': { status: 'vencido', daysLeft: -10, lastUpdate: '2024-05-28' },
- '2-5': { status: 'regular', daysLeft: 300, lastUpdate: '2024-06-25' },
- '2-6': { status: 'atencao', daysLeft: 20, lastUpdate: '2024-06-15' },
- '2-7': { status: 'protocolo', daysLeft: null, lastUpdate: '2024-06-24' },
- '2-8': { status: 'regular', daysLeft: 100, lastUpdate: '2024-06-19' },
- '3-1': { status: 'regular', daysLeft: 180, lastUpdate: '2024-06-17' },
- '3-2': { status: 'regular', daysLeft: 95, lastUpdate: '2024-06-21' },
- '3-3': { status: 'vencido', daysLeft: -3, lastUpdate: '2024-06-02' },
- '3-4': { status: 'atencao', daysLeft: 28, lastUpdate: '2024-06-13' },
- '3-5': { status: 'regular', daysLeft: 250, lastUpdate: '2024-06-23' },
- '3-6': { status: 'exigencia', daysLeft: 7, lastUpdate: '2024-06-20' },
- '3-7': { status: 'regular', daysLeft: 160, lastUpdate: '2024-06-18' },
- '3-8': { status: 'regular', daysLeft: 75, lastUpdate: '2024-06-16' },
- '4-1': { status: 'atencao', daysLeft: 22, lastUpdate: '2024-06-11' },
- '4-2': { status: 'regular', daysLeft: 110, lastUpdate: '2024-06-19' },
- '4-3': { status: 'regular', daysLeft: 140, lastUpdate: '2024-06-22' },
- '4-4': { status: 'regular', daysLeft: 85, lastUpdate: '2024-06-17' },
- '4-5': { status: 'protocolo', daysLeft: null, lastUpdate: '2024-06-26' },
- '4-6': { status: 'vencido', daysLeft: -8, lastUpdate: '2024-05-30' },
- '4-7': { status: 'regular', daysLeft: 190, lastUpdate: '2024-06-21' },
- '4-8': { status: 'atencao', daysLeft: 35, lastUpdate: '2024-06-14' },
- '5-1': { status: 'regular', daysLeft: 70, lastUpdate: '2024-06-24' },
- '5-2': { status: 'atencao', daysLeft: 18, lastUpdate: '2024-06-12' },
- '5-3': { status: 'regular', daysLeft: 130, lastUpdate: '2024-06-20' },
- '5-4': { status: 'regular', daysLeft: 95, lastUpdate: '2024-06-18' },
- '5-5': { status: 'regular', daysLeft: 280, lastUpdate: '2024-06-25' },
- '5-6': { status: 'regular', daysLeft: 60, lastUpdate: '2024-06-15' },
- '5-7': { status: 'exigencia', daysLeft: 12, lastUpdate: '2024-06-22' },
- '5-8': { status: 'regular', daysLeft: 105, lastUpdate: '2024-06-19' },
- '6-1': { status: 'vencido', daysLeft: -15, lastUpdate: '2024-05-25' },
- '6-2': { status: 'regular', daysLeft: 125, lastUpdate: '2024-06-23' },
- '6-3': { status: 'atencao', daysLeft: 27, lastUpdate: '2024-06-13' },
- '6-4': { status: 'regular', daysLeft: 88, lastUpdate: '2024-06-17' },
- '6-5': { status: 'regular', daysLeft: 220, lastUpdate: '2024-06-24' },
- '6-6': { status: 'protocolo', daysLeft: null, lastUpdate: '2024-06-27' },
- '6-7': { status: 'regular', daysLeft: 175, lastUpdate: '2024-06-20' },
- '6-8': { status: 'regular', daysLeft: 65, lastUpdate: '2024-06-16' },
- };
- const getStatusInfo = (status, daysLeft) => {
- const configs = {
- regular: {
- color: 'bg-green-500',
- icon: CheckCircle,
- label: 'Regular',
- intensity: daysLeft > 60 ? 'bg-green-500' : 'bg-green-400'
- },
- atencao: {
- color: 'bg-orange-500',
- icon: AlertCircle,
- label: 'Precisa de Atenção',
- intensity: daysLeft < 20 ? 'bg-orange-600' : 'bg-orange-400'
- },
- vencido: {
- color: 'bg-red-500',
- icon: XCircle,
- label: 'Vencido',
- intensity: Math.abs(daysLeft) > 10 ? 'bg-red-600' : 'bg-red-500'
- },
- protocolo: {
- color: 'bg-blue-500',
- icon: FileText,
- label: 'Protocolo',
- intensity: 'bg-blue-500'
- },
- exigencia: {
- color: 'bg-purple-500',
- icon: Clock,
- label: 'Em Exigência',
- intensity: daysLeft < 10 ? 'bg-purple-600' : 'bg-purple-500'
- }
- };
- return configs[status] || configs.regular;
- };
- const filteredEstablishments = useMemo(() => {
- return establishments.filter(est =>
- est.name.toLowerCase().includes(searchTerm.toLowerCase()) ||
- est.cnpj.includes(searchTerm) ||
- est.city.toLowerCase().includes(searchTerm.toLowerCase())
- );
- }, [searchTerm]);
- const getStatusCounts = () => {
- const counts = { regular: 0, atencao: 0, vencido: 0, protocolo: 0, exigencia: 0 };
- Object.values(documentStatus).forEach(doc => {
- counts[doc.status]++;
- });
- return counts;
- };
- const statusCounts = getStatusCounts();
- const Tooltip = ({ cell, establishment, document }) => {
- if (!cell) return null;
- const statusInfo = getStatusInfo(cell.status, cell.daysLeft);
- const Icon = statusInfo.icon;
- return (
- <div className="absolute z-50 bg-gray-900 text-white p-3 rounded-lg shadow-xl border border-gray-700 min-w-64">
- <div className="flex items-center gap-2 mb-2">
- <Icon size={16} className="text-gray-300" />
- <span className="font-semibold">{statusInfo.label}</span>
- </div>
- <div className="space-y-1 text-sm">
- <div><strong>Estabelecimento:</strong> {establishment.name}</div>
- <div><strong>Documento:</strong> {document.name}</div>
- <div><strong>CNPJ:</strong> {establishment.cnpj}</div>
- <div><strong>Localização:</strong> {establishment.city}/{establishment.region}</div>
- {cell.daysLeft !== null && (
- <div><strong>Dias restantes:</strong>
- <span className={cell.daysLeft < 0 ? 'text-red-400' : cell.daysLeft < 30 ? 'text-orange-400' : 'text-green-400'}>
- {cell.daysLeft < 0 ? ` ${Math.abs(cell.daysLeft)} dias vencido` : ` ${cell.daysLeft} dias`}
- </span>
- </div>
- )}
- <div><strong>Última atualização:</strong> {cell.lastUpdate}</div>
- </div>
- </div>
- );
- };
- return (
- <div className="min-h-screen bg-gray-50 p-6">
- {/* Header */}
- <div className="mb-6">
- <div className="flex items-center gap-3 mb-4">
- <div className="w-10 h-10 bg-green-500 rounded-full flex items-center justify-center">
- <span className="text-white font-bold text-lg">F</span>
- </div>
- <h1 className="text-2xl font-bold text-gray-800">Farmakeep</h1>
- </div>
- {/* Status Cards */}
- <div className="grid grid-cols-5 gap-4 mb-6">
- <div className="bg-green-500 text-white p-4 rounded-lg text-center">
- <div className="text-2xl font-bold">{statusCounts.regular}</div>
- <div className="text-sm opacity-90">REGULAR</div>
- </div>
- <div className="bg-orange-500 text-white p-4 rounded-lg text-center">
- <div className="text-2xl font-bold">{statusCounts.atencao}</div>
- <div className="text-sm opacity-90">PRECISA DE ATENÇÃO</div>
- </div>
- <div className="bg-red-500 text-white p-4 rounded-lg text-center">
- <div className="text-2xl font-bold">{statusCounts.vencido}</div>
- <div className="text-sm opacity-90">VENCIDO</div>
- </div>
- <div className="bg-blue-500 text-white p-4 rounded-lg text-center">
- <div className="text-2xl font-bold">{statusCounts.protocolo}</div>
- <div className="text-sm opacity-90">PROTOCOLO</div>
- </div>
- <div className="bg-purple-500 text-white p-4 rounded-lg text-center">
- <div className="text-2xl font-bold">{statusCounts.exigencia}</div>
- <div className="text-sm opacity-90">EM EXIGÊNCIA</div>
- </div>
- </div>
- </div>
- {/* Controls */}
- <div className="bg-white rounded-lg shadow-sm border p-4 mb-6">
- <div className="flex flex-wrap gap-4 items-center justify-between">
- <div className="flex gap-4 items-center">
- <div className="relative">
- <Search className="absolute left-3 top-1/2 transform -translate-y-1/2 text-gray-400" size={16} />
- <input
- type="text"
- placeholder="Buscar estabelecimento, CNPJ ou cidade..."
- className="pl-10 pr-4 py-2 border border-gray-300 rounded-lg w-80"
- value={searchTerm}
- onChange={(e) => setSearchTerm(e.target.value)}
- />
- </div>
- <select
- className="px-4 py-2 border border-gray-300 rounded-lg"
- value={statusFilter}
- onChange={(e) => setStatusFilter(e.target.value)}
- >
- <option value="all">Todos os Status</option>
- <option value="vencido">Apenas Vencidos</option>
- <option value="atencao">Precisa Atenção</option>
- <option value="regular">Regulares</option>
- </select>
- </div>
- <div className="flex gap-2 items-center">
- <span className="text-sm text-gray-600">Visualização:</span>
- <select
- className="px-3 py-2 border border-gray-300 rounded-lg text-sm"
- value={viewMode}
- onChange={(e) => setViewMode(e.target.value)}
- >
- <option value="compact">Compacta</option>
- <option value="normal">Normal</option>
- <option value="detailed">Detalhada</option>
- </select>
- </div>
- </div>
- </div>
- {/* Grid */}
- <div className="bg-white rounded-lg shadow-sm border overflow-hidden">
- <div className="overflow-x-auto">
- <div className="min-w-max">
- {/* Header Row */}
- <div className="flex bg-gray-100 border-b sticky top-0 z-10">
- <div className="w-48 p-3 font-medium text-gray-700 border-r bg-gray-100">
- <div className="flex items-center gap-2">
- <Building size={16} />
- Estabelecimentos
- </div>
- </div>
- {documents.map((doc) => (
- <div key={doc.id} className="w-24 p-2 text-center border-r bg-gray-100">
- <div className="transform -rotate-45 origin-center text-xs font-medium text-gray-700 whitespace-nowrap">
- {doc.name}
- </div>
- {doc.critical && (
- <div className="mt-1 flex justify-center">
- <AlertCircle size={12} className="text-red-500" />
- </div>
- )}
- </div>
- ))}
- </div>
- {/* Data Rows */}
- {filteredEstablishments.map((establishment) => (
- <div key={establishment.id} className="flex border-b hover:bg-gray-50">
- <div className="w-48 p-3 border-r bg-white sticky left-0">
- <div className="font-medium text-gray-900">{establishment.name}</div>
- <div className="text-xs text-gray-500">{establishment.cnpj}</div>
- <div className="text-xs text-gray-500">{establishment.city}/{establishment.region}</div>
- </div>
- {documents.map((document) => {
- const cellKey = `${establishment.id}-${document.id}`;
- const cellData = documentStatus[cellKey];
- const statusInfo = getStatusInfo(cellData?.status || 'regular', cellData?.daysLeft);
- const Icon = statusInfo.icon;
- return (
- <div
- key={document.id}
- className="w-24 h-16 border-r relative cursor-pointer group"
- onMouseEnter={() => setHoveredCell({ cellData, establishment, document, cellKey })}
- onMouseLeave={() => setHoveredCell(null)}
- onClick={() => setSelectedCell({ cellData, establishment, document })}
- >
- <div className={`w-full h-full ${statusInfo.intensity} flex items-center justify-center transition-all duration-200 group-hover:scale-110 group-hover:shadow-lg`}>
- <Icon size={viewMode === 'compact' ? 12 : viewMode === 'detailed' ? 20 : 16} className="text-white" />
- {viewMode === 'detailed' && cellData?.daysLeft !== null && (
- <span className="absolute bottom-1 text-xs text-white font-bold">
- {cellData.daysLeft < 0 ? cellData.daysLeft : `+${cellData.daysLeft}`}
- </span>
- )}
- </div>
- {document.critical && (
- <div className="absolute -top-1 -right-1 w-3 h-3 bg-red-600 rounded-full border border-white"></div>
- )}
- </div>
- );
- })}
- </div>
- ))}
- </div>
- </div>
- </div>
- {/* Tooltip */}
- {hoveredCell && (
- <div className="fixed pointer-events-none z-50" style={{
- left: `${Math.min(window.innerWidth - 300, window.event?.clientX + 10)}px`,
- top: `${Math.max(10, window.event?.clientY - 50)}px`
- }}>
- <Tooltip
- cell={hoveredCell.cellData}
- establishment={hoveredCell.establishment}
- document={hoveredCell.document}
- />
- </div>
- )}
- {/* Side Panel */}
- {selectedCell && (
- <div className="fixed right-0 top-0 h-full w-80 bg-white shadow-2xl border-l z-40 transform transition-transform duration-300">
- <div className="p-6">
- <div className="flex items-center justify-between mb-4">
- <h3 className="text-lg font-semibold">Detalhes do Documento</h3>
- <button
- onClick={() => setSelectedCell(null)}
- className="text-gray-400 hover:text-gray-600"
- >
- <XCircle size={20} />
- </button>
- </div>
- <div className="space-y-4">
- <div>
- <label className="text-sm font-medium text-gray-600">Estabelecimento</label>
- <div className="mt-1 text-gray-900">{selectedCell.establishment.name}</div>
- </div>
- <div>
- <label className="text-sm font-medium text-gray-600">Documento</label>
- <div className="mt-1 text-gray-900">{selectedCell.document.name}</div>
- </div>
- <div>
- <label className="text-sm font-medium text-gray-600">Status</label>
- <div className="mt-1">
- <span className={`inline-flex items-center gap-2 px-3 py-1 rounded-full text-sm text-white ${getStatusInfo(selectedCell.cellData?.status, selectedCell.cellData?.daysLeft).color}`}>
- {React.createElement(getStatusInfo(selectedCell.cellData?.status, selectedCell.cellData?.daysLeft).icon, { size: 16 })}
- {getStatusInfo(selectedCell.cellData?.status, selectedCell.cellData?.daysLeft).label}
- </span>
- </div>
- </div>
- {selectedCell.cellData?.daysLeft !== null && (
- <div>
- <label className="text-sm font-medium text-gray-600">Situação</label>
- <div className="mt-1 text-gray-900">
- {selectedCell.cellData.daysLeft < 0
- ? `Vencido há ${Math.abs(selectedCell.cellData.daysLeft)} dias`
- : `${selectedCell.cellData.daysLeft} dias restantes`
- }
- </div>
- </div>
- )}
- <div>
- <label className="text-sm font-medium text-gray-600">Última Atualização</label>
- <div className="mt-1 text-gray-900">{selectedCell.cellData?.lastUpdate}</div>
- </div>
- <div className="pt-4 space-y-2">
- <button className="w-full bg-blue-600 text-white py-2 px-4 rounded-lg hover:bg-blue-700">
- Ver Documento
- </button>
- <button className="w-full bg-green-600 text-white py-2 px-4 rounded-lg hover:bg-green-700">
- Atualizar Status
- </button>
- <button className="w-full bg-gray-600 text-white py-2 px-4 rounded-lg hover:bg-gray-700">
- Histórico
- </button>
- </div>
- </div>
- </div>
- </div>
- )}
- </div>
- );
- };
- export default FarmakeepGrid;
Advertisement
Add Comment
Please, Sign In to add comment
Advertisement