chore(ops): migrate clube67 to newwhats.clube67.com directory
continuous-integration/webhook Falha no deploy de clube67_newwhats.local (VPS 4)
continuous-integration/webhook Falha no deploy de clube67_newwhats.local (VPS 4)
This commit is contained in:
@@ -0,0 +1,97 @@
|
||||
import React, { useState, useEffect } from 'react';
|
||||
import { MedLandingPage } from './LandingPage.tsx';
|
||||
import { MedLayout } from './MedLayout.tsx';
|
||||
import { MedDashboard } from './MedDashboard.tsx';
|
||||
import { MedicoBackend } from './services/medicoBackend.ts';
|
||||
|
||||
export type MedViewKey =
|
||||
| 'med-landing'
|
||||
| 'med-dashboard'
|
||||
| 'med-pacientes'
|
||||
| 'med-agenda'
|
||||
| 'med-prontuarios'
|
||||
| 'med-fidelidade'
|
||||
| 'med-financeiro'
|
||||
| 'medical-repasse';
|
||||
|
||||
const ROUTE_MAP: Record<string, MedViewKey> = {
|
||||
'/': 'med-landing',
|
||||
'/landingpage': 'med-landing',
|
||||
'/dashboard': 'med-dashboard',
|
||||
'/pacientes': 'med-pacientes',
|
||||
'/agenda': 'med-agenda',
|
||||
'/prontuarios': 'med-prontuarios',
|
||||
'/fidelidade': 'med-fidelidade',
|
||||
'/financeiro': 'med-financeiro',
|
||||
'/repasse': 'medical-repasse',
|
||||
};
|
||||
|
||||
const VIEW_TO_ROUTE: Record<MedViewKey, string> = {
|
||||
'med-landing': '/landingpage',
|
||||
'med-dashboard': '/dashboard',
|
||||
'med-pacientes': '/pacientes',
|
||||
'med-agenda': '/agenda',
|
||||
'med-prontuarios': '/prontuarios',
|
||||
'med-fidelidade': '/fidelidade',
|
||||
'med-financeiro': '/financeiro',
|
||||
'medical-repasse': '/repasse',
|
||||
};
|
||||
|
||||
function getHashPath(): string {
|
||||
const hash = window.location.hash;
|
||||
if (hash.startsWith('#/')) return hash.slice(1);
|
||||
return '/';
|
||||
}
|
||||
|
||||
function resolveViewFromUrl(): MedViewKey {
|
||||
const path = getHashPath();
|
||||
return ROUTE_MAP[path] ?? 'med-landing';
|
||||
}
|
||||
|
||||
function navigate(view: MedViewKey) {
|
||||
window.location.hash = VIEW_TO_ROUTE[view];
|
||||
}
|
||||
|
||||
const App: React.FC = () => {
|
||||
const [currentView, setCurrentView] = useState<MedViewKey>(resolveViewFromUrl);
|
||||
|
||||
useEffect(() => {
|
||||
const onHashChange = () => {
|
||||
setCurrentView(resolveViewFromUrl());
|
||||
};
|
||||
window.addEventListener('hashchange', onHashChange);
|
||||
return () => window.removeEventListener('hashchange', onHashChange);
|
||||
}, []);
|
||||
|
||||
const handleNavigate = (view: string) => {
|
||||
const key = view as MedViewKey;
|
||||
setCurrentView(key);
|
||||
navigate(key);
|
||||
};
|
||||
|
||||
const renderView = () => {
|
||||
switch (currentView) {
|
||||
case 'med-landing': return <MedLandingPage />;
|
||||
case 'med-dashboard': return <MedDashboard />;
|
||||
case 'med-pacientes': return <div className="text-white p-20 text-center font-black uppercase tracking-widest text-2xl">Módulo de Pacientes Médicos <br /><span className="text-cyan-400 text-sm">Em Desenvolvimento</span></div>;
|
||||
case 'med-agenda': return <div className="text-white p-20 text-center font-black uppercase tracking-widest text-2xl">Agenda Médica Premium <br /><span className="text-cyan-400 text-sm">Em Desenvolvimento</span></div>;
|
||||
case 'med-prontuarios': return <div className="text-white p-20 text-center font-black uppercase tracking-widest text-2xl">Sistema de Prontuários <br /><span className="text-cyan-400 text-sm">Em Desenvolvimento</span></div>;
|
||||
case 'med-fidelidade': return <div className="text-white p-20 text-center font-black uppercase tracking-widest text-2xl">Gestão de Fidelidade <br /><span className="text-cyan-400 text-sm">Em Desenvolvimento</span></div>;
|
||||
case 'med-financeiro': return <div className="text-white p-20 text-center font-black uppercase tracking-widest text-2xl">Financeiro & MRR <br /><span className="text-cyan-400 text-sm">Em Desenvolvimento</span></div>;
|
||||
case 'medical-repasse': return <div className="text-white p-20 text-center font-black uppercase tracking-widest text-2xl">Repasse Médico <br /><span className="text-cyan-400 text-sm">Em Desenvolvimento</span></div>;
|
||||
default: return <MedLandingPage />;
|
||||
}
|
||||
};
|
||||
|
||||
if (currentView === 'med-landing') {
|
||||
return <MedLandingPage />;
|
||||
}
|
||||
|
||||
return (
|
||||
<MedLayout currentView={currentView} onNavigate={handleNavigate}>
|
||||
{renderView()}
|
||||
</MedLayout>
|
||||
);
|
||||
};
|
||||
|
||||
export default App;
|
||||
@@ -0,0 +1,207 @@
|
||||
import React from 'react';
|
||||
import { Shield, Activity, Heart, Check, ArrowRight, Star, Clock, Users, Smartphone, Zap } from 'lucide-react';
|
||||
|
||||
export const MedLandingPage: React.FC = () => {
|
||||
return (
|
||||
<div className="min-h-screen bg-[#0f172a] text-white font-sans selection:bg-cyan-500 selection:text-white">
|
||||
{/* Navigation */}
|
||||
<nav className="fixed top-0 w-full z-50 bg-slate-900/50 backdrop-blur-xl border-b border-white/10">
|
||||
<div className="max-w-7xl mx-auto px-6 h-20 flex items-center justify-between">
|
||||
<div className="flex items-center gap-3">
|
||||
<div className="w-10 h-10 bg-gradient-to-tr from-cyan-500 to-blue-600 rounded-xl flex items-center justify-center shadow-lg shadow-cyan-500/20">
|
||||
<Activity className="text-white" size={24} />
|
||||
</div>
|
||||
<span className="text-xl font-black tracking-tighter uppercase italic">CRM MÉDICO <span className="text-cyan-400">FIDELIDADE</span></span>
|
||||
</div>
|
||||
<div className="hidden md:flex items-center gap-8 text-sm font-bold uppercase tracking-widest text-slate-400">
|
||||
<a href="#planos" className="hover:text-white transition-colors">Planos</a>
|
||||
<a href="#beneficios" className="hover:text-white transition-colors">Benefícios</a>
|
||||
<button
|
||||
onClick={() => window.location.hash = '/login'}
|
||||
className="hover:text-white transition-colors uppercase"
|
||||
>
|
||||
Acesso Restrito
|
||||
</button>
|
||||
<button
|
||||
onClick={() => window.location.hash = '/'}
|
||||
className="px-6 py-2.5 bg-white text-slate-900 rounded-full hover:bg-cyan-400 transition-all hover:-translate-y-1"
|
||||
>
|
||||
Voltar ao CRM Odonto
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</nav>
|
||||
|
||||
{/* Hero Section */}
|
||||
<section className="relative pt-40 pb-20 overflow-hidden">
|
||||
<div className="absolute top-0 left-1/2 -translate-x-1/2 w-full h-full pointer-events-none">
|
||||
<div className="absolute top-1/4 left-1/4 w-[500px] h-[500px] bg-cyan-500/10 rounded-full blur-[120px] animate-pulse"></div>
|
||||
<div className="absolute bottom-1/4 right-1/4 w-[400px] h-[400px] bg-blue-600/10 rounded-full blur-[120px] animate-pulse delay-700"></div>
|
||||
</div>
|
||||
|
||||
<div className="max-w-7xl mx-auto px-6 relative">
|
||||
<div className="max-w-3xl">
|
||||
<div className="inline-flex items-center gap-2 px-4 py-2 bg-white/5 border border-white/10 rounded-full text-xs font-black uppercase tracking-widest text-cyan-400 mb-8 animate-in fade-in slide-in-from-bottom-4 duration-700">
|
||||
<Zap size={14} /> Novo: Fidelidade Multimédica
|
||||
</div>
|
||||
<h1 className="text-6xl md:text-8xl font-black tracking-tighter leading-[0.9] text-white mb-8 animate-in fade-in slide-in-from-bottom-6 duration-1000">
|
||||
SAÚDE TOTAL, <br />
|
||||
<span className="text-transparent bg-clip-text bg-gradient-to-r from-cyan-400 via-blue-500 to-purple-600">SEM COMPROMISSO.</span>
|
||||
</h1>
|
||||
<p className="text-xl text-slate-400 max-w-xl leading-relaxed mb-12 animate-in fade-in slide-in-from-bottom-8 duration-1000 delay-200">
|
||||
O primeiro plano de fidelidade que integra medicina, odontologia e bem-estar em um único ecossistema. Fidelize sua saúde com tecnologia de ponta.
|
||||
</p>
|
||||
<div className="flex flex-wrap gap-4 animate-in fade-in slide-in-from-bottom-10 duration-1000 delay-500">
|
||||
<button className="px-10 py-5 bg-gradient-to-r from-cyan-500 to-blue-600 rounded-2xl font-black uppercase tracking-widest hover:scale-105 transition-all shadow-xl shadow-cyan-500/20 flex items-center gap-3">
|
||||
COMEÇAR AGORA <ArrowRight size={20} />
|
||||
</button>
|
||||
<button className="px-10 py-5 bg-white/5 hover:bg-white/10 border border-white/10 rounded-2xl font-black uppercase tracking-widest transition-all">
|
||||
VER PLANOS
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
{/* Plans Section */}
|
||||
<section id="planos" className="py-32 bg-slate-900/40 relative">
|
||||
<div className="max-w-7xl mx-auto px-6">
|
||||
<div className="text-center mb-20">
|
||||
<h2 className="text-4xl md:text-5xl font-black tracking-tighter mb-4">ESTRUTURA DE <span className="text-cyan-400">PLANOS</span></h2>
|
||||
<p className="text-slate-400 uppercase tracking-[0.3em] font-bold text-xs">Exclusivo: Combos que não aceitam desmembramento</p>
|
||||
</div>
|
||||
|
||||
<div className="grid md:grid-cols-2 gap-10 max-w-5xl mx-auto">
|
||||
{/* Base Plan */}
|
||||
<div className="group relative bg-white/5 border border-white/10 rounded-[3rem] p-12 hover:bg-white/[0.08] transition-all hover:-translate-y-2">
|
||||
<div className="absolute top-8 right-8 text-slate-700 font-black text-6xl group-hover:text-slate-600 transition-colors">01</div>
|
||||
<div className="mb-10">
|
||||
<span className="px-4 py-1.5 bg-cyan-500/20 text-cyan-400 text-[10px] font-black uppercase tracking-widest rounded-full">LINHA BASE (OBRIGATÓRIO)</span>
|
||||
<h3 className="text-4xl font-black tracking-tighter mt-4">MÉDICO + <br />ODONTO</h3>
|
||||
</div>
|
||||
<ul className="space-y-4 mb-12">
|
||||
{[
|
||||
'Consultas Médicas ilimitadas',
|
||||
'Limpeza e Prevenção Dental',
|
||||
'Urgências 24h',
|
||||
'Rede Credenciada Platinum',
|
||||
'Descontos em Medicamentos'
|
||||
].map((item, i) => (
|
||||
<li key={i} className="flex items-center gap-3 text-slate-300 font-medium">
|
||||
<div className="w-5 h-5 rounded-full bg-cyan-500/20 flex items-center justify-center text-cyan-400">
|
||||
<Check size={12} strokeWidth={4} />
|
||||
</div>
|
||||
{item}
|
||||
</li>
|
||||
))}
|
||||
</ul>
|
||||
<div className="flex items-end gap-2 mb-8">
|
||||
<span className="text-5xl font-black leading-none">R$ 149</span>
|
||||
<span className="text-slate-500 font-bold uppercase text-[10px] tracking-widest mb-1">/mês individual</span>
|
||||
</div>
|
||||
<button className="w-full py-5 bg-white text-slate-900 rounded-2xl font-black uppercase tracking-widest hover:bg-cyan-400 transition-all">
|
||||
SELECIONAR PLANO
|
||||
</button>
|
||||
</div>
|
||||
|
||||
{/* Premium Plan */}
|
||||
<div className="group relative bg-white/5 border-2 border-cyan-500/50 rounded-[3rem] p-12 overflow-hidden hover:bg-white/[0.08] transition-all hover:-translate-y-2">
|
||||
<div className="absolute -top-10 -right-10 w-40 h-40 bg-cyan-500/20 blur-[60px]"></div>
|
||||
<div className="absolute top-8 right-8 text-cyan-900/30 font-black text-6xl group-hover:text-cyan-900/50 transition-colors">02</div>
|
||||
|
||||
<div className="mb-10">
|
||||
<div className="flex items-center gap-2 mb-2">
|
||||
<span className="px-4 py-1.5 bg-gradient-to-r from-amber-400 to-orange-500 text-black text-[10px] font-black uppercase tracking-widest rounded-full">MAIS POPULAR</span>
|
||||
</div>
|
||||
<span className="px-4 py-1.5 bg-cyan-500/20 text-cyan-400 text-[10px] font-black uppercase tracking-widest rounded-full">LINHA PREMIUM</span>
|
||||
<h3 className="text-4xl font-black tracking-tighter mt-4">MÉDICO + ODONTO <br /><span className="text-cyan-400">+ PSICOLOGIA</span></h3>
|
||||
</div>
|
||||
<ul className="space-y-4 mb-12">
|
||||
{[
|
||||
'Tudo da Linha Base',
|
||||
'Suporte Psicológico 24h',
|
||||
'Sessões de Terapia Mensais',
|
||||
'Check-up Executivo Anual',
|
||||
'Prioridade na Agenda (VIP)'
|
||||
].map((item, i) => (
|
||||
<li key={i} className="flex items-center gap-3 text-slate-300 font-medium">
|
||||
<div className="w-5 h-5 rounded-full bg-cyan-500 flex items-center justify-center text-white">
|
||||
<Check size={12} strokeWidth={4} />
|
||||
</div>
|
||||
{item}
|
||||
</li>
|
||||
))}
|
||||
</ul>
|
||||
<div className="flex items-end gap-2 mb-8">
|
||||
<span className="text-5xl font-black leading-none">R$ 249</span>
|
||||
<span className="text-slate-500 font-bold uppercase text-[10px] tracking-widest mb-1">/mês individual</span>
|
||||
</div>
|
||||
<button className="w-full py-5 bg-gradient-to-r from-cyan-500 to-blue-600 text-white rounded-2xl font-black uppercase tracking-widest hover:scale-[1.02] transition-all shadow-lg shadow-cyan-500/20">
|
||||
ASSINAR PREMIUM
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
{/* Features (CRM) */}
|
||||
<section id="beneficios" className="py-32 px-6">
|
||||
<div className="max-w-7xl mx-auto">
|
||||
<div className="grid md:grid-cols-4 gap-8">
|
||||
{[
|
||||
{ icon: Users, title: 'TITULAR E DEP.', desc: 'Gestão completa familiar com carteirinha individual digital.' },
|
||||
{ icon: Shield, title: 'ESTABILIDADE', desc: 'Previsibilidade total para o médico e segurança para o paciente.' },
|
||||
{ icon: Smartphone, title: 'WALLET DIGITAL', desc: 'QR Code e histórico médico sempre no seu bolso.' },
|
||||
{ icon: Zap, title: 'SEM GLOSA', desc: 'Processamento automático e repasse médico instantâneo.' }
|
||||
].map((feat, i) => (
|
||||
<div key={i} className="p-8 bg-white/2 hover:bg-white/5 border border-white/5 rounded-3xl transition-all">
|
||||
<feat.icon className="text-cyan-400 mb-6" size={32} />
|
||||
<h4 className="text-lg font-black tracking-widest uppercase mb-4">{feat.title}</h4>
|
||||
<p className="text-slate-400 text-sm leading-relaxed">{feat.desc}</p>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
{/* Footer */}
|
||||
<footer className="py-20 border-t border-white/5 bg-slate-950">
|
||||
<div className="max-w-7xl mx-auto px-6 flex flex-col md:flex-row justify-between items-center gap-10">
|
||||
<div className="flex items-center gap-3">
|
||||
<div className="w-8 h-8 bg-cyan-500 rounded-lg flex items-center justify-center">
|
||||
<Activity className="text-white" size={18} />
|
||||
</div>
|
||||
<span className="font-black uppercase tracking-tighter">CRM MÉDICO</span>
|
||||
</div>
|
||||
<div className="text-slate-500 text-[10px] font-black uppercase tracking-[0.4em]">
|
||||
© 2026 SCOREODONTO HYBRID MEDICAL ENGINE. ALL RIGHTS RESERVED.
|
||||
</div>
|
||||
<div className="flex gap-6">
|
||||
<button
|
||||
onClick={() => window.location.hash = '/'}
|
||||
className="text-slate-400 hover:text-white transition-colors uppercase font-bold text-xs tracking-widest"
|
||||
>
|
||||
Sair para Odonto
|
||||
</button>
|
||||
<button
|
||||
onClick={() => window.location.hash = '/login'}
|
||||
className="text-slate-400 hover:text-white transition-colors uppercase font-bold text-xs tracking-widest"
|
||||
>
|
||||
Painel Admin
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</footer>
|
||||
|
||||
<style>{`
|
||||
@keyframes animate-in {
|
||||
from { opacity: 0; transform: translateY(20px); }
|
||||
to { opacity: 1; transform: translateY(0); }
|
||||
}
|
||||
.animate-in {
|
||||
animation: animate-in 0.8s ease-out forwards;
|
||||
}
|
||||
`}</style>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
@@ -0,0 +1,146 @@
|
||||
import React, { useEffect, useState } from 'react';
|
||||
import {
|
||||
Users, Calendar, DollarSign, Activity,
|
||||
ArrowUpRight, ArrowDownRight, Zap,
|
||||
TrendingUp, AlertCircle, ArrowRight
|
||||
} from 'lucide-react';
|
||||
import { MedicoBackend } from './services/medicoBackend.ts';
|
||||
import { MedicoNotif } from './services/medicoNotif.ts';
|
||||
import { MedPaciente, MedAgendamento, MedNotificacao } from './types.ts';
|
||||
|
||||
export const MedDashboard: React.FC = () => {
|
||||
const [pacientes, setPacientes] = useState<MedPaciente[]>([]);
|
||||
const [agendamentos, setAgendamentos] = useState<MedAgendamento[]>([]);
|
||||
const [notificacoes, setNotificacoes] = useState<MedNotificacao[]>([]);
|
||||
const [loading, setLoading] = useState(true);
|
||||
|
||||
useEffect(() => {
|
||||
const loadData = async () => {
|
||||
await MedicoNotif.generateIntelligentAlerts();
|
||||
const [p, a, n] = await Promise.all([
|
||||
MedicoBackend.getPacientes(),
|
||||
MedicoBackend.getAgendamentos(),
|
||||
MedicoBackend.getNotificacoes()
|
||||
]);
|
||||
setPacientes(p);
|
||||
setAgendamentos(a);
|
||||
setNotificacoes(n.filter(notif => !notif.lida));
|
||||
setLoading(false);
|
||||
};
|
||||
loadData();
|
||||
}, []);
|
||||
|
||||
const stats = [
|
||||
{ label: 'Pacientes Ativos', value: pacientes.length.toString(), change: '+12%', positive: true, icon: Users, color: 'cyan' },
|
||||
{ label: 'Consultas Hoje', value: agendamentos.filter(a => a.start.includes(new Date().toISOString().split('T')[0])).length.toString(), change: '+4', positive: true, icon: Calendar, color: 'blue' },
|
||||
{ label: 'Receita Est. (MRR)', value: 'R$ 84.200', change: '+8%', positive: true, icon: DollarSign, color: 'emerald' },
|
||||
{ label: 'Alertas Ativos', value: notificacoes.length.toString(), change: '', positive: true, icon: Activity, color: 'purple' },
|
||||
];
|
||||
|
||||
if (loading) {
|
||||
return <div className="p-20 text-center text-cyan-500 font-black uppercase tracking-[0.5em] animate-pulse">Iniciando Motor Médico...</div>;
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="space-y-10 animate-in fade-in duration-700">
|
||||
{/* Header */}
|
||||
<div>
|
||||
<h1 className="text-4xl font-black tracking-tighter text-white mb-2 uppercase italic">Painel <span className="text-cyan-400">Estratégico</span></h1>
|
||||
<p className="text-slate-500 font-bold uppercase tracking-widest text-xs">Visão geral do ecossistema médico e fidelidade</p>
|
||||
</div>
|
||||
|
||||
{/* Stats Grid */}
|
||||
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-4 gap-6">
|
||||
{stats.map((stat, i) => {
|
||||
const Icon = stat.icon;
|
||||
return (
|
||||
<div key={i} className="bg-white/5 border border-white/10 p-8 rounded-[2rem] hover:bg-white/[0.08] transition-all group">
|
||||
<div className="flex justify-between items-start mb-6">
|
||||
<div className={`p-4 rounded-2xl bg-${stat.color}-500/10 text-${stat.color}-400 group-hover:scale-110 transition-transform`}>
|
||||
<Icon size={24} />
|
||||
</div>
|
||||
<div className={`flex items-center gap-1 text-[10px] font-black uppercase tracking-widest ${stat.positive ? 'text-emerald-400' : 'text-rose-400'}`}>
|
||||
{stat.change && (stat.positive ? <ArrowUpRight size={14} /> : <ArrowDownRight size={14} />)}
|
||||
{stat.change}
|
||||
</div>
|
||||
</div>
|
||||
<div className="space-y-1">
|
||||
<span className="text-slate-500 text-[10px] font-black uppercase tracking-[0.2em]">{stat.label}</span>
|
||||
<div className="text-3xl font-black text-white tracking-tighter">{stat.value}</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
|
||||
{/* Main Content Grid */}
|
||||
<div className="grid grid-cols-1 lg:grid-cols-3 gap-8">
|
||||
{/* Upcoming Appointments */}
|
||||
<div className="lg:col-span-2 bg-white/5 border border-white/10 rounded-[2.5rem] overflow-hidden">
|
||||
<div className="p-8 border-b border-white/5 flex justify-between items-center">
|
||||
<h3 className="text-sm font-black uppercase tracking-[0.3em] text-white">Próximos Atendimentos</h3>
|
||||
<button className="text-[10px] font-black text-cyan-400 hover:text-cyan-300 uppercase tracking-widest transition-colors">Ver Agenda Completa</button>
|
||||
</div>
|
||||
<div className="p-4">
|
||||
{agendamentos.length > 0 ? agendamentos.slice(0, 5).map((item, i) => (
|
||||
<div key={i} className="flex items-center justify-between p-4 hover:bg-white/5 rounded-2xl transition-all cursor-pointer group">
|
||||
<div className="flex items-center gap-4">
|
||||
<div className="w-12 h-12 rounded-xl bg-slate-800 flex flex-col items-center justify-center border border-white/5">
|
||||
<span className="text-[10px] font-black text-slate-500 leading-none">HRS</span>
|
||||
<span className="text-sm font-black text-white">{new Date(item.start).toLocaleTimeString([], { hour: '2-digit', minute: '2-digit' })}</span>
|
||||
</div>
|
||||
<div>
|
||||
<div className="text-sm font-black text-white uppercase tracking-tight group-hover:text-cyan-400 transition-colors">{item.pacienteNome}</div>
|
||||
<div className="text-[10px] text-slate-500 font-bold uppercase tracking-widest">{item.tipo}</div>
|
||||
</div>
|
||||
</div>
|
||||
<div className="flex items-center gap-4">
|
||||
<span className={`px-3 py-1 rounded-full text-[9px] font-black uppercase tracking-widest ${item.status === 'Confirmado' ? 'bg-emerald-500/10 text-emerald-400' : 'bg-slate-800 text-slate-400'
|
||||
}`}>
|
||||
{item.status}
|
||||
</span>
|
||||
<button className="p-2 text-slate-600 hover:text-white transition-colors">
|
||||
<ArrowUpRight size={18} />
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
)) : (
|
||||
<div className="p-10 text-center text-slate-500 font-bold uppercase tracking-widest text-xs">Nenhum agendamento para hoje</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Intelligent Insights */}
|
||||
<div className="bg-gradient-to-b from-cyan-600/20 to-transparent border border-cyan-500/20 rounded-[2.5rem] p-8 relative overflow-hidden">
|
||||
<div className="absolute top-0 right-0 w-32 h-32 bg-cyan-500/20 blur-[60px]"></div>
|
||||
<div className="relative">
|
||||
<div className="w-12 h-12 bg-cyan-500 rounded-2xl flex items-center justify-center text-white mb-8 shadow-lg shadow-cyan-500/20">
|
||||
<Zap size={24} />
|
||||
</div>
|
||||
<h3 className="text-xl font-black text-white tracking-tighter mb-4 italic uppercase">Inteligência <br />Médica</h3>
|
||||
<div className="space-y-6">
|
||||
{notificacoes.length > 0 ? notificacoes.slice(0, 2).map((notif, i) => (
|
||||
<div key={i} className="p-5 bg-black/20 rounded-2xl border border-white/5">
|
||||
<div className="flex items-center gap-2 mb-2">
|
||||
<AlertCircle size={14} className={notif.priority === 'high' ? 'text-rose-400' : 'text-amber-400'} />
|
||||
<span className={`text-[10px] font-black uppercase tracking-widest ${notif.priority === 'high' ? 'text-rose-400' : 'text-amber-400'}`}>
|
||||
{notif.titulo}
|
||||
</span>
|
||||
</div>
|
||||
<p className="text-xs text-slate-300 leading-relaxed font-medium">{notif.mensagem}</p>
|
||||
</div>
|
||||
)) : (
|
||||
<div className="p-5 bg-black/20 rounded-2xl border border-white/5 text-center">
|
||||
<p className="text-[10px] text-slate-500 font-black uppercase tracking-widest">Sem alertas pendentes</p>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
<button className="w-full mt-10 py-5 bg-white text-slate-900 rounded-2xl font-black uppercase tracking-widest hover:bg-cyan-400 transition-all flex items-center justify-center gap-3">
|
||||
Ver Insights <ArrowRight size={18} />
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
@@ -0,0 +1,87 @@
|
||||
import React, { useState } from 'react';
|
||||
import { MedSidebar } from './MedSidebar.tsx';
|
||||
import { Bell, Search, Settings, HelpCircle } from 'lucide-react';
|
||||
|
||||
interface MedLayoutProps {
|
||||
children: React.ReactNode;
|
||||
currentView: string;
|
||||
onNavigate: (view: any) => void;
|
||||
}
|
||||
|
||||
export const MedLayout: React.FC<MedLayoutProps> = ({ children, currentView, onNavigate }) => {
|
||||
return (
|
||||
<div className="flex min-h-screen bg-[#020617] text-slate-200 overflow-hidden font-sans">
|
||||
{/* Sidebar */}
|
||||
<MedSidebar activeTab={currentView} setActiveTab={onNavigate} />
|
||||
|
||||
{/* Main Content Area */}
|
||||
<div className="flex-1 ml-72 flex flex-col h-screen overflow-hidden">
|
||||
{/* Premium Top Bar */}
|
||||
<header className="h-24 px-10 flex items-center justify-between border-b border-white/5 bg-[#020617]/50 backdrop-blur-xl sticky top-0 z-40">
|
||||
<div className="flex items-center gap-6 flex-1 max-w-xl">
|
||||
<div className="relative group w-full">
|
||||
<Search className="absolute left-4 top-1/2 -translate-y-1/2 text-slate-500 group-focus-within:text-cyan-400 transition-colors" size={18} />
|
||||
<input
|
||||
type="text"
|
||||
placeholder="BUSCAR PACIENTES, AGENDAMENTOS OU GUIAS..."
|
||||
className="w-full bg-white/5 border border-white/10 rounded-2xl py-3.5 pl-12 pr-6 text-xs font-bold uppercase tracking-widest focus:outline-none focus:border-cyan-500/50 focus:bg-white/[0.08] transition-all"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="flex items-center gap-6">
|
||||
<div className="flex items-center gap-2">
|
||||
{[
|
||||
{ icon: Bell, alert: true },
|
||||
{ icon: HelpCircle, alert: false },
|
||||
{ icon: Settings, alert: false }
|
||||
].map((item, i) => (
|
||||
<button key={i} className="w-12 h-12 rounded-2xl bg-white/5 border border-white/10 flex items-center justify-center hover:bg-white/10 transition-all relative">
|
||||
<item.icon size={20} className="text-slate-400" />
|
||||
{item.alert && <div className="absolute top-3 right-3 w-2.5 h-2.5 bg-cyan-500 border-2 border-[#020617] rounded-full"></div>}
|
||||
</button>
|
||||
))}
|
||||
</div>
|
||||
|
||||
<div className="h-10 w-px bg-white/10 mx-2"></div>
|
||||
|
||||
<div className="flex items-center gap-3 pl-2">
|
||||
<div className="text-right">
|
||||
<span className="text-[10px] font-black text-cyan-500 block leading-none uppercase tracking-widest">Acesso Premium</span>
|
||||
<span className="text-xs font-black text-white uppercase tracking-tighter">Portal Médico Cassems</span>
|
||||
</div>
|
||||
<div className="w-12 h-12 rounded-full bg-gradient-to-tr from-cyan-500 to-blue-600 p-[2px] shadow-lg shadow-cyan-500/20">
|
||||
<div className="w-full h-full rounded-full bg-slate-900 flex items-center justify-center text-xs font-black text-white">
|
||||
MED
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</header>
|
||||
|
||||
{/* Dashboard/View Container */}
|
||||
<main className="flex-1 overflow-y-auto p-10 custom-scrollbar">
|
||||
<div className="max-w-7xl mx-auto pb-20">
|
||||
{children}
|
||||
</div>
|
||||
</main>
|
||||
</div>
|
||||
|
||||
<style>{`
|
||||
.custom-scrollbar::-webkit-scrollbar {
|
||||
width: 6px;
|
||||
}
|
||||
.custom-scrollbar::-webkit-scrollbar-track {
|
||||
background: transparent;
|
||||
}
|
||||
.custom-scrollbar::-webkit-scrollbar-thumb {
|
||||
background: rgba(255, 255, 255, 0.05);
|
||||
border-radius: 10px;
|
||||
}
|
||||
.custom-scrollbar::-webkit-scrollbar-thumb:hover {
|
||||
background: rgba(255, 255, 255, 0.1);
|
||||
}
|
||||
`}</style>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
@@ -0,0 +1,106 @@
|
||||
import React from 'react';
|
||||
import {
|
||||
LayoutDashboard, Users, Calendar, ClipboardList,
|
||||
Activity, Shield, DollarSign, LogOut, Settings,
|
||||
ChevronRight, Zap, Heart, Bell
|
||||
} from 'lucide-react';
|
||||
|
||||
interface MedSidebarProps {
|
||||
activeTab: string;
|
||||
setActiveTab: (tab: string) => void;
|
||||
}
|
||||
|
||||
export const MedSidebar: React.FC<MedSidebarProps> = ({ activeTab, setActiveTab }) => {
|
||||
const menuItems = [
|
||||
{ id: 'med-dashboard', label: 'Painel Geral', icon: LayoutDashboard },
|
||||
{ id: 'med-pacientes', label: 'Pacientes', icon: Users },
|
||||
{ id: 'med-agenda', label: 'Agenda Médica', icon: Calendar },
|
||||
{ id: 'med-prontuarios', label: 'Prontuários', icon: ClipboardList },
|
||||
{ id: 'med-fidelidade', label: 'Planos Fidelidade', icon: Shield },
|
||||
{ id: 'med-financeiro', label: 'Financeiro / MRR', icon: DollarSign },
|
||||
{ id: 'medical-repasse', label: 'Repasse Médico', icon: Zap },
|
||||
];
|
||||
|
||||
const handleLogout = () => {
|
||||
if (window.confirm('Deseja realmente sair do portal médico?')) {
|
||||
window.location.hash = '/medico/landingpage';
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="w-72 bg-[#0f172a] h-screen flex flex-col border-r border-white/5 fixed left-0 top-0 z-50 overflow-hidden">
|
||||
{/* Logo Section */}
|
||||
<div className="p-8">
|
||||
<div className="flex items-center gap-3 group cursor-pointer" onClick={() => setActiveTab('med-dashboard')}>
|
||||
<div className="w-10 h-10 bg-gradient-to-tr from-cyan-500 to-blue-600 rounded-xl flex items-center justify-center shadow-lg shadow-cyan-500/20 group-hover:scale-110 transition-transform">
|
||||
<Activity className="text-white" size={24} />
|
||||
</div>
|
||||
<div>
|
||||
<span className="text-lg font-black tracking-tighter text-white block leading-none italic">CRM <span className="text-cyan-400">MÉDICO</span></span>
|
||||
<span className="text-[10px] text-slate-500 font-bold uppercase tracking-widest">Premium Engine</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Navigation */}
|
||||
<nav className="flex-1 px-4 space-y-2 overflow-y-auto py-4">
|
||||
<div className="px-4 mb-4">
|
||||
<span className="text-[10px] text-slate-600 font-black uppercase tracking-[0.3em]">Navegação Principal</span>
|
||||
</div>
|
||||
{menuItems.map((item) => {
|
||||
const Icon = item.icon;
|
||||
const isActive = activeTab === item.id;
|
||||
return (
|
||||
<button
|
||||
key={item.id}
|
||||
onClick={() => setActiveTab(item.id)}
|
||||
className={`w-full flex items-center justify-between px-4 py-3.5 rounded-xl transition-all group ${isActive
|
||||
? 'bg-gradient-to-r from-cyan-500/10 to-transparent border-l-4 border-cyan-500 text-white'
|
||||
: 'text-slate-400 hover:text-white hover:bg-white/5'
|
||||
}`}
|
||||
>
|
||||
<div className="flex items-center gap-3">
|
||||
<Icon size={20} className={isActive ? 'text-cyan-400' : 'group-hover:text-cyan-400 transition-colors'} />
|
||||
<span className="text-xs font-bold uppercase tracking-wider">{item.label}</span>
|
||||
</div>
|
||||
{isActive && <ChevronRight size={14} className="text-cyan-500" />}
|
||||
</button>
|
||||
);
|
||||
})}
|
||||
</nav>
|
||||
|
||||
{/* Bottom Section / User Profile */}
|
||||
<div className="p-6 bg-slate-900/40 border-t border-white/5">
|
||||
<div className="flex items-center gap-4 mb-6 px-2">
|
||||
<div className="w-10 h-10 rounded-full bg-gradient-to-br from-slate-700 to-slate-800 border border-white/10 flex items-center justify-center text-cyan-400 font-black text-xs">
|
||||
DR
|
||||
</div>
|
||||
<div className="flex-1 min-w-0">
|
||||
<span className="text-xs font-black text-white block truncate uppercase tracking-tighter">Dr. Profissional</span>
|
||||
<span className="text-[10px] text-cyan-500/80 font-bold uppercase tracking-widest">Médico Diretor</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="space-y-2">
|
||||
<button className="w-full flex items-center gap-3 px-4 py-3 text-slate-400 hover:text-white hover:bg-white/5 rounded-xl transition-all text-xs font-bold uppercase tracking-widest">
|
||||
<Settings size={18} />
|
||||
Configurações
|
||||
</button>
|
||||
<button
|
||||
onClick={handleLogout}
|
||||
className="w-full flex items-center gap-3 px-4 py-3 text-rose-500 hover:bg-rose-500/10 rounded-xl transition-all text-xs font-bold uppercase tracking-widest"
|
||||
>
|
||||
<LogOut size={18} />
|
||||
Sair do Portal
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Status Indicator */}
|
||||
<div className="px-8 py-4 bg-slate-950/50 flex items-center gap-3">
|
||||
<div className="w-2 h-2 bg-cyan-500 rounded-full animate-pulse shadow-[0_0_8px_rgba(34,211,238,0.5)]"></div>
|
||||
<span className="text-[10px] text-slate-500 font-black uppercase tracking-[0.2em]">Sincronizado</span>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
@@ -0,0 +1,21 @@
|
||||
@import "tailwindcss";
|
||||
|
||||
:root {
|
||||
font-family: Inter, system-ui, Avenir, Helvetica, Arial, sans-serif;
|
||||
line-height: 1.5;
|
||||
font-weight: 400;
|
||||
color-scheme: dark;
|
||||
}
|
||||
|
||||
body {
|
||||
margin: 0;
|
||||
display: flex;
|
||||
place-items: center;
|
||||
min-width: 320px;
|
||||
min-height: 100vh;
|
||||
background-color: #020617;
|
||||
}
|
||||
|
||||
#root {
|
||||
width: 100%;
|
||||
}
|
||||
@@ -0,0 +1,15 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="pt-BR">
|
||||
|
||||
<head>
|
||||
<meta charset="UTF-8" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||
<title>CRM Médico | Fidelidade Multimédica</title>
|
||||
</head>
|
||||
|
||||
<body>
|
||||
<div id="root"></div>
|
||||
<script type="module" src="/index.tsx"></script>
|
||||
</body>
|
||||
|
||||
</html>
|
||||
@@ -0,0 +1,14 @@
|
||||
import React from 'react';
|
||||
import { createRoot } from 'react-dom/client';
|
||||
import App from './App.tsx';
|
||||
import './index.css';
|
||||
|
||||
const container = document.getElementById('root');
|
||||
if (container) {
|
||||
const root = createRoot(container);
|
||||
root.render(
|
||||
<React.StrictMode>
|
||||
<App />
|
||||
</React.StrictMode>
|
||||
);
|
||||
}
|
||||
+2543
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,29 @@
|
||||
{
|
||||
"name": "medico-crm",
|
||||
"private": true,
|
||||
"version": "1.0.0",
|
||||
"type": "module",
|
||||
"scripts": {
|
||||
"dev": "vite --port 3001",
|
||||
"build": "vite build",
|
||||
"preview": "vite preview"
|
||||
},
|
||||
"dependencies": {
|
||||
"lucide-react": "^0.563.0",
|
||||
"react": "^19.2.4",
|
||||
"react-dom": "^19.2.4"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@tailwindcss/postcss": "^4.2.0",
|
||||
"@tailwindcss/vite": "^4.2.0",
|
||||
"@types/node": "^22.14.0",
|
||||
"@types/react": "^19.0.0",
|
||||
"@types/react-dom": "^19.0.0",
|
||||
"@vitejs/plugin-react": "^5.0.0",
|
||||
"autoprefixer": "^10.4.24",
|
||||
"postcss": "^8.5.6",
|
||||
"tailwindcss": "^4.2.0",
|
||||
"typescript": "~5.8.2",
|
||||
"vite": "^6.2.0"
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,154 @@
|
||||
import {
|
||||
MedPaciente,
|
||||
MedProfissional,
|
||||
MedAgendamento,
|
||||
MedProntuario,
|
||||
MedFinanceiro,
|
||||
MedNotificacao
|
||||
} from '../types.ts';
|
||||
|
||||
const API_BASE_URL = '/api/med'; // New namespace for medical API
|
||||
|
||||
export class MedicoBackend {
|
||||
// --- STORAGE KEYS ---
|
||||
private static KEYS = {
|
||||
PACIENTES: 'med_pacientes',
|
||||
PROFISSIONAIS: 'med_profissionais',
|
||||
AGENDAMENTOS: 'med_agendamentos',
|
||||
PRONTUARIOS: 'med_prontuarios',
|
||||
FINANCEIRO: 'med_financeiro',
|
||||
NOTIFICACOES: 'med_notificacoes',
|
||||
AUTH: 'med_auth_token'
|
||||
};
|
||||
|
||||
// --- HELPERS ---
|
||||
private static async request<T>(path: string, options: RequestInit = {}): Promise<T> {
|
||||
const token = localStorage.getItem(this.KEYS.AUTH);
|
||||
const headers = {
|
||||
'Content-Type': 'application/json',
|
||||
...(token ? { 'Authorization': `Bearer ${token}` } : {}),
|
||||
...options.headers,
|
||||
};
|
||||
|
||||
try {
|
||||
const response = await fetch(`${API_BASE_URL}${path}`, { ...options, headers });
|
||||
if (!response.ok) throw new Error(`API Error: ${response.statusText}`);
|
||||
return await response.json();
|
||||
} catch (err) {
|
||||
console.warn(`API unavailable at ${path}, falling back to local storage.`, err);
|
||||
throw err; // Let the caller decide how to handle fallback
|
||||
}
|
||||
}
|
||||
|
||||
// --- PACIENTES ---
|
||||
static async getPacientes(): Promise<MedPaciente[]> {
|
||||
try {
|
||||
return await this.request<MedPaciente[]>('/pacientes');
|
||||
} catch {
|
||||
const stored = localStorage.getItem(this.KEYS.PACIENTES);
|
||||
return stored ? JSON.parse(stored) : [];
|
||||
}
|
||||
}
|
||||
|
||||
static async savePaciente(paciente: MedPaciente): Promise<void> {
|
||||
try {
|
||||
await this.request('/pacientes', {
|
||||
method: 'POST',
|
||||
body: JSON.stringify(paciente)
|
||||
});
|
||||
} catch {
|
||||
const pacientes = await this.getPacientes();
|
||||
const index = pacientes.findIndex(p => p.id === paciente.id);
|
||||
if (index >= 0) pacientes[index] = paciente;
|
||||
else pacientes.push(paciente);
|
||||
localStorage.setItem(this.KEYS.PACIENTES, JSON.stringify(pacientes));
|
||||
}
|
||||
}
|
||||
|
||||
// --- AGENDA / PROFISSIONAIS ---
|
||||
static async getProfissionais(): Promise<MedProfissional[]> {
|
||||
try {
|
||||
return await this.request<MedProfissional[]>('/profissionais');
|
||||
} catch {
|
||||
const stored = localStorage.getItem(this.KEYS.PROFISSIONAIS);
|
||||
if (stored) return JSON.parse(stored);
|
||||
// Mock initial data if empty
|
||||
const mock: MedProfissional[] = [
|
||||
{ id: '1', nome: 'Dr. Ricardo Santos', crm: '12345-MS', especialidade: 'Cardiologia', email: 'ricardo@med.com', telefone: '67 9999-9999', ativo: true, corAgenda: '#22d3ee' },
|
||||
{ id: '2', nome: 'Dra. Ana Oliveira', crm: '67890-MS', especialidade: 'Pediatria', email: 'ana@med.com', telefone: '67 8888-8888', ativo: true, corAgenda: '#f43f5e' }
|
||||
];
|
||||
localStorage.setItem(this.KEYS.PROFISSIONAIS, JSON.stringify(mock));
|
||||
return mock;
|
||||
}
|
||||
}
|
||||
|
||||
static async getAgendamentos(): Promise<MedAgendamento[]> {
|
||||
try {
|
||||
return await this.request<MedAgendamento[]>('/agendamentos');
|
||||
} catch {
|
||||
const stored = localStorage.getItem(this.KEYS.AGENDAMENTOS);
|
||||
return stored ? JSON.parse(stored) : [];
|
||||
}
|
||||
}
|
||||
|
||||
static async saveAgendamento(agenda: MedAgendamento): Promise<void> {
|
||||
try {
|
||||
await this.request('/agendamentos', {
|
||||
method: 'POST',
|
||||
body: JSON.stringify(agenda)
|
||||
});
|
||||
} catch {
|
||||
const data = await this.getAgendamentos();
|
||||
const index = data.findIndex(a => a.id === agenda.id);
|
||||
if (index >= 0) data[index] = agenda;
|
||||
else data.push(agenda);
|
||||
localStorage.setItem(this.KEYS.AGENDAMENTOS, JSON.stringify(data));
|
||||
}
|
||||
}
|
||||
|
||||
// --- FINANCEIRO ---
|
||||
static async getFinanceiro(): Promise<MedFinanceiro[]> {
|
||||
try {
|
||||
return await this.request<MedFinanceiro[]>('/financeiro');
|
||||
} catch {
|
||||
const stored = localStorage.getItem(this.KEYS.FINANCEIRO);
|
||||
return stored ? JSON.parse(stored) : [];
|
||||
}
|
||||
}
|
||||
|
||||
// --- NOTIFICACOES ---
|
||||
static async getNotificacoes(): Promise<MedNotificacao[]> {
|
||||
try {
|
||||
return await this.request<MedNotificacao[]>('/notificacoes');
|
||||
} catch {
|
||||
const stored = localStorage.getItem(this.KEYS.NOTIFICACOES);
|
||||
return stored ? JSON.parse(stored) : [];
|
||||
}
|
||||
}
|
||||
|
||||
static async markNotificationRead(id: string): Promise<void> {
|
||||
try {
|
||||
await this.request(`/notificacoes/${id}/read`, { method: 'PUT' });
|
||||
} catch {
|
||||
const notifs = await this.getNotificacoes();
|
||||
const index = notifs.findIndex(n => n.id === id);
|
||||
if (index >= 0) {
|
||||
notifs[index].lida = true;
|
||||
localStorage.setItem(this.KEYS.NOTIFICACOES, JSON.stringify(notifs));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// --- AUTH ---
|
||||
static isAuthenticated(): boolean {
|
||||
return !!localStorage.getItem(this.KEYS.AUTH);
|
||||
}
|
||||
|
||||
static login(token: string) {
|
||||
localStorage.setItem(this.KEYS.AUTH, token);
|
||||
}
|
||||
|
||||
static logout() {
|
||||
localStorage.removeItem(this.KEYS.AUTH);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,65 @@
|
||||
import { MedNotificacao, MedAgendamento, MedFinanceiro } from '../types.ts';
|
||||
import { MedicoBackend } from './medicoBackend.ts';
|
||||
|
||||
/**
|
||||
* Intelligent Notification Engine for Medical Module
|
||||
*/
|
||||
export class MedicoNotif {
|
||||
static async generateIntelligentAlerts(): Promise<void> {
|
||||
const agendamentos = await MedicoBackend.getAgendamentos();
|
||||
const financeiro = await MedicoBackend.getFinanceiro();
|
||||
const existingNotifs = await MedicoBackend.getNotificacoes();
|
||||
|
||||
const newNotifs: MedNotificacao[] = [];
|
||||
const now = new Date();
|
||||
|
||||
// 1. Check for upcoming appointments (next 24h)
|
||||
agendamentos.forEach(ag => {
|
||||
const agDate = new Date(ag.start);
|
||||
const diffHours = (agDate.getTime() - now.getTime()) / (1000 * 60 * 60);
|
||||
|
||||
if (diffHours > 0 && diffHours < 24 && ag.status === 'Agendado') {
|
||||
const title = 'Confirmação de Agenda';
|
||||
const msg = `Paciente ${ag.pacienteNome} tem agendamento amanhã às ${agDate.toLocaleTimeString([], { hour: '2-digit', minute: '2-digit' })}`;
|
||||
|
||||
if (!this.exists(existingNotifs, title, msg)) {
|
||||
newNotifs.push(this.create(title, msg, 'agenda', 'medium'));
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
// 2. Check for overdue payments
|
||||
financeiro.forEach(fin => {
|
||||
const venc = new Date(fin.dataVencimento);
|
||||
if (fin.status === 'Pendente' && venc < now) {
|
||||
const title = 'Pagamento em Atraso';
|
||||
const msg = `Mensalidade do paciente com ID ${fin.pacienteId} está atrasada desde ${venc.toLocaleDateString()}.`;
|
||||
|
||||
if (!this.exists(existingNotifs, title, msg)) {
|
||||
newNotifs.push(this.create(title, msg, 'financeiro', 'high'));
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
if (newNotifs.length > 0) {
|
||||
const allNotifs = [...newNotifs, ...existingNotifs];
|
||||
localStorage.setItem('med_notificacoes', JSON.stringify(allNotifs));
|
||||
}
|
||||
}
|
||||
|
||||
private static create(titulo: string, mensagem: string, tipo: MedNotificacao['tipo'], priority: MedNotificacao['priority']): MedNotificacao {
|
||||
return {
|
||||
id: Math.random().toString(36).substr(2, 9),
|
||||
titulo,
|
||||
mensagem,
|
||||
tipo,
|
||||
priority,
|
||||
data: new Date().toISOString(),
|
||||
lida: false
|
||||
};
|
||||
}
|
||||
|
||||
private static exists(list: MedNotificacao[], title: string, msg: string): boolean {
|
||||
return list.some(n => n.titulo === title && n.mensagem === msg);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,28 @@
|
||||
{
|
||||
"compilerOptions": {
|
||||
"target": "ESNext",
|
||||
"useDefineForClassFields": true,
|
||||
"lib": [
|
||||
"DOM",
|
||||
"DOM.Iterable",
|
||||
"ESNext"
|
||||
],
|
||||
"allowJs": false,
|
||||
"skipLibCheck": true,
|
||||
"esModuleInterop": false,
|
||||
"allowSyntheticDefaultImports": true,
|
||||
"strict": true,
|
||||
"forceConsistentCasingInFileNames": true,
|
||||
"module": "ESNext",
|
||||
"moduleResolution": "Bundler",
|
||||
"resolveJsonModule": true,
|
||||
"isolatedModules": true,
|
||||
"noEmit": true,
|
||||
"jsx": "react-jsx",
|
||||
"allowImportingTsExtensions": true
|
||||
},
|
||||
"include": [
|
||||
"**/*.ts",
|
||||
"**/*.tsx"
|
||||
]
|
||||
}
|
||||
@@ -0,0 +1,91 @@
|
||||
/**
|
||||
* MEDICAL MODULE TYPES
|
||||
* Standalone engine for the Medical CRM + Loyalty system
|
||||
*/
|
||||
|
||||
export interface MedPaciente {
|
||||
id: string;
|
||||
nome: string;
|
||||
cpf: string;
|
||||
telefone: string;
|
||||
email: string;
|
||||
dataNascimento: string;
|
||||
planoTipo: 'Base' | 'Premium'; // Médico + Odonto vs Médico + Odonto + Psico
|
||||
statusFidelidade: 'Ativo' | 'Inativo' | 'Inadimplente';
|
||||
ultimoAtendimento?: string;
|
||||
}
|
||||
|
||||
export interface MedProfissional {
|
||||
id: string;
|
||||
nome: string;
|
||||
crm: string;
|
||||
especialidade: string;
|
||||
email: string;
|
||||
telefone: string;
|
||||
ativo: boolean;
|
||||
horarios?: string;
|
||||
corAgenda: string;
|
||||
}
|
||||
|
||||
export interface MedAgendamento {
|
||||
id: string;
|
||||
pacienteId: string;
|
||||
pacienteNome: string;
|
||||
profissionalId: string;
|
||||
start: string; // ISO String
|
||||
end: string; // ISO String
|
||||
status: 'Agendado' | 'Confirmado' | 'Em Atendimento' | 'Finalizado' | 'Cancelado' | 'Falta';
|
||||
tipo: 'Consulta' | 'Retorno' | 'Exame' | 'Procedimento';
|
||||
observacoes?: string;
|
||||
cor?: string;
|
||||
}
|
||||
|
||||
export interface MedProntuario {
|
||||
id: string;
|
||||
pacienteId: string;
|
||||
data: string;
|
||||
profissionalId: string;
|
||||
anamnese: string;
|
||||
prescricao?: string;
|
||||
examesSolicitados?: string[];
|
||||
cid?: string[];
|
||||
anexos?: string[];
|
||||
}
|
||||
|
||||
export interface MedFinanceiro {
|
||||
id: string;
|
||||
pacienteId: string;
|
||||
descricao: string;
|
||||
valor: number;
|
||||
dataVencimento: string;
|
||||
dataPagamento?: string;
|
||||
status: 'Pendente' | 'Pago' | 'Atrasado';
|
||||
metodo: 'Boleto' | 'Pix' | 'Cartão' | 'Dinheiro';
|
||||
recorrencia: boolean; // Para o motor de fidelidade
|
||||
}
|
||||
|
||||
export interface MedFidelidadePlano {
|
||||
id: string;
|
||||
nome: string;
|
||||
valorMensal: number;
|
||||
inclusoOdonto: boolean; // Sempre true na base
|
||||
inclusoPsicologia: boolean; // True apenas no premium
|
||||
limiteConsultasMes: number;
|
||||
descontoMedicamentos: number;
|
||||
}
|
||||
|
||||
export interface MedNotificacao {
|
||||
id: string;
|
||||
titulo: string;
|
||||
mensagem: string;
|
||||
tipo: 'alerta' | 'financeiro' | 'agenda' | 'lead';
|
||||
data: string;
|
||||
lida: boolean;
|
||||
priority: 'low' | 'medium' | 'high';
|
||||
}
|
||||
|
||||
export interface MedConfig {
|
||||
apiKey?: string;
|
||||
hospitalName: string;
|
||||
unidadeId: string;
|
||||
}
|
||||
@@ -0,0 +1,13 @@
|
||||
import { defineConfig } from 'vite';
|
||||
import react from '@vitejs/plugin-react';
|
||||
import tailwindcss from '@tailwindcss/vite';
|
||||
|
||||
export default defineConfig({
|
||||
plugins: [
|
||||
react(),
|
||||
tailwindcss(),
|
||||
],
|
||||
server: {
|
||||
port: 3001,
|
||||
}
|
||||
});
|
||||
Reference in New Issue
Block a user