Advertisement
lucasvinicius

FarmaKeeep - Dashboard de visualização

Jul 1st, 2025
219
0
Never
Not a member of Pastebin yet? Sign Up, it unlocks many cool features!
JavaScript 19.18 KB | Source Code | 0 0
  1. import React, { useState, useMemo } from 'react';
  2. import { Search, Filter, Eye, AlertCircle, CheckCircle, XCircle, Clock, Building, FileText, Settings } from 'lucide-react';
  3.  
  4. const FarmakeepGrid = () => {
  5.   const [selectedCell, setSelectedCell] = useState(null);
  6.   const [searchTerm, setSearchTerm] = useState('');
  7.   const [statusFilter, setStatusFilter] = useState('all');
  8.   const [viewMode, setViewMode] = useState('normal');
  9.   const [hoveredCell, setHoveredCell] = useState(null);
  10.  
  11.   // Dados simulados
  12.   const establishments = [
  13.     { id: 1, name: 'Farmácia Central', cnpj: '12.345.678/0001-90', city: 'São Paulo', region: 'SP' },
  14.     { id: 2, name: 'Drogaria Saúde', cnpj: '98.765.432/0001-10', city: 'Rio de Janeiro', region: 'RJ' },
  15.     { id: 3, name: 'Farmácia Popular', cnpj: '11.222.333/0001-44', city: 'Belo Horizonte', region: 'MG' },
  16.     { id: 4, name: 'Medicenter', cnpj: '55.666.777/0001-88', city: 'Brasília', region: 'DF' },
  17.     { id: 5, name: 'FarmaVida', cnpj: '33.444.555/0001-22', city: 'Salvador', region: 'BA' },
  18.     { id: 6, name: 'DrugStore Plus', cnpj: '77.888.999/0001-66', city: 'Fortaleza', region: 'CE' },
  19.   ];
  20.  
  21.   const documents = [
  22.     { id: 1, name: 'Alvará Sanitário', type: 'sanitario', critical: true },
  23.     { id: 2, name: 'Licença ANVISA', type: 'sanitario', critical: true },
  24.     { id: 3, name: 'AFE', type: 'fiscal', critical: false },
  25.     { id: 4, name: 'Responsável Técnico', type: 'tecnico', critical: true },
  26.     { id: 5, name: 'CNPJ Ativo', type: 'fiscal', critical: false },
  27.     { id: 6, name: 'Licença Municipal', type: 'municipal', critical: true },
  28.     { id: 7, name: 'Certificado Digital', type: 'fiscal', critical: false },
  29.     { id: 8, name: 'Plano de Gerenciamento', type: 'ambiental', critical: false },
  30.   ];
  31.  
  32.   // Status dos documentos (simulado)
  33.   const documentStatus = {
  34.     '1-1': { status: 'regular', daysLeft: 120, lastUpdate: '2024-06-15' },
  35.     '1-2': { status: 'atencao', daysLeft: 25, lastUpdate: '2024-06-10' },
  36.     '1-3': { status: 'vencido', daysLeft: -5, lastUpdate: '2024-06-01' },
  37.     '1-4': { status: 'regular', daysLeft: 90, lastUpdate: '2024-06-20' },
  38.     '1-5': { status: 'protocolo', daysLeft: null, lastUpdate: '2024-06-25' },
  39.     '1-6': { status: 'exigencia', daysLeft: 10, lastUpdate: '2024-06-22' },
  40.     '1-7': { status: 'regular', daysLeft: 200, lastUpdate: '2024-06-18' },
  41.     '1-8': { status: 'atencao', daysLeft: 30, lastUpdate: '2024-06-12' },
  42.    
  43.     '2-1': { status: 'atencao', daysLeft: 15, lastUpdate: '2024-06-14' },
  44.     '2-2': { status: 'regular', daysLeft: 80, lastUpdate: '2024-06-16' },
  45.     '2-3': { status: 'regular', daysLeft: 150, lastUpdate: '2024-06-20' },
  46.     '2-4': { status: 'vencido', daysLeft: -10, lastUpdate: '2024-05-28' },
  47.     '2-5': { status: 'regular', daysLeft: 300, lastUpdate: '2024-06-25' },
  48.     '2-6': { status: 'atencao', daysLeft: 20, lastUpdate: '2024-06-15' },
  49.     '2-7': { status: 'protocolo', daysLeft: null, lastUpdate: '2024-06-24' },
  50.     '2-8': { status: 'regular', daysLeft: 100, lastUpdate: '2024-06-19' },
  51.  
  52.     '3-1': { status: 'regular', daysLeft: 180, lastUpdate: '2024-06-17' },
  53.     '3-2': { status: 'regular', daysLeft: 95, lastUpdate: '2024-06-21' },
  54.     '3-3': { status: 'vencido', daysLeft: -3, lastUpdate: '2024-06-02' },
  55.     '3-4': { status: 'atencao', daysLeft: 28, lastUpdate: '2024-06-13' },
  56.     '3-5': { status: 'regular', daysLeft: 250, lastUpdate: '2024-06-23' },
  57.     '3-6': { status: 'exigencia', daysLeft: 7, lastUpdate: '2024-06-20' },
  58.     '3-7': { status: 'regular', daysLeft: 160, lastUpdate: '2024-06-18' },
  59.     '3-8': { status: 'regular', daysLeft: 75, lastUpdate: '2024-06-16' },
  60.  
  61.     '4-1': { status: 'atencao', daysLeft: 22, lastUpdate: '2024-06-11' },
  62.     '4-2': { status: 'regular', daysLeft: 110, lastUpdate: '2024-06-19' },
  63.     '4-3': { status: 'regular', daysLeft: 140, lastUpdate: '2024-06-22' },
  64.     '4-4': { status: 'regular', daysLeft: 85, lastUpdate: '2024-06-17' },
  65.     '4-5': { status: 'protocolo', daysLeft: null, lastUpdate: '2024-06-26' },
  66.     '4-6': { status: 'vencido', daysLeft: -8, lastUpdate: '2024-05-30' },
  67.     '4-7': { status: 'regular', daysLeft: 190, lastUpdate: '2024-06-21' },
  68.     '4-8': { status: 'atencao', daysLeft: 35, lastUpdate: '2024-06-14' },
  69.  
  70.     '5-1': { status: 'regular', daysLeft: 70, lastUpdate: '2024-06-24' },
  71.     '5-2': { status: 'atencao', daysLeft: 18, lastUpdate: '2024-06-12' },
  72.     '5-3': { status: 'regular', daysLeft: 130, lastUpdate: '2024-06-20' },
  73.     '5-4': { status: 'regular', daysLeft: 95, lastUpdate: '2024-06-18' },
  74.     '5-5': { status: 'regular', daysLeft: 280, lastUpdate: '2024-06-25' },
  75.     '5-6': { status: 'regular', daysLeft: 60, lastUpdate: '2024-06-15' },
  76.     '5-7': { status: 'exigencia', daysLeft: 12, lastUpdate: '2024-06-22' },
  77.     '5-8': { status: 'regular', daysLeft: 105, lastUpdate: '2024-06-19' },
  78.  
  79.     '6-1': { status: 'vencido', daysLeft: -15, lastUpdate: '2024-05-25' },
  80.     '6-2': { status: 'regular', daysLeft: 125, lastUpdate: '2024-06-23' },
  81.     '6-3': { status: 'atencao', daysLeft: 27, lastUpdate: '2024-06-13' },
  82.     '6-4': { status: 'regular', daysLeft: 88, lastUpdate: '2024-06-17' },
  83.     '6-5': { status: 'regular', daysLeft: 220, lastUpdate: '2024-06-24' },
  84.     '6-6': { status: 'protocolo', daysLeft: null, lastUpdate: '2024-06-27' },
  85.     '6-7': { status: 'regular', daysLeft: 175, lastUpdate: '2024-06-20' },
  86.     '6-8': { status: 'regular', daysLeft: 65, lastUpdate: '2024-06-16' },
  87.   };
  88.  
  89.   const getStatusInfo = (status, daysLeft) => {
  90.     const configs = {
  91.       regular: {
  92.         color: 'bg-green-500',
  93.         icon: CheckCircle,
  94.         label: 'Regular',
  95.         intensity: daysLeft > 60 ? 'bg-green-500' : 'bg-green-400'
  96.       },
  97.       atencao: {
  98.         color: 'bg-orange-500',
  99.         icon: AlertCircle,
  100.         label: 'Precisa de Atenção',
  101.         intensity: daysLeft < 20 ? 'bg-orange-600' : 'bg-orange-400'
  102.       },
  103.       vencido: {
  104.         color: 'bg-red-500',
  105.         icon: XCircle,
  106.         label: 'Vencido',
  107.         intensity: Math.abs(daysLeft) > 10 ? 'bg-red-600' : 'bg-red-500'
  108.       },
  109.       protocolo: {
  110.         color: 'bg-blue-500',
  111.         icon: FileText,
  112.         label: 'Protocolo',
  113.         intensity: 'bg-blue-500'
  114.       },
  115.       exigencia: {
  116.         color: 'bg-purple-500',
  117.         icon: Clock,
  118.         label: 'Em Exigência',
  119.         intensity: daysLeft < 10 ? 'bg-purple-600' : 'bg-purple-500'
  120.       }
  121.     };
  122.     return configs[status] || configs.regular;
  123.   };
  124.  
  125.   const filteredEstablishments = useMemo(() => {
  126.     return establishments.filter(est =>
  127.       est.name.toLowerCase().includes(searchTerm.toLowerCase()) ||
  128.       est.cnpj.includes(searchTerm) ||
  129.       est.city.toLowerCase().includes(searchTerm.toLowerCase())
  130.     );
  131.   }, [searchTerm]);
  132.  
  133.   const getStatusCounts = () => {
  134.     const counts = { regular: 0, atencao: 0, vencido: 0, protocolo: 0, exigencia: 0 };
  135.     Object.values(documentStatus).forEach(doc => {
  136.       counts[doc.status]++;
  137.     });
  138.     return counts;
  139.   };
  140.  
  141.   const statusCounts = getStatusCounts();
  142.  
  143.   const Tooltip = ({ cell, establishment, document }) => {
  144.     if (!cell) return null;
  145.    
  146.     const statusInfo = getStatusInfo(cell.status, cell.daysLeft);
  147.     const Icon = statusInfo.icon;
  148.    
  149.     return (
  150.       <div className="absolute z-50 bg-gray-900 text-white p-3 rounded-lg shadow-xl border border-gray-700 min-w-64">
  151.         <div className="flex items-center gap-2 mb-2">
  152.           <Icon size={16} className="text-gray-300" />
  153.           <span className="font-semibold">{statusInfo.label}</span>
  154.         </div>
  155.         <div className="space-y-1 text-sm">
  156.           <div><strong>Estabelecimento:</strong> {establishment.name}</div>
  157.           <div><strong>Documento:</strong> {document.name}</div>
  158.           <div><strong>CNPJ:</strong> {establishment.cnpj}</div>
  159.           <div><strong>Localização:</strong> {establishment.city}/{establishment.region}</div>
  160.           {cell.daysLeft !== null && (
  161.             <div><strong>Dias restantes:</strong>
  162.               <span className={cell.daysLeft < 0 ? 'text-red-400' : cell.daysLeft < 30 ? 'text-orange-400' : 'text-green-400'}>
  163.                 {cell.daysLeft < 0 ? ` ${Math.abs(cell.daysLeft)} dias vencido` : ` ${cell.daysLeft} dias`}
  164.               </span>
  165.             </div>
  166.           )}
  167.           <div><strong>Última atualização:</strong> {cell.lastUpdate}</div>
  168.         </div>
  169.       </div>
  170.     );
  171.   };
  172.  
  173.   return (
  174.     <div className="min-h-screen bg-gray-50 p-6">
  175.       {/* Header */}
  176.       <div className="mb-6">
  177.         <div className="flex items-center gap-3 mb-4">
  178.           <div className="w-10 h-10 bg-green-500 rounded-full flex items-center justify-center">
  179.             <span className="text-white font-bold text-lg">F</span>
  180.           </div>
  181.           <h1 className="text-2xl font-bold text-gray-800">Farmakeep</h1>
  182.         </div>
  183.        
  184.         {/* Status Cards */}
  185.         <div className="grid grid-cols-5 gap-4 mb-6">
  186.           <div className="bg-green-500 text-white p-4 rounded-lg text-center">
  187.             <div className="text-2xl font-bold">{statusCounts.regular}</div>
  188.             <div className="text-sm opacity-90">REGULAR</div>
  189.           </div>
  190.           <div className="bg-orange-500 text-white p-4 rounded-lg text-center">
  191.             <div className="text-2xl font-bold">{statusCounts.atencao}</div>
  192.             <div className="text-sm opacity-90">PRECISA DE ATENÇÃO</div>
  193.           </div>
  194.           <div className="bg-red-500 text-white p-4 rounded-lg text-center">
  195.             <div className="text-2xl font-bold">{statusCounts.vencido}</div>
  196.             <div className="text-sm opacity-90">VENCIDO</div>
  197.           </div>
  198.           <div className="bg-blue-500 text-white p-4 rounded-lg text-center">
  199.             <div className="text-2xl font-bold">{statusCounts.protocolo}</div>
  200.             <div className="text-sm opacity-90">PROTOCOLO</div>
  201.           </div>
  202.           <div className="bg-purple-500 text-white p-4 rounded-lg text-center">
  203.             <div className="text-2xl font-bold">{statusCounts.exigencia}</div>
  204.             <div className="text-sm opacity-90">EM EXIGÊNCIA</div>
  205.           </div>
  206.         </div>
  207.       </div>
  208.  
  209.       {/* Controls */}
  210.       <div className="bg-white rounded-lg shadow-sm border p-4 mb-6">
  211.         <div className="flex flex-wrap gap-4 items-center justify-between">
  212.           <div className="flex gap-4 items-center">
  213.             <div className="relative">
  214.               <Search className="absolute left-3 top-1/2 transform -translate-y-1/2 text-gray-400" size={16} />
  215.               <input
  216.                 type="text"
  217.                 placeholder="Buscar estabelecimento, CNPJ ou cidade..."
  218.                 className="pl-10 pr-4 py-2 border border-gray-300 rounded-lg w-80"
  219.                 value={searchTerm}
  220.                 onChange={(e) => setSearchTerm(e.target.value)}
  221.               />
  222.             </div>
  223.            
  224.             <select
  225.               className="px-4 py-2 border border-gray-300 rounded-lg"
  226.               value={statusFilter}
  227.               onChange={(e) => setStatusFilter(e.target.value)}
  228.             >
  229.               <option value="all">Todos os Status</option>
  230.               <option value="vencido">Apenas Vencidos</option>
  231.               <option value="atencao">Precisa Atenção</option>
  232.               <option value="regular">Regulares</option>
  233.             </select>
  234.           </div>
  235.  
  236.           <div className="flex gap-2 items-center">
  237.             <span className="text-sm text-gray-600">Visualização:</span>
  238.             <select
  239.               className="px-3 py-2 border border-gray-300 rounded-lg text-sm"
  240.               value={viewMode}
  241.               onChange={(e) => setViewMode(e.target.value)}
  242.             >
  243.               <option value="compact">Compacta</option>
  244.               <option value="normal">Normal</option>
  245.               <option value="detailed">Detalhada</option>
  246.             </select>
  247.           </div>
  248.         </div>
  249.       </div>
  250.  
  251.       {/* Grid */}
  252.       <div className="bg-white rounded-lg shadow-sm border overflow-hidden">
  253.         <div className="overflow-x-auto">
  254.           <div className="min-w-max">
  255.             {/* Header Row */}
  256.             <div className="flex bg-gray-100 border-b sticky top-0 z-10">
  257.               <div className="w-48 p-3 font-medium text-gray-700 border-r bg-gray-100">
  258.                 <div className="flex items-center gap-2">
  259.                   <Building size={16} />
  260.                   Estabelecimentos
  261.                 </div>
  262.               </div>
  263.               {documents.map((doc) => (
  264.                 <div key={doc.id} className="w-24 p-2 text-center border-r bg-gray-100">
  265.                   <div className="transform -rotate-45 origin-center text-xs font-medium text-gray-700 whitespace-nowrap">
  266.                     {doc.name}
  267.                   </div>
  268.                   {doc.critical && (
  269.                     <div className="mt-1 flex justify-center">
  270.                       <AlertCircle size={12} className="text-red-500" />
  271.                     </div>
  272.                   )}
  273.                 </div>
  274.               ))}
  275.             </div>
  276.  
  277.             {/* Data Rows */}
  278.             {filteredEstablishments.map((establishment) => (
  279.               <div key={establishment.id} className="flex border-b hover:bg-gray-50">
  280.                 <div className="w-48 p-3 border-r bg-white sticky left-0">
  281.                   <div className="font-medium text-gray-900">{establishment.name}</div>
  282.                   <div className="text-xs text-gray-500">{establishment.cnpj}</div>
  283.                   <div className="text-xs text-gray-500">{establishment.city}/{establishment.region}</div>
  284.                 </div>
  285.                
  286.                 {documents.map((document) => {
  287.                   const cellKey = `${establishment.id}-${document.id}`;
  288.                   const cellData = documentStatus[cellKey];
  289.                   const statusInfo = getStatusInfo(cellData?.status || 'regular', cellData?.daysLeft);
  290.                   const Icon = statusInfo.icon;
  291.                  
  292.                   return (
  293.                     <div
  294.                       key={document.id}
  295.                       className="w-24 h-16 border-r relative cursor-pointer group"
  296.                       onMouseEnter={() => setHoveredCell({ cellData, establishment, document, cellKey })}
  297.                       onMouseLeave={() => setHoveredCell(null)}
  298.                       onClick={() => setSelectedCell({ cellData, establishment, document })}
  299.                     >
  300.                       <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`}>
  301.                         <Icon size={viewMode === 'compact' ? 12 : viewMode === 'detailed' ? 20 : 16} className="text-white" />
  302.                         {viewMode === 'detailed' && cellData?.daysLeft !== null && (
  303.                           <span className="absolute bottom-1 text-xs text-white font-bold">
  304.                             {cellData.daysLeft < 0 ? cellData.daysLeft : `+${cellData.daysLeft}`}
  305.                           </span>
  306.                         )}
  307.                       </div>
  308.                      
  309.                       {document.critical && (
  310.                         <div className="absolute -top-1 -right-1 w-3 h-3 bg-red-600 rounded-full border border-white"></div>
  311.                       )}
  312.                     </div>
  313.                   );
  314.                 })}
  315.               </div>
  316.             ))}
  317.           </div>
  318.         </div>
  319.       </div>
  320.  
  321.       {/* Tooltip */}
  322.       {hoveredCell && (
  323.         <div className="fixed pointer-events-none z-50" style={{
  324.           left: `${Math.min(window.innerWidth - 300, window.event?.clientX + 10)}px`,
  325.           top: `${Math.max(10, window.event?.clientY - 50)}px`
  326.         }}>
  327.           <Tooltip
  328.             cell={hoveredCell.cellData}
  329.             establishment={hoveredCell.establishment}
  330.             document={hoveredCell.document}
  331.           />
  332.         </div>
  333.       )}
  334.  
  335.       {/* Side Panel */}
  336.       {selectedCell && (
  337.         <div className="fixed right-0 top-0 h-full w-80 bg-white shadow-2xl border-l z-40 transform transition-transform duration-300">
  338.           <div className="p-6">
  339.             <div className="flex items-center justify-between mb-4">
  340.               <h3 className="text-lg font-semibold">Detalhes do Documento</h3>
  341.               <button
  342.                 onClick={() => setSelectedCell(null)}
  343.                 className="text-gray-400 hover:text-gray-600"
  344.               >
  345.                 <XCircle size={20} />
  346.               </button>
  347.             </div>
  348.            
  349.             <div className="space-y-4">
  350.               <div>
  351.                 <label className="text-sm font-medium text-gray-600">Estabelecimento</label>
  352.                 <div className="mt-1 text-gray-900">{selectedCell.establishment.name}</div>
  353.               </div>
  354.              
  355.               <div>
  356.                 <label className="text-sm font-medium text-gray-600">Documento</label>
  357.                 <div className="mt-1 text-gray-900">{selectedCell.document.name}</div>
  358.               </div>
  359.              
  360.               <div>
  361.                 <label className="text-sm font-medium text-gray-600">Status</label>
  362.                 <div className="mt-1">
  363.                   <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}`}>
  364.                     {React.createElement(getStatusInfo(selectedCell.cellData?.status, selectedCell.cellData?.daysLeft).icon, { size: 16 })}
  365.                     {getStatusInfo(selectedCell.cellData?.status, selectedCell.cellData?.daysLeft).label}
  366.                   </span>
  367.                 </div>
  368.               </div>
  369.              
  370.               {selectedCell.cellData?.daysLeft !== null && (
  371.                 <div>
  372.                   <label className="text-sm font-medium text-gray-600">Situação</label>
  373.                   <div className="mt-1 text-gray-900">
  374.                     {selectedCell.cellData.daysLeft < 0
  375.                       ? `Vencido há ${Math.abs(selectedCell.cellData.daysLeft)} dias`
  376.                       : `${selectedCell.cellData.daysLeft} dias restantes`
  377.                     }
  378.                   </div>
  379.                 </div>
  380.               )}
  381.              
  382.               <div>
  383.                 <label className="text-sm font-medium text-gray-600">Última Atualização</label>
  384.                 <div className="mt-1 text-gray-900">{selectedCell.cellData?.lastUpdate}</div>
  385.               </div>
  386.              
  387.               <div className="pt-4 space-y-2">
  388.                 <button className="w-full bg-blue-600 text-white py-2 px-4 rounded-lg hover:bg-blue-700">
  389.                   Ver Documento
  390.                 </button>
  391.                 <button className="w-full bg-green-600 text-white py-2 px-4 rounded-lg hover:bg-green-700">
  392.                   Atualizar Status
  393.                 </button>
  394.                 <button className="w-full bg-gray-600 text-white py-2 px-4 rounded-lg hover:bg-gray-700">
  395.                   Histórico
  396.                 </button>
  397.               </div>
  398.             </div>
  399.           </div>
  400.         </div>
  401.       )}
  402.     </div>
  403.   );
  404. };
  405.  
  406. export default FarmakeepGrid;
Advertisement
Add Comment
Please, Sign In to add comment
Advertisement