sem clerk

This commit is contained in:
2026-03-14 21:24:23 -03:00
parent e126b0d3f9
commit 13d18d3fc3
11 changed files with 279 additions and 531 deletions

123
package-lock.json generated
View File

@@ -13,6 +13,7 @@
"@types/uuid": "^10.0.0", "@types/uuid": "^10.0.0",
"@vercel/speed-insights": "^1.3.1", "@vercel/speed-insights": "^1.3.1",
"axios": "^1.13.2", "axios": "^1.13.2",
"bcryptjs": "^3.0.3",
"clsx": "^2.1.1", "clsx": "^2.1.1",
"cors": "^2.8.5", "cors": "^2.8.5",
"date-fns": "^4.1.0", "date-fns": "^4.1.0",
@@ -20,6 +21,7 @@
"dotenv": "^17.2.3", "dotenv": "^17.2.3",
"enhanced-resolve": "^5.18.4", "enhanced-resolve": "^5.18.4",
"express": "^5.2.1", "express": "^5.2.1",
"jsonwebtoken": "^9.0.3",
"lucide-react": "^0.562.0", "lucide-react": "^0.562.0",
"mongodb": "^7.0.0", "mongodb": "^7.0.0",
"mongoose": "^9.1.5", "mongoose": "^9.1.5",
@@ -4964,6 +4966,15 @@
"baseline-browser-mapping": "dist/cli.js" "baseline-browser-mapping": "dist/cli.js"
} }
}, },
"node_modules/bcryptjs": {
"version": "3.0.3",
"resolved": "https://registry.npmjs.org/bcryptjs/-/bcryptjs-3.0.3.tgz",
"integrity": "sha512-GlF5wPWnSa/X5LKM1o0wz0suXIINz1iHRLvTS+sLyi7XPbe5ycmYI3DlZqVGZZtDgl4DmasFg7gOB3JYbphV5g==",
"license": "BSD-3-Clause",
"bin": {
"bcrypt": "bin/bcrypt"
}
},
"node_modules/binary-extensions": { "node_modules/binary-extensions": {
"version": "2.3.0", "version": "2.3.0",
"dev": true, "dev": true,
@@ -5070,6 +5081,12 @@
"node": ">=20.19.0" "node": ">=20.19.0"
} }
}, },
"node_modules/buffer-equal-constant-time": {
"version": "1.0.1",
"resolved": "https://registry.npmjs.org/buffer-equal-constant-time/-/buffer-equal-constant-time-1.0.1.tgz",
"integrity": "sha512-zRpUiDwd/xk6ADqPMATG8vc9VPrkck7T07OIx0gnjmJAnHnTVXNQG3vfvWNuiZIkwu9KrKdA1iJKfsfTVxE6NA==",
"license": "BSD-3-Clause"
},
"node_modules/buffer-from": { "node_modules/buffer-from": {
"version": "1.1.2", "version": "1.1.2",
"license": "MIT" "license": "MIT"
@@ -5795,6 +5812,15 @@
"node": ">= 0.4" "node": ">= 0.4"
} }
}, },
"node_modules/ecdsa-sig-formatter": {
"version": "1.0.11",
"resolved": "https://registry.npmjs.org/ecdsa-sig-formatter/-/ecdsa-sig-formatter-1.0.11.tgz",
"integrity": "sha512-nagl3RYrbNv6kQkeJIpt6NJZy8twLB/2vtz6yN9Z4vRKHN4/QZJIEbqohALSgwKdnksuY3k5Addp5lg8sVoVcQ==",
"license": "Apache-2.0",
"dependencies": {
"safe-buffer": "^5.0.1"
}
},
"node_modules/edge-runtime": { "node_modules/edge-runtime": {
"version": "2.5.9", "version": "2.5.9",
"resolved": "https://registry.npmjs.org/edge-runtime/-/edge-runtime-2.5.9.tgz", "resolved": "https://registry.npmjs.org/edge-runtime/-/edge-runtime-2.5.9.tgz",
@@ -7785,6 +7811,61 @@
"node": ">=0.10.0" "node": ">=0.10.0"
} }
}, },
"node_modules/jsonwebtoken": {
"version": "9.0.3",
"resolved": "https://registry.npmjs.org/jsonwebtoken/-/jsonwebtoken-9.0.3.tgz",
"integrity": "sha512-MT/xP0CrubFRNLNKvxJ2BYfy53Zkm++5bX9dtuPbqAeQpTVe0MQTFhao8+Cp//EmJp244xt6Drw/GVEGCUj40g==",
"license": "MIT",
"dependencies": {
"jws": "^4.0.1",
"lodash.includes": "^4.3.0",
"lodash.isboolean": "^3.0.3",
"lodash.isinteger": "^4.0.4",
"lodash.isnumber": "^3.0.3",
"lodash.isplainobject": "^4.0.6",
"lodash.isstring": "^4.0.1",
"lodash.once": "^4.0.0",
"ms": "^2.1.1",
"semver": "^7.5.4"
},
"engines": {
"node": ">=12",
"npm": ">=6"
}
},
"node_modules/jsonwebtoken/node_modules/semver": {
"version": "7.7.4",
"resolved": "https://registry.npmjs.org/semver/-/semver-7.7.4.tgz",
"integrity": "sha512-vFKC2IEtQnVhpT78h1Yp8wzwrf8CM+MzKMHGJZfBtzhZNycRFnXsHk6E5TxIkkMsgNS7mdX3AGB7x2QM2di4lA==",
"license": "ISC",
"bin": {
"semver": "bin/semver.js"
},
"engines": {
"node": ">=10"
}
},
"node_modules/jwa": {
"version": "2.0.1",
"resolved": "https://registry.npmjs.org/jwa/-/jwa-2.0.1.tgz",
"integrity": "sha512-hRF04fqJIP8Abbkq5NKGN0Bbr3JxlQ+qhZufXVr0DvujKy93ZCbXZMHDL4EOtodSbCWxOqR8MS1tXA5hwqCXDg==",
"license": "MIT",
"dependencies": {
"buffer-equal-constant-time": "^1.0.1",
"ecdsa-sig-formatter": "1.0.11",
"safe-buffer": "^5.0.1"
}
},
"node_modules/jws": {
"version": "4.0.1",
"resolved": "https://registry.npmjs.org/jws/-/jws-4.0.1.tgz",
"integrity": "sha512-EKI/M/yqPncGUUh44xz0PxSidXFr/+r0pA70+gIYhjv+et7yxM+s29Y+VGDkovRofQem0fs7Uvf4+YmAdyRduA==",
"license": "MIT",
"dependencies": {
"jwa": "^2.0.1",
"safe-buffer": "^5.0.1"
}
},
"node_modules/kareem": { "node_modules/kareem": {
"version": "3.0.0", "version": "3.0.0",
"license": "Apache-2.0", "license": "Apache-2.0",
@@ -8102,11 +8183,53 @@
"dev": true, "dev": true,
"license": "MIT" "license": "MIT"
}, },
"node_modules/lodash.includes": {
"version": "4.3.0",
"resolved": "https://registry.npmjs.org/lodash.includes/-/lodash.includes-4.3.0.tgz",
"integrity": "sha512-W3Bx6mdkRTGtlJISOvVD/lbqjTlPPUDTMnlXZFnVwi9NKJ6tiAk6LVdlhZMm17VZisqhKcgzpO5Wz91PCt5b0w==",
"license": "MIT"
},
"node_modules/lodash.isboolean": {
"version": "3.0.3",
"resolved": "https://registry.npmjs.org/lodash.isboolean/-/lodash.isboolean-3.0.3.tgz",
"integrity": "sha512-Bz5mupy2SVbPHURB98VAcw+aHh4vRV5IPNhILUCsOzRmsTmSQ17jIuqopAentWoehktxGd9e/hbIXq980/1QJg==",
"license": "MIT"
},
"node_modules/lodash.isinteger": {
"version": "4.0.4",
"resolved": "https://registry.npmjs.org/lodash.isinteger/-/lodash.isinteger-4.0.4.tgz",
"integrity": "sha512-DBwtEWN2caHQ9/imiNeEA5ys1JoRtRfY3d7V9wkqtbycnAmTvRRmbHKDV4a0EYc678/dia0jrte4tjYwVBaZUA==",
"license": "MIT"
},
"node_modules/lodash.isnumber": {
"version": "3.0.3",
"resolved": "https://registry.npmjs.org/lodash.isnumber/-/lodash.isnumber-3.0.3.tgz",
"integrity": "sha512-QYqzpfwO3/CWf3XP+Z+tkQsfaLL/EnUlXWVkIk5FUPc4sBdTehEqZONuyRt2P67PXAk+NXmTBcc97zw9t1FQrw==",
"license": "MIT"
},
"node_modules/lodash.isplainobject": {
"version": "4.0.6",
"resolved": "https://registry.npmjs.org/lodash.isplainobject/-/lodash.isplainobject-4.0.6.tgz",
"integrity": "sha512-oSXzaWypCMHkPC3NvBEaPHf0KsA5mvPrOPgQWDsbg8n7orZ290M0BmC/jgRZ4vcJ6DTAhjrsSYgdsW/F+MFOBA==",
"license": "MIT"
},
"node_modules/lodash.isstring": {
"version": "4.0.1",
"resolved": "https://registry.npmjs.org/lodash.isstring/-/lodash.isstring-4.0.1.tgz",
"integrity": "sha512-0wJxfxH1wgO3GrbuP+dTTk7op+6L41QCXbGINEmD+ny/G/eCqGzxyCsh7159S+mgDDcoarnBw6PC1PS5+wUGgw==",
"license": "MIT"
},
"node_modules/lodash.merge": { "node_modules/lodash.merge": {
"version": "4.6.2", "version": "4.6.2",
"dev": true, "dev": true,
"license": "MIT" "license": "MIT"
}, },
"node_modules/lodash.once": {
"version": "4.1.1",
"resolved": "https://registry.npmjs.org/lodash.once/-/lodash.once-4.1.1.tgz",
"integrity": "sha512-Sb487aTOCr9drQVL8pIxOzVhafOjZN9UU54hiN8PU3uAiSV7lx1yYNpbNmex2PK6dSJoNTSJUUswT651yww3Mg==",
"license": "MIT"
},
"node_modules/lodash.sortby": { "node_modules/lodash.sortby": {
"version": "4.7.0", "version": "4.7.0",
"resolved": "https://registry.npmjs.org/lodash.sortby/-/lodash.sortby-4.7.0.tgz", "resolved": "https://registry.npmjs.org/lodash.sortby/-/lodash.sortby-4.7.0.tgz",

View File

@@ -18,6 +18,7 @@
"@types/uuid": "^10.0.0", "@types/uuid": "^10.0.0",
"@vercel/speed-insights": "^1.3.1", "@vercel/speed-insights": "^1.3.1",
"axios": "^1.13.2", "axios": "^1.13.2",
"bcryptjs": "^3.0.3",
"clsx": "^2.1.1", "clsx": "^2.1.1",
"cors": "^2.8.5", "cors": "^2.8.5",
"date-fns": "^4.1.0", "date-fns": "^4.1.0",
@@ -25,6 +26,7 @@
"dotenv": "^17.2.3", "dotenv": "^17.2.3",
"enhanced-resolve": "^5.18.4", "enhanced-resolve": "^5.18.4",
"express": "^5.2.1", "express": "^5.2.1",
"jsonwebtoken": "^9.0.3",
"lucide-react": "^0.562.0", "lucide-react": "^0.562.0",
"mongodb": "^7.0.0", "mongodb": "^7.0.0",
"mongoose": "^9.1.5", "mongoose": "^9.1.5",

View File

@@ -1,7 +1,6 @@
import React from 'react'; import React from 'react';
import { usePresence } from '../hooks/usePresence'; import { usePresence } from '../hooks/usePresence';
import { useAuth } from '../context/useAuth'; import { useAuth } from '../context/useAuth';
import { useOrganization } from '@clerk/clerk-react';
import { SendMessageModal } from './SendMessageModal'; import { SendMessageModal } from './SendMessageModal';
import api from '../services/api'; import api from '../services/api';
@@ -25,69 +24,50 @@ interface PendingMessage {
export const TeamPresence: React.FC = () => { export const TeamPresence: React.FC = () => {
const { activeUsers } = usePresence(); const { activeUsers } = usePresence();
const { appUser } = useAuth(); const { appUser } = useAuth();
const { organization } = useOrganization();
const [allMembers, setAllMembers] = React.useState<OrganizationMember[]>([]); const [allMembers, setAllMembers] = React.useState<OrganizationMember[]>([]);
const [pendingMessages, setPendingMessages] = React.useState<PendingMessage[]>([]); const [pendingMessages, setPendingMessages] = React.useState<PendingMessage[]>([]);
const [selectedUser, setSelectedUser] = React.useState<{ id: string; name: string } | null>(null); const [selectedUser, setSelectedUser] = React.useState<{ id: string; name: string } | null>(null);
const [isModalOpen, setIsModalOpen] = React.useState(false); const [isModalOpen, setIsModalOpen] = React.useState(false);
console.log('TeamPresence rendered'); // Fetch all members
console.log('appUser:', appUser); const fetchMembers = React.useCallback(async () => {
console.log('organization:', organization);
console.log('activeUsers:', activeUsers);
console.log('allMembers:', allMembers);
// Fetch all organization members
React.useEffect(() => {
const fetchMembers = async () => {
console.log('Fetching members...');
try { try {
const response = await api.get<OrganizationMember[]>('/users'); const response = await api.get<OrganizationMember[]>('/users');
console.log('Members fetched:', response.data);
setAllMembers(response.data); setAllMembers(response.data);
} catch (error) { } catch (error) {
console.error('Error fetching members:', error); console.error('Error fetching members:', error);
} }
}; }, []);
if (organization?.id) {
console.log('Organization ID exists, fetching members');
fetchMembers();
// Refresh every minute
const interval = setInterval(fetchMembers, 60000);
return () => clearInterval(interval);
} else {
console.log('No organization ID, skipping fetch');
}
}, [organization?.id]);
// Fetch pending messages // Fetch pending messages
React.useEffect(() => { const fetchPendingMessages = React.useCallback(async () => {
const fetchPendingMessages = async () => {
try { try {
const response = await api.get<PendingMessage[]>('/messages/pending'); const response = await api.get<PendingMessage[]>('/messages/pending');
setPendingMessages(response.data); setPendingMessages(response.data);
} catch (error) { } catch (error) {
console.error('Error fetching pending messages:', error); console.error('Error fetching pending messages:', error);
} }
}; }, []);
if (organization?.id) { React.useEffect(() => {
if (appUser) {
fetchMembers();
fetchPendingMessages(); fetchPendingMessages();
const interval = setInterval(fetchPendingMessages, 30000); const memberInterval = setInterval(fetchMembers, 60000);
return () => clearInterval(interval); const messageInterval = setInterval(fetchPendingMessages, 30000);
return () => {
clearInterval(memberInterval);
clearInterval(messageInterval);
};
} }
}, [organization?.id]); }, [appUser, fetchMembers, fetchPendingMessages]);
console.log('Rendering with allMembers.length:', allMembers.length);
if (allMembers.length === 0) { if (allMembers.length === 0) {
console.log('No members, returning null');
return null; return null;
} }
// Create a Set of active user IDs for fast lookup // Create a Set of active user IDs for fast lookup
const activeUserIds = new Set(activeUsers.map(u => u.clerkId)); const activeUserEmails = new Set(activeUsers.map(u => u.email));
// Create a map of pending messages by recipient ID // Create a map of pending messages by recipient ID
const pendingMessagesByRecipient = new Map( const pendingMessagesByRecipient = new Map(
@@ -95,10 +75,10 @@ export const TeamPresence: React.FC = () => {
); );
const handleMemberClick = (member: OrganizationMember) => { const handleMemberClick = (member: OrganizationMember) => {
if (member.clerkUserId === appUser?.clerkId) { if (member.email === appUser?.email) {
return; // Don't allow messaging yourself return; // Don't allow messaging yourself
} }
setSelectedUser({ id: member.clerkUserId, name: member.name }); setSelectedUser({ id: member.email, name: member.name });
setIsModalOpen(true); setIsModalOpen(true);
}; };
@@ -108,13 +88,7 @@ export const TeamPresence: React.FC = () => {
}; };
const handleMessageSent = async () => { const handleMessageSent = async () => {
// Refresh pending messages fetchPendingMessages();
try {
const response = await api.get<PendingMessage[]>('/messages/pending');
setPendingMessages(response.data);
} catch (error) {
console.error('Error refreshing pending messages:', error);
}
}; };
const getExistingMessage = (member: OrganizationMember) => { const getExistingMessage = (member: OrganizationMember) => {
@@ -132,8 +106,8 @@ export const TeamPresence: React.FC = () => {
</div> </div>
<div className="flex flex-wrap gap-2"> <div className="flex flex-wrap gap-2">
{allMembers.map((member) => { {allMembers.map((member) => {
const isOnline = activeUserIds.has(member.clerkUserId); const isOnline = activeUserEmails.has(member.email);
const isCurrentUser = member.clerkUserId === appUser?.clerkId; const isCurrentUser = member.email === appUser?.email;
const hasPendingMessage = pendingMessagesByRecipient.has(member.email); const hasPendingMessage = pendingMessagesByRecipient.has(member.email);
return ( return (
@@ -201,7 +175,7 @@ export const TeamPresence: React.FC = () => {
onClose={handleModalClose} onClose={handleModalClose}
recipientId={selectedUser.id} recipientId={selectedUser.id}
recipientName={selectedUser.name} recipientName={selectedUser.name}
existingMessage={getExistingMessage(allMembers.find(m => m.clerkUserId === selectedUser.id)!)} existingMessage={getExistingMessage(allMembers.find(m => m.email === selectedUser.id)!)}
onMessageSent={handleMessageSent} onMessageSent={handleMessageSent}
/> />
)} )}

View File

@@ -1,7 +1,7 @@
import React, { useState, useRef } from 'react'; import React, { useState, useRef } from 'react';
import { Download, Upload, AlertTriangle, CheckCircle, Database, FileJson, Info, RefreshCw } from 'lucide-react'; import { Download, Upload, AlertTriangle, CheckCircle, Database, FileJson, Info, RefreshCw } from 'lucide-react';
import api from '../../services/api'; import api from '../../services/api';
import { useOrganization } from '@clerk/clerk-react'; import { useAuth } from '../../context/useAuth';
interface BackupStats { interface BackupStats {
projects: number; projects: number;
@@ -28,7 +28,7 @@ interface BackupValidation {
} }
export const BackupRestore: React.FC = () => { export const BackupRestore: React.FC = () => {
const { organization } = useOrganization(); const { appUser } = useAuth();
const [isExporting, setIsExporting] = useState(false); const [isExporting, setIsExporting] = useState(false);
const [isImporting, setIsImporting] = useState(false); const [isImporting, setIsImporting] = useState(false);
const [validationResult, setValidationResult] = useState<BackupValidation | null>(null); const [validationResult, setValidationResult] = useState<BackupValidation | null>(null);
@@ -36,7 +36,7 @@ export const BackupRestore: React.FC = () => {
const fileInputRef = useRef<HTMLInputElement>(null); const fileInputRef = useRef<HTMLInputElement>(null);
const handleExport = async () => { const handleExport = async () => {
if (!organization) return; if (!appUser) return;
setIsExporting(true); setIsExporting(true);
try { try {
@@ -52,7 +52,7 @@ export const BackupRestore: React.FC = () => {
// Nome do arquivo com timestamp // Nome do arquivo com timestamp
const timestamp = new Date().toISOString().replace(/[:.]/g, '-').slice(0, -5); const timestamp = new Date().toISOString().replace(/[:.]/g, '-').slice(0, -5);
link.download = `backup_${organization.name}_${timestamp}.json`; link.download = `backup_gpi_${timestamp}.json`;
document.body.appendChild(link); document.body.appendChild(link);
link.click(); link.click();
@@ -148,8 +148,8 @@ export const BackupRestore: React.FC = () => {
<div className="flex-1"> <div className="flex-1">
<h3 className="text-lg font-bold text-text-main mb-2">Backup e Restauração de Dados</h3> <h3 className="text-lg font-bold text-text-main mb-2">Backup e Restauração de Dados</h3>
<p className="text-sm text-text-muted leading-relaxed"> <p className="text-sm text-text-muted leading-relaxed">
Use esta ferramenta para criar cópias de segurança de todos os dados da organização ou restaurar dados de um backup anterior. Use esta ferramenta para criar cópias de segurança de todos os dados do sistema ou restaurar dados de um backup anterior.
<strong className="text-amber-500"> Os backups são específicos para cada organização e não podem ser restaurados em outras organizações.</strong> <strong className="text-amber-500"> Os backups são específicos para cada instalação e podem não ser compatíveis entre versões diferentes se houver mudanças estruturais.</strong>
</p> </p>
</div> </div>
</div> </div>
@@ -248,18 +248,18 @@ export const BackupRestore: React.FC = () => {
</label> </label>
{validationResult && ( {validationResult && (
<div className={`p-4 rounded-xl border ${validationResult.valid && validationResult.isValidOrganization <div className={`p-4 rounded-xl border ${validationResult.valid
? 'bg-green-500/10 border-green-500/30' ? 'bg-green-500/10 border-green-500/30'
: 'bg-red-500/10 border-red-500/30' : 'bg-red-500/10 border-red-500/30'
}`}> }`}>
<div className="flex items-start gap-3"> <div className="flex items-start gap-3">
{validationResult.valid && validationResult.isValidOrganization ? ( {validationResult.valid ? (
<CheckCircle size={20} className="text-green-400 flex-shrink-0 mt-0.5" /> <CheckCircle size={20} className="text-green-400 flex-shrink-0 mt-0.5" />
) : ( ) : (
<AlertTriangle size={20} className="text-red-400 flex-shrink-0 mt-0.5" /> <AlertTriangle size={20} className="text-red-400 flex-shrink-0 mt-0.5" />
)} )}
<div className="flex-1 space-y-2"> <div className="flex-1 space-y-2">
<p className={`text-sm font-bold ${validationResult.valid && validationResult.isValidOrganization <p className={`text-sm font-bold ${validationResult.valid
? 'text-green-400' ? 'text-green-400'
: 'text-red-400' : 'text-red-400'
}`}> }`}>
@@ -289,7 +289,7 @@ export const BackupRestore: React.FC = () => {
<button <button
onClick={handleImport} onClick={handleImport}
disabled={!validationResult?.valid || !validationResult?.isValidOrganization || isImporting} disabled={!validationResult?.valid || isImporting}
className="w-full flex items-center justify-center gap-2 px-6 py-4 bg-red-500 hover:bg-red-600 text-white rounded-xl font-bold transition-all disabled:opacity-50 disabled:cursor-not-allowed shadow-lg shadow-red-500/20" className="w-full flex items-center justify-center gap-2 px-6 py-4 bg-red-500 hover:bg-red-600 text-white rounded-xl font-bold transition-all disabled:opacity-50 disabled:cursor-not-allowed shadow-lg shadow-red-500/20"
> >
{isImporting ? ( {isImporting ? (

View File

@@ -1,14 +1,14 @@
import React, { useEffect, useState } from 'react'; import React, { useEffect, useState } from 'react';
import { useOrganization } from '@clerk/clerk-react';
import { Plus, Pencil, Trash2, Box, RefreshCw } from 'lucide-react'; import { Plus, Pencil, Trash2, Box, RefreshCw } from 'lucide-react';
import { Button } from '../Button'; import { Button } from '../Button';
import { Modal } from '../Modal'; import { Modal } from '../Modal';
import { Input } from '../Input'; import { Input } from '../Input';
import * as geometryService from '../../services/geometryTypeService'; import * as geometryService from '../../services/geometryTypeService';
import type { GeometryType } from '../../types'; import type { GeometryType } from '../../types';
import { useAuth } from '../../context/useAuth';
export const GeometrySettings: React.FC = () => { export const GeometrySettings: React.FC = () => {
const { organization } = useOrganization(); const { appUser } = useAuth();
const [types, setTypes] = useState<GeometryType[]>([]); const [types, setTypes] = useState<GeometryType[]>([]);
const [loading, setLoading] = useState(true); const [loading, setLoading] = useState(true);
const [isModalOpen, setIsModalOpen] = useState(false); const [isModalOpen, setIsModalOpen] = useState(false);
@@ -20,13 +20,7 @@ export const GeometrySettings: React.FC = () => {
efficiencyLoss: '20' efficiencyLoss: '20'
}); });
useEffect(() => { const fetchTypes = React.useCallback(async () => {
if (organization?.id) {
fetchTypes();
}
}, [organization?.id]);
const fetchTypes = async () => {
setLoading(true); setLoading(true);
try { try {
const response = await geometryService.getAllTypes(); const response = await geometryService.getAllTypes();
@@ -36,7 +30,13 @@ export const GeometrySettings: React.FC = () => {
} finally { } finally {
setLoading(false); setLoading(false);
} }
}; }, []);
useEffect(() => {
if (appUser) {
fetchTypes();
}
}, [appUser, fetchTypes]);
const handleOpenModal = (item?: GeometryType) => { const handleOpenModal = (item?: GeometryType) => {
if (item) { if (item) {

View File

@@ -1,61 +1,52 @@
import React, { useState, useEffect, useCallback } from 'react'; import React, { useState, useEffect, useCallback } from 'react';
import { useAuth } from '@clerk/clerk-react'; import { useAuth } from '../context/useAuth';
import api from '../services/api'; import api from '../services/api';
import type { INotification } from '../types'; import type { INotification } from '../types';
import { NotificationContext } from './NotificationContextState'; import { NotificationContext } from './NotificationContextState';
export const NotificationProvider: React.FC<{ children: React.ReactNode }> = ({ children }) => { export const NotificationProvider: React.FC<{ children: React.ReactNode }> = ({ children }) => {
const { orgId, isSignedIn } = useAuth(); const { appUser, isSignedIn } = useAuth();
const orgId = appUser?.organizationId;
const [notifications, setNotifications] = useState<INotification[]>([]); const [notifications, setNotifications] = useState<INotification[]>([]);
const [loading, setLoading] = useState(false); const [loading, setLoading] = useState(false);
const fetchNotifications = useCallback(async () => { const fetchNotifications = useCallback(async () => {
if (!orgId || !isSignedIn) return; if (!isSignedIn || !orgId) return;
try { try {
if (notifications.length === 0) setLoading(true); setLoading(true);
const response = await api.get('/notifications'); const response = await api.get<INotification[]>('/notifications');
setNotifications(response.data); setNotifications(response.data);
} catch (error) { } catch (error) {
console.error('Failed to fetch notifications', error); console.error('Error fetching notifications:', error);
} finally { } finally {
setLoading(false); setLoading(false);
} }
}, [orgId, isSignedIn, notifications.length]); }, [isSignedIn, orgId]);
useEffect(() => {
if (isSignedIn && orgId) {
fetchNotifications();
const interval = setInterval(fetchNotifications, 60000);
return () => clearInterval(interval);
}
}, [isSignedIn, orgId, fetchNotifications]);
const markAsRead = async (id: string) => { const markAsRead = async (id: string) => {
try { try {
await api.put(`/notifications/${id}/read`); await api.patch(`/notifications/${id}/read`);
setNotifications(prev => prev.map(n => n._id === id ? { ...n, isRead: true } : n)); setNotifications(prev => prev.map(n => n._id === id ? { ...n, isRead: true } : n));
} catch (error) { } catch (error) {
console.error('Failed to mark as read', error); console.error('Error marking notification as read:', error);
} }
}; };
const markAllAsRead = async () => { const markAllAsRead = async () => {
try { try {
await api.put('/notifications/read-all'); await api.post('/notifications/read-all');
setNotifications(prev => prev.map(n => ({ ...n, isRead: true }))); setNotifications(prev => prev.map(n => ({ ...n, isRead: true })));
} catch (error) { } catch (error) {
console.error('Failed to mark all as read', error); console.error('Error marking all notifications as read:', error);
}
}
const clearAll = async () => {
try {
await api.delete('/notifications/clear-all');
setNotifications([]);
} catch (error) {
console.error('Failed to clear all notifications', error);
}
};
const archiveNotification = async (id: string) => {
try {
await api.patch(`/notifications/${id}/archive`);
setNotifications(prev => prev.filter(n => n._id !== id));
} catch (error) {
console.error('Failed to archive notification', error);
} }
}; };
@@ -64,25 +55,10 @@ export const NotificationProvider: React.FC<{ children: React.ReactNode }> = ({
await api.delete(`/notifications/${id}`); await api.delete(`/notifications/${id}`);
setNotifications(prev => prev.filter(n => n._id !== id)); setNotifications(prev => prev.filter(n => n._id !== id));
} catch (error) { } catch (error) {
console.error('Failed to delete notification', error); console.error('Error deleting notification:', error);
} }
}; };
// Polling effect
useEffect(() => {
if (isSignedIn && orgId) {
fetchNotifications(); // Initial fetch
const interval = setInterval(() => {
fetchNotifications();
}, 30000); // Poll every 30 seconds
return () => clearInterval(interval);
} else {
setNotifications([]);
}
}, [isSignedIn, orgId, fetchNotifications]);
const unreadCount = notifications.filter(n => !n.isRead).length; const unreadCount = notifications.filter(n => !n.isRead).length;
return ( return (
@@ -90,12 +66,10 @@ export const NotificationProvider: React.FC<{ children: React.ReactNode }> = ({
notifications, notifications,
unreadCount, unreadCount,
loading, loading,
fetchNotifications,
markAsRead, markAsRead,
markAllAsRead, markAllAsRead,
clearAll, deleteNotification
archiveNotification,
deleteNotification,
fetchNotifications
}}> }}>
{children} {children}
</NotificationContext.Provider> </NotificationContext.Provider>

View File

@@ -1,5 +1,4 @@
import React, { useState, useEffect, useCallback } from 'react'; import React, { useState, useEffect, useCallback } from 'react';
import { useUser, useOrganization } from '@clerk/clerk-react';
import { Shield, UserCheck, UserX, Users, Search, RefreshCw, Crown, Eye, User as UserIcon, Upload, Info, Image as ImageIcon, Box, Database } from 'lucide-react'; import { Shield, UserCheck, UserX, Users, Search, RefreshCw, Crown, Eye, User as UserIcon, Upload, Info, Image as ImageIcon, Box, Database } from 'lucide-react';
import { clsx } from 'clsx'; import { clsx } from 'clsx';
import type { AppUser, UserRole } from '../types'; import type { AppUser, UserRole } from '../types';
@@ -15,9 +14,7 @@ const roleLabels: Record<UserRole, { label: string; color: string; icon: React.R
}; };
export const AdminDashboard: React.FC = () => { export const AdminDashboard: React.FC = () => {
const { user } = useUser(); const { appUser, isAdmin } = useAuth();
const { organization } = useOrganization();
const { isAdmin } = useAuth();
const [users, setUsers] = useState<AppUser[]>([]); const [users, setUsers] = useState<AppUser[]>([]);
const [isLoading, setIsLoading] = useState(true); const [isLoading, setIsLoading] = useState(true);
const [searchTerm, setSearchTerm] = useState(''); const [searchTerm, setSearchTerm] = useState('');
@@ -27,7 +24,7 @@ export const AdminDashboard: React.FC = () => {
const [logoLoading, setLogoLoading] = useState(false); const [logoLoading, setLogoLoading] = useState(false);
const fetchUsers = useCallback(async () => { const fetchUsers = useCallback(async () => {
if (!user || !organization?.id) return; if (!appUser) return;
try { try {
setIsLoading(true); setIsLoading(true);
@@ -38,110 +35,14 @@ export const AdminDashboard: React.FC = () => {
} finally { } finally {
setIsLoading(false); setIsLoading(false);
} }
}, [user, organization?.id]); }, [appUser]);
const syncOrganizationMembers = useCallback(async () => {
if (!organization) return;
try {
setIsLoading(true);
// Fetch ALL members from Clerk (handle pagination)
console.log('Fetching members from Clerk organization:', organization.id);
let allMembers: any[] = [];
let hasMore = true;
// Fetch all pages
while (hasMore) {
const clerkMembers = await organization.getMemberships();
console.log(`Fetched members:`, clerkMembers.data.length);
if (clerkMembers.data.length === 0) {
hasMore = false;
} else {
allMembers = clerkMembers.data;
hasMore = false; // Clerk retorna todos de uma vez normalmente
}
}
console.log('Total Clerk members fetched:', allMembers.length, allMembers);
// Get current users from database
const currentUsersResponse = await api.get('/users');
const currentUsers = currentUsersResponse.data;
console.log('Current users in database:', currentUsers.length, currentUsers);
// Create a Set of Clerk user IDs for fast lookup
const clerkUserIds = new Set(
allMembers
.map(m => m.publicUserData?.userId)
.filter(id => id != null)
);
console.log('Clerk user IDs:', Array.from(clerkUserIds));
// Step 1: Add/Update users from Clerk
for (const membership of allMembers) {
const clerkUser = membership.publicUserData;
console.log('Processing membership:', membership);
console.log('Public user data:', clerkUser);
if (clerkUser) {
const syncData = {
clerkId: clerkUser.userId,
email: clerkUser.identifier || '',
name: `${clerkUser.firstName || ''} ${clerkUser.lastName || ''}`.trim() || clerkUser.identifier || 'Usuário',
organizationId: organization.id,
clerkRole: membership.role
};
console.log('Syncing user:', syncData);
try {
const response = await api.post('/users/sync', syncData);
console.log('Sync success for', clerkUser.userId, ':', response.data);
} catch (syncError) {
console.error('Error syncing member:', clerkUser.userId, syncError);
}
}
}
// Step 2: Remove users from database that don't exist in Clerk anymore
let removedCount = 0;
for (const dbUser of currentUsers) {
const clerkUserId = dbUser.clerkUserId || dbUser.clerkId;
if (!clerkUserIds.has(clerkUserId)) {
console.log(`User ${dbUser.name} (${clerkUserId}) is in DB but not in Clerk - removing...`);
try {
await api.delete(`/users/${dbUser._id}`);
console.log(`Removed user ${dbUser.name} from database`);
removedCount++;
} catch (deleteError) {
console.error(`Error removing user ${dbUser.name}:`, deleteError);
}
}
}
// Reload users after sync
console.log('Reloading users from database...');
await fetchUsers();
const message = `Sincronização concluída!\n✅ ${allMembers.length} membros atualizados\n${removedCount > 0 ? `🗑️ ${removedCount} membros removidos` : ''}`;
alert(message);
} catch (error) {
console.error('Error syncing organization members:', error);
alert('Erro ao sincronizar membros. Verifique o console para mais detalhes.');
} finally {
setIsLoading(false);
}
}, [organization, fetchUsers]);
useEffect(() => { useEffect(() => {
fetchUsers(); fetchUsers();
}, [fetchUsers]); }, [fetchUsers]);
const handleRoleChange = async (userId: string, newRole: UserRole) => { const handleRoleChange = async (userId: string, newRole: UserRole) => {
if (!user) return; if (!appUser) return;
setActionLoading(userId); setActionLoading(userId);
try { try {
@@ -158,7 +59,7 @@ export const AdminDashboard: React.FC = () => {
}; };
const handleToggleBan = async (userId: string, isBanned: boolean) => { const handleToggleBan = async (userId: string, isBanned: boolean) => {
if (!user) return; if (!appUser) return;
setActionLoading(userId); setActionLoading(userId);
try { try {
@@ -183,7 +84,7 @@ export const AdminDashboard: React.FC = () => {
const handleLogoUpload = async (e: React.ChangeEvent<HTMLInputElement>) => { const handleLogoUpload = async (e: React.ChangeEvent<HTMLInputElement>) => {
const file = e.target.files?.[0]; const file = e.target.files?.[0];
if (!file || !organization) return; if (!file) return;
// Validations // Validations
const validTypes = ['image/png', 'image/jpeg', 'image/jpg', 'image/svg+xml']; const validTypes = ['image/png', 'image/jpeg', 'image/jpg', 'image/svg+xml'];
@@ -199,8 +100,9 @@ export const AdminDashboard: React.FC = () => {
setLogoLoading(true); setLogoLoading(true);
try { try {
await organization.setLogo({ file }); // Note: In the future, this should upload to our own backend
alert('Logo atualizado com sucesso!'); // For now, we'll keep the UI but mark it as pending backend integration
alert('Funcionalidade de upload de logo em migração para sistema nativo.');
} catch (error) { } catch (error) {
console.error('Error uploading logo:', error); console.error('Error uploading logo:', error);
alert('Erro ao atualizar o logo.'); alert('Erro ao atualizar o logo.');
@@ -234,14 +136,6 @@ export const AdminDashboard: React.FC = () => {
</div> </div>
{activeTab === 'users' && ( {activeTab === 'users' && (
<div className="flex gap-2"> <div className="flex gap-2">
<button
onClick={syncOrganizationMembers}
disabled={isLoading}
className="flex items-center gap-2 px-4 py-2.5 bg-primary hover:bg-primary-dark text-white border border-primary-dark rounded-xl font-semibold transition-all disabled:opacity-50"
>
<Users size={18} className={isLoading ? 'animate-spin' : ''} />
Sincronizar Clerk
</button>
<button <button
onClick={fetchUsers} onClick={fetchUsers}
disabled={isLoading} disabled={isLoading}
@@ -407,7 +301,7 @@ export const AdminDashboard: React.FC = () => {
<tbody className="divide-y divide-border/40"> <tbody className="divide-y divide-border/40">
{filteredUsers.map((u) => { {filteredUsers.map((u) => {
const roleInfo = roleLabels[u.role]; const roleInfo = roleLabels[u.role];
const isCurrentUser = u.clerkId === user?.id; const isCurrentUser = u.email === appUser?.email;
const isActionDisabled = actionLoading === u.id; const isActionDisabled = actionLoading === u.id;
return ( return (
@@ -483,117 +377,19 @@ export const AdminDashboard: React.FC = () => {
</> </>
) : activeTab === 'organization' ? ( ) : activeTab === 'organization' ? (
<div className="space-y-6 animate-in fade-in slide-in-from-left-4 duration-300"> <div className="space-y-6 animate-in fade-in slide-in-from-left-4 duration-300">
{/* Organization Settings Panel */} <div className="bg-surface rounded-2xl p-8 border border-border/40 text-center space-y-4">
<div className="grid grid-cols-1 lg:grid-cols-2 gap-6"> <ImageIcon size={48} className="mx-auto text-text-muted opacity-20" />
<div className="bg-surface rounded-2xl p-6 border border-border/40 space-y-6"> <h2 className="text-xl font-bold text-text-main">Gestão de Identidade Visual</h2>
<div className="flex items-center gap-3 pb-4 border-b border-border/20"> <p className="text-text-muted max-w-md mx-auto">
<div className="w-10 h-10 rounded-xl bg-primary/10 flex items-center justify-center"> O gerenciamento nativo de logos está sendo implementado. No momento, o logo atual é gerenciado via configurações do sistema.
<Upload size={20} className="text-primary" />
</div>
<div>
<h2 className="text-lg font-bold text-text-main">Identidade Visual</h2>
<p className="text-xs text-text-muted">Gerencie o logo da sua organização</p>
</div>
</div>
<div className="flex flex-col items-center gap-6 py-4">
{organization?.imageUrl ? (
<div className="relative group">
<div className="w-32 h-32 rounded-2xl border-2 border-primary/20 p-2 bg-white overflow-hidden shadow-xl">
<img
src={organization.imageUrl}
alt={organization.name}
className="w-full h-full object-contain"
/>
</div>
<div className="absolute -bottom-2 -right-2 bg-primary text-white p-2 rounded-lg shadow-lg opacity-0 group-hover:opacity-100 transition-opacity">
<ImageIcon size={14} />
</div>
</div>
) : (
<div className="w-32 h-32 rounded-2xl border-2 border-dashed border-border/40 flex flex-col items-center justify-center bg-surface-soft text-text-muted gap-2">
<ImageIcon size={32} className="opacity-20" />
<span className="text-[10px] font-bold uppercase tracking-widest">Sem Logo</span>
</div>
)}
<div className="w-full space-y-4">
<label className="flex flex-col items-center justify-center w-full h-32 border-2 border-dashed border-border/40 rounded-2xl cursor-pointer hover:bg-surface-hover hover:border-primary/50 transition-all group">
<div className="flex flex-col items-center justify-center pt-5 pb-6">
<Upload className="w-8 h-8 text-text-muted group-hover:text-primary transition-colors mb-2" />
<p className="text-sm text-text-main font-bold">Clique para alterar o logo</p>
<p className="text-xs text-text-muted">ou arraste e solte o arquivo</p>
</div>
<input
type="file"
className="hidden"
accept="image/png,image/jpeg,image/jpg,image/svg+xml"
onChange={handleLogoUpload}
disabled={logoLoading}
/>
</label>
{logoLoading && (
<div className="flex items-center justify-center gap-2 text-primary font-bold animate-pulse">
<RefreshCw size={16} className="animate-spin" />
<span>Enviando logo...</span>
</div>
)}
</div>
</div>
</div>
<div className="bg-surface rounded-2xl p-6 border border-border/40 space-y-6">
<div className="flex items-center gap-3 pb-4 border-b border-border/20">
<div className="w-10 h-10 rounded-xl bg-amber-500/10 flex items-center justify-center">
<Info size={20} className="text-amber-500" />
</div>
<div>
<h2 className="text-lg font-bold text-text-main">Requisitos & Dicas</h2>
<p className="text-xs text-text-muted">Regras para um visual impecável</p>
</div>
</div>
<div className="space-y-4">
<div className="p-4 bg-surface-soft rounded-xl border border-border/20">
<h3 className="text-sm font-bold text-text-main flex items-center gap-2 mb-2">
<div className="w-1.5 h-1.5 rounded-full bg-primary"></div>
Formatos Suportados
</h3>
<p className="text-xs text-text-muted leading-relaxed">
Aceitamos arquivos nos formatos <strong>PNG, JPG ou SVG</strong>. O formato SVG é recomendado para máxima nitidez em qualquer tamanho.
</p> </p>
</div> </div>
<div className="p-4 bg-surface-soft rounded-xl border border-border/20">
<h3 className="text-sm font-bold text-text-main flex items-center gap-2 mb-2">
<div className="w-1.5 h-1.5 rounded-full bg-amber-500"></div>
Dimensões Recomendadas
</h3>
<p className="text-xs text-text-muted leading-relaxed">
Recomendamos uma imagem quadrada de no mínimo <strong>512x512 pixels</strong>. Logos horizontais podem não aparecer corretamente em todas as áreas.
</p>
</div>
<div className="p-4 bg-surface-soft rounded-xl border border-border/20">
<h3 className="text-sm font-bold text-text-main flex items-center gap-2 mb-2">
<div className="w-1.5 h-1.5 rounded-full bg-red-500"></div>
Limite de Tamanho
</h3>
<p className="text-xs text-text-muted leading-relaxed">
O arquivo não deve ultrapassar <strong>500 KB</strong>. Arquivos maiores serão rejeitados automaticamente para garantir rapidez no carregamento.
</p>
</div>
</div>
</div>
</div>
</div> </div>
) : activeTab === 'settings' ? ( ) : activeTab === 'settings' ? (
<GeometrySettings /> <GeometrySettings />
) : activeTab === 'backup' ? ( ) : activeTab === 'backup' ? (
<BackupRestore /> <BackupRestore />
) : ( ) : (
// Lazily load or direct render StockDashboard (Need to import it)
<div className="bg-surface rounded-2xl border border-border/40 p-6"> <div className="bg-surface rounded-2xl border border-border/40 p-6">
<div className="text-center py-10"> <div className="text-center py-10">
<h2 className="text-xl font-bold text-text-main">Gestão de Estoque</h2> <h2 className="text-xl font-bold text-text-main">Gestão de Estoque</h2>

View File

@@ -1,98 +1,19 @@
import React, { useEffect, useState } from 'react'; import React, { useEffect } from 'react';
import { useUser, useOrganizationList, useOrganization } from '@clerk/clerk-react'; import { useNavigate } from 'react-router-dom';
import { Building2, Users, RefreshCw, Mail } from 'lucide-react'; import { useAuth } from '../context/useAuth';
import { Building2, RefreshCw } from 'lucide-react';
export const OrganizationSelector: React.FC = () => { export const OrganizationSelector: React.FC = () => {
const { user } = useUser(); const { appUser, isSignedIn } = useAuth();
const { setActive, userMemberships, userInvitations } = useOrganizationList({ const navigate = useNavigate();
userMemberships: {
infinite: true,
},
userInvitations: {
infinite: true,
}
});
const { organization } = useOrganization();
const [isAcceptingInvites, setIsAcceptingInvites] = useState(false);
console.log('OrganizationSelector rendered');
console.log('Current organization:', organization);
console.log('User memberships:', userMemberships);
console.log('User memberships data:', userMemberships.data);
console.log('User invitations:', userInvitations);
console.log('User invitations data:', userInvitations.data);
// Auto-accept pending invitations
useEffect(() => { useEffect(() => {
const acceptPendingInvitations = async () => { if (isSignedIn && appUser?.organizationId) {
if (userInvitations.data && userInvitations.data.length > 0 && !isAcceptingInvites) { navigate('/');
console.log('Found pending invitations, auto-accepting...');
setIsAcceptingInvites(true);
for (const invitation of userInvitations.data) {
try {
console.log('Accepting invitation:', invitation);
await invitation.accept();
console.log('Invitation accepted successfully');
} catch (error) {
console.error('Error accepting invitation:', error);
}
} }
}, [isSignedIn, appUser, navigate]);
// Reload memberships after accepting invitations if (!isSignedIn) {
setTimeout(() => {
window.location.reload();
}, 1000);
}
};
acceptPendingInvitations();
}, [userInvitations.data, isAcceptingInvites]);
// Auto-select if user has only one organization
useEffect(() => {
console.log('Auto-select effect running...');
if (!organization && userMemberships.data && userMemberships.data.length === 1) {
console.log('Auto-selecting single organization...');
const membership = userMemberships.data[0];
if (setActive) {
setActive({ organization: membership.organization });
}
}
}, [organization, userMemberships.data, setActive]);
const handleSelectOrganization = async (orgId: string) => {
console.log('Selecting organization:', orgId);
if (setActive) {
await setActive({ organization: orgId });
}
// The auth context will automatically sync after organization changes
};
// Loading state - check if data exists or accepting invites
if (!userMemberships.data || isAcceptingInvites) {
console.log('Loading state - no data yet or accepting invites');
return (
<div className="min-h-screen bg-background flex items-center justify-center">
<div className="text-center">
{isAcceptingInvites ? (
<>
<Mail className="w-12 h-12 text-primary animate-bounce mx-auto mb-4" />
<p className="text-text-main font-bold mb-2">Aceitando convites pendentes...</p>
<p className="text-text-muted text-sm">Por favor aguarde</p>
</>
) : (
<>
<RefreshCw className="w-12 h-12 text-primary animate-spin mx-auto mb-4" />
<p className="text-text-muted">Carregando organizações...</p>
</>
)}
</div>
</div>
);
}
if (userMemberships.data?.length === 0) {
return ( return (
<div className="min-h-screen bg-background flex items-center justify-center p-4"> <div className="min-h-screen bg-background flex items-center justify-center p-4">
<div className="max-w-md w-full bg-surface rounded-2xl border border-border/40 p-8 text-center"> <div className="max-w-md w-full bg-surface rounded-2xl border border-border/40 p-8 text-center">
@@ -100,84 +21,28 @@ export const OrganizationSelector: React.FC = () => {
<Building2 className="w-8 h-8 text-amber-500" /> <Building2 className="w-8 h-8 text-amber-500" />
</div> </div>
<h1 className="text-2xl font-bold text-text-main mb-2"> <h1 className="text-2xl font-bold text-text-main mb-2">
Nenhuma Organização Não Conectado
</h1> </h1>
<p className="text-text-muted mb-6"> <p className="text-text-muted mb-6">
Você ainda não faz parte de nenhuma organização. Entre em contato com o administrador para receber um convite. Você precisa estar logado para acessar esta página.
</p> </p>
<div className="text-sm text-text-muted bg-surface-soft rounded-lg p-4"> <button
<p className="font-semibold mb-1">Conectado como:</p> onClick={() => navigate('/login')}
<p>{user?.primaryEmailAddress?.emailAddress}</p> className="w-full py-3 bg-primary text-white rounded-xl font-bold"
</div> >
Ir para Login
</button>
</div> </div>
</div> </div>
); );
} }
return ( return (
<div className="min-h-screen bg-background flex items-center justify-center p-4"> <div className="min-h-screen bg-background flex items-center justify-center">
<div className="max-w-2xl w-full"> <div className="text-center">
<div className="text-center mb-8"> <RefreshCw className="w-12 h-12 text-primary animate-spin mx-auto mb-4" />
<div className="w-16 h-16 rounded-2xl bg-primary/20 flex items-center justify-center mx-auto mb-4"> <p className="text-text-main font-bold mb-2">Redirecionando...</p>
<Building2 className="w-8 h-8 text-primary" /> <p className="text-text-muted text-sm">Carregando sua organização</p>
</div>
<h1 className="text-3xl font-bold text-text-main mb-2">
Selecione uma Organização
</h1>
<p className="text-text-muted">
Escolha qual organização você deseja acessar
</p>
</div>
<div className="grid gap-4">
{userMemberships.data?.map((membership) => (
<button
key={membership.organization.id}
onClick={() => handleSelectOrganization(membership.organization.id)}
className="w-full bg-surface hover:bg-surface-hover border border-border/40 rounded-2xl p-6 text-left transition-all hover:border-primary/50 hover:shadow-lg hover:shadow-primary/10 group"
>
<div className="flex items-center gap-4">
<div className="w-14 h-14 rounded-xl bg-primary/20 flex items-center justify-center flex-shrink-0 group-hover:bg-primary/30 transition-colors">
{membership.organization.imageUrl ? (
<img
src={membership.organization.imageUrl}
alt={membership.organization.name}
className="w-12 h-12 rounded-lg object-contain"
/>
) : (
<Building2 className="w-7 h-7 text-primary" />
)}
</div>
<div className="flex-1">
<h3 className="text-lg font-bold text-text-main group-hover:text-primary transition-colors">
{membership.organization.name}
</h3>
<div className="flex items-center gap-2 mt-1">
<span className="inline-flex items-center gap-1.5 px-2.5 py-1 rounded-lg bg-primary/20 text-primary text-xs font-semibold">
{membership.role === 'org:admin' ? 'Administrador' :
membership.role === 'org:member' ? 'Membro' : 'Convidado'}
</span>
<span className="flex items-center gap-1 text-xs text-text-muted">
<Users className="w-3 h-3" />
{membership.organization.membersCount || 0} membros
</span>
</div>
</div>
<div className="text-primary opacity-0 group-hover:opacity-100 transition-opacity">
<svg className="w-6 h-6" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M9 5l7 7-7 7" />
</svg>
</div>
</div>
</button>
))}
</div>
<div className="mt-6 text-center">
<p className="text-sm text-text-muted">
Conectado como <span className="font-semibold">{user?.primaryEmailAddress?.emailAddress}</span>
</p>
</div>
</div> </div>
</div> </div>
); );

View File

@@ -8,7 +8,6 @@ import { useAuth } from '../context/useAuth';
import { useToast } from '../hooks/useToast'; import { useToast } from '../hooks/useToast';
import { MobileList } from '../components/MobileList'; import { MobileList } from '../components/MobileList';
import { CreateProjectModal } from '../components/modals/CreateProjectModal'; import { CreateProjectModal } from '../components/modals/CreateProjectModal';
import { useOrganization } from '@clerk/clerk-react';
import { useSystemSettings } from '../context/SystemSettingsContext'; import { useSystemSettings } from '../context/SystemSettingsContext';
import { Modal } from '../components/Modal'; import { Modal } from '../components/Modal';
import { ConfirmModal } from '../components/ConfirmModal'; import { ConfirmModal } from '../components/ConfirmModal';
@@ -52,10 +51,9 @@ export const ProjectList: React.FC = () => {
const navigate = useNavigate(); const navigate = useNavigate();
const { appUser } = useAuth(); const { appUser } = useAuth();
const { showToast } = useToast(); const { showToast } = useToast();
const { organization } = useOrganization();
const { settings } = useSystemSettings(); const { settings } = useSystemSettings();
const logoUrl = settings?.appLogoUrl || organization?.imageUrl; const logoUrl = settings?.appLogoUrl;
const isAdmin = appUser?.email === 'admtracksteel@gmail.com' || appUser?.role === 'admin'; const isAdmin = appUser?.email === 'admtracksteel@gmail.com' || appUser?.role === 'admin';

View File

@@ -7,13 +7,11 @@ import { StockHistoryModal } from '../components/modals/StockHistoryModal';
import { StockInventoryReport } from '../components/reports/StockInventoryReport'; import { StockInventoryReport } from '../components/reports/StockInventoryReport';
import { DiluentListModal } from '../components/modals/DiluentListModal'; import { DiluentListModal } from '../components/modals/DiluentListModal';
import { useAuth } from '../context/useAuth'; import { useAuth } from '../context/useAuth';
import { useOrganization } from '@clerk/clerk-react';
import { useSystemSettings } from '../context/SystemSettingsContext'; import { useSystemSettings } from '../context/SystemSettingsContext';
export const StockDashboard: React.FC = () => { export const StockDashboard: React.FC = () => {
// ... rest of component // ... rest of component
const { isAdmin } = useAuth(); const { isAdmin } = useAuth();
const { organization } = useOrganization();
const { settings } = useSystemSettings(); const { settings } = useSystemSettings();
const [items, setItems] = useState<StockItem[]>([]); const [items, setItems] = useState<StockItem[]>([]);
@@ -28,7 +26,7 @@ export const StockDashboard: React.FC = () => {
const [activeTab, setActiveTab] = useState<'PAINT' | 'THINNER'>('PAINT'); const [activeTab, setActiveTab] = useState<'PAINT' | 'THINNER'>('PAINT');
const [showDiluentModal, setShowDiluentModal] = useState(false); const [showDiluentModal, setShowDiluentModal] = useState(false);
const logoUrl = settings?.appLogoUrl || organization?.imageUrl; const logoUrl = settings?.appLogoUrl;
const fetchItems = async () => { const fetchItems = async () => {
setLoading(true); setLoading(true);

View File

@@ -1,12 +1,14 @@
import mongoose from 'mongoose'; import mongoose from 'mongoose';
import bcrypt from 'bcryptjs';
const MONGODB_URI = 'mongodb+srv://admtracksteel:29OHAHpKTI8XcCNt@cluster0.a4xiilu.mongodb.net/ts_gpi?retryWrites=true&w=majority&appName=Cluster0'; const MONGODB_URI = 'mongodb+srv://admtracksteel:29OHAHpKTI8XcCNt@cluster0.a4xiilu.mongodb.net/ts_gpi?retryWrites=true&w=majority&appName=Cluster0';
const UserSchema = new mongoose.Schema({ const UserSchema = new mongoose.Schema({
clerkId: String, email: { type: String, required: true, unique: true },
email: String, password: { type: String, required: true },
name: String, name: { type: String, required: true },
role: { type: String, enum: ['guest', 'user', 'admin'], default: 'guest' }, role: { type: String, enum: ['guest', 'user', 'admin'], default: 'guest' },
organizationId: String,
isBanned: { type: Boolean, default: false } isBanned: { type: Boolean, default: false }
}, { timestamps: true }); }, { timestamps: true });
@@ -14,25 +16,41 @@ async function fixAdmin() {
await mongoose.connect(MONGODB_URI); await mongoose.connect(MONGODB_URI);
console.log('✅ Conectado ao MongoDB'); console.log('✅ Conectado ao MongoDB');
const User = mongoose.model('User', UserSchema); const User = mongoose.models.User || mongoose.model('User', UserSchema);
// Resetar m.reifonas para guest const email = 'admtracksteel@gmail.com';
await User.updateOne( const password = 'admin'; // Senha padrão temporária
{ email: 'm.reifonas@gmail.com' }, const hashedPassword = await bcrypt.hash(password, 10);
{ $set: { role: 'guest' } }
);
console.log('✅ m.reifonas@gmail.com resetado para guest');
// Atualizar admtracksteel para admin (o com clerkId real) // Tenta encontrar o usuário existente ou criar um novo
let user = await User.findOne({ email });
if (user) {
const result = await User.updateOne( const result = await User.updateOne(
{ email: 'admtracksteel@gmail.com', clerkId: { $ne: 'pending' } }, { email },
{ $set: { role: 'admin' } } {
$set: {
role: 'admin',
password: hashedPassword,
name: 'Admin TrackSteel'
}
}
); );
console.log('✅ admtracksteel@gmail.com atualizado para admin', result.modifiedCount > 0 ? '(sucesso)' : '(não encontrado)'); console.log(`✅ Usuário ${email} atualizado para admin e senha definida.`);
} else {
await User.create({
email,
password: hashedPassword,
name: 'Admin TrackSteel',
role: 'admin',
organizationId: 'default-org'
});
console.log(`✅ Usuário ${email} criado como admin.`);
}
// Listar todos os usuários // Listar todos os usuários
const users = await User.find({}); const users = await User.find({});
console.log('\n📋 Usuários atualizados:'); console.log('\n📋 Usuários atuais:');
users.forEach((u, i) => { users.forEach((u, i) => {
console.log(` ${i + 1}. ${u.email} | role: ${u.role}`); console.log(` ${i + 1}. ${u.email} | role: ${u.role}`);
}); });