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",
"@vercel/speed-insights": "^1.3.1",
"axios": "^1.13.2",
"bcryptjs": "^3.0.3",
"clsx": "^2.1.1",
"cors": "^2.8.5",
"date-fns": "^4.1.0",
@@ -20,6 +21,7 @@
"dotenv": "^17.2.3",
"enhanced-resolve": "^5.18.4",
"express": "^5.2.1",
"jsonwebtoken": "^9.0.3",
"lucide-react": "^0.562.0",
"mongodb": "^7.0.0",
"mongoose": "^9.1.5",
@@ -4964,6 +4966,15 @@
"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": {
"version": "2.3.0",
"dev": true,
@@ -5070,6 +5081,12 @@
"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": {
"version": "1.1.2",
"license": "MIT"
@@ -5795,6 +5812,15 @@
"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": {
"version": "2.5.9",
"resolved": "https://registry.npmjs.org/edge-runtime/-/edge-runtime-2.5.9.tgz",
@@ -7785,6 +7811,61 @@
"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": {
"version": "3.0.0",
"license": "Apache-2.0",
@@ -8102,11 +8183,53 @@
"dev": true,
"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": {
"version": "4.6.2",
"dev": true,
"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": {
"version": "4.7.0",
"resolved": "https://registry.npmjs.org/lodash.sortby/-/lodash.sortby-4.7.0.tgz",

View File

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

View File

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

View File

@@ -1,7 +1,7 @@
import React, { useState, useRef } from 'react';
import { Download, Upload, AlertTriangle, CheckCircle, Database, FileJson, Info, RefreshCw } from 'lucide-react';
import api from '../../services/api';
import { useOrganization } from '@clerk/clerk-react';
import { useAuth } from '../../context/useAuth';
interface BackupStats {
projects: number;
@@ -28,7 +28,7 @@ interface BackupValidation {
}
export const BackupRestore: React.FC = () => {
const { organization } = useOrganization();
const { appUser } = useAuth();
const [isExporting, setIsExporting] = useState(false);
const [isImporting, setIsImporting] = useState(false);
const [validationResult, setValidationResult] = useState<BackupValidation | null>(null);
@@ -36,7 +36,7 @@ export const BackupRestore: React.FC = () => {
const fileInputRef = useRef<HTMLInputElement>(null);
const handleExport = async () => {
if (!organization) return;
if (!appUser) return;
setIsExporting(true);
try {
@@ -52,7 +52,7 @@ export const BackupRestore: React.FC = () => {
// Nome do arquivo com timestamp
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);
link.click();
@@ -148,8 +148,8 @@ export const BackupRestore: React.FC = () => {
<div className="flex-1">
<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">
Use esta ferramenta para criar cópias de segurança de todos os dados da organização 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>
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 instalação e podem não ser compatíveis entre versões diferentes se houver mudanças estruturais.</strong>
</p>
</div>
</div>
@@ -248,18 +248,18 @@ export const BackupRestore: React.FC = () => {
</label>
{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-red-500/10 border-red-500/30'
}`}>
<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" />
) : (
<AlertTriangle size={20} className="text-red-400 flex-shrink-0 mt-0.5" />
)}
<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-red-400'
}`}>
@@ -289,7 +289,7 @@ export const BackupRestore: React.FC = () => {
<button
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"
>
{isImporting ? (

View File

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

View File

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

View File

@@ -1,5 +1,4 @@
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 { clsx } from 'clsx';
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 = () => {
const { user } = useUser();
const { organization } = useOrganization();
const { isAdmin } = useAuth();
const { appUser, isAdmin } = useAuth();
const [users, setUsers] = useState<AppUser[]>([]);
const [isLoading, setIsLoading] = useState(true);
const [searchTerm, setSearchTerm] = useState('');
@@ -27,7 +24,7 @@ export const AdminDashboard: React.FC = () => {
const [logoLoading, setLogoLoading] = useState(false);
const fetchUsers = useCallback(async () => {
if (!user || !organization?.id) return;
if (!appUser) return;
try {
setIsLoading(true);
@@ -38,110 +35,14 @@ export const AdminDashboard: React.FC = () => {
} finally {
setIsLoading(false);
}
}, [user, organization?.id]);
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]);
}, [appUser]);
useEffect(() => {
fetchUsers();
}, [fetchUsers]);
const handleRoleChange = async (userId: string, newRole: UserRole) => {
if (!user) return;
if (!appUser) return;
setActionLoading(userId);
try {
@@ -158,7 +59,7 @@ export const AdminDashboard: React.FC = () => {
};
const handleToggleBan = async (userId: string, isBanned: boolean) => {
if (!user) return;
if (!appUser) return;
setActionLoading(userId);
try {
@@ -183,7 +84,7 @@ export const AdminDashboard: React.FC = () => {
const handleLogoUpload = async (e: React.ChangeEvent<HTMLInputElement>) => {
const file = e.target.files?.[0];
if (!file || !organization) return;
if (!file) return;
// Validations
const validTypes = ['image/png', 'image/jpeg', 'image/jpg', 'image/svg+xml'];
@@ -199,8 +100,9 @@ export const AdminDashboard: React.FC = () => {
setLogoLoading(true);
try {
await organization.setLogo({ file });
alert('Logo atualizado com sucesso!');
// Note: In the future, this should upload to our own backend
// 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) {
console.error('Error uploading logo:', error);
alert('Erro ao atualizar o logo.');
@@ -234,14 +136,6 @@ export const AdminDashboard: React.FC = () => {
</div>
{activeTab === 'users' && (
<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
onClick={fetchUsers}
disabled={isLoading}
@@ -407,7 +301,7 @@ export const AdminDashboard: React.FC = () => {
<tbody className="divide-y divide-border/40">
{filteredUsers.map((u) => {
const roleInfo = roleLabels[u.role];
const isCurrentUser = u.clerkId === user?.id;
const isCurrentUser = u.email === appUser?.email;
const isActionDisabled = actionLoading === u.id;
return (
@@ -483,117 +377,19 @@ export const AdminDashboard: React.FC = () => {
</>
) : activeTab === 'organization' ? (
<div className="space-y-6 animate-in fade-in slide-in-from-left-4 duration-300">
{/* Organization Settings Panel */}
<div className="grid grid-cols-1 lg:grid-cols-2 gap-6">
<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-primary/10 flex items-center justify-center">
<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.
<div className="bg-surface rounded-2xl p-8 border border-border/40 text-center space-y-4">
<ImageIcon size={48} className="mx-auto text-text-muted opacity-20" />
<h2 className="text-xl font-bold text-text-main">Gestão de Identidade Visual</h2>
<p className="text-text-muted max-w-md mx-auto">
O gerenciamento nativo de logos está sendo implementado. No momento, o logo atual é gerenciado via configurações do sistema.
</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-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>
) : activeTab === 'settings' ? (
<GeometrySettings />
) : activeTab === 'backup' ? (
<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="text-center py-10">
<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 { useUser, useOrganizationList, useOrganization } from '@clerk/clerk-react';
import { Building2, Users, RefreshCw, Mail } from 'lucide-react';
import React, { useEffect } from 'react';
import { useNavigate } from 'react-router-dom';
import { useAuth } from '../context/useAuth';
import { Building2, RefreshCw } from 'lucide-react';
export const OrganizationSelector: React.FC = () => {
const { user } = useUser();
const { setActive, userMemberships, userInvitations } = useOrganizationList({
userMemberships: {
infinite: true,
},
userInvitations: {
infinite: true,
}
});
const { organization } = useOrganization();
const [isAcceptingInvites, setIsAcceptingInvites] = useState(false);
const { appUser, isSignedIn } = useAuth();
const navigate = useNavigate();
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(() => {
const acceptPendingInvitations = async () => {
if (userInvitations.data && userInvitations.data.length > 0 && !isAcceptingInvites) {
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);
}
if (isSignedIn && appUser?.organizationId) {
navigate('/');
}
}, [isSignedIn, appUser, navigate]);
// Reload memberships after accepting invitations
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) {
if (!isSignedIn) {
return (
<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">
@@ -100,84 +21,28 @@ export const OrganizationSelector: React.FC = () => {
<Building2 className="w-8 h-8 text-amber-500" />
</div>
<h1 className="text-2xl font-bold text-text-main mb-2">
Nenhuma Organização
Não Conectado
</h1>
<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>
<div className="text-sm text-text-muted bg-surface-soft rounded-lg p-4">
<p className="font-semibold mb-1">Conectado como:</p>
<p>{user?.primaryEmailAddress?.emailAddress}</p>
</div>
<button
onClick={() => navigate('/login')}
className="w-full py-3 bg-primary text-white rounded-xl font-bold"
>
Ir para Login
</button>
</div>
</div>
);
}
return (
<div className="min-h-screen bg-background flex items-center justify-center p-4">
<div className="max-w-2xl w-full">
<div className="text-center mb-8">
<div className="w-16 h-16 rounded-2xl bg-primary/20 flex items-center justify-center mx-auto mb-4">
<Building2 className="w-8 h-8 text-primary" />
</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 className="min-h-screen bg-background flex items-center justify-center">
<div className="text-center">
<RefreshCw className="w-12 h-12 text-primary animate-spin mx-auto mb-4" />
<p className="text-text-main font-bold mb-2">Redirecionando...</p>
<p className="text-text-muted text-sm">Carregando sua organização</p>
</div>
</div>
);

View File

@@ -8,7 +8,6 @@ import { useAuth } from '../context/useAuth';
import { useToast } from '../hooks/useToast';
import { MobileList } from '../components/MobileList';
import { CreateProjectModal } from '../components/modals/CreateProjectModal';
import { useOrganization } from '@clerk/clerk-react';
import { useSystemSettings } from '../context/SystemSettingsContext';
import { Modal } from '../components/Modal';
import { ConfirmModal } from '../components/ConfirmModal';
@@ -52,10 +51,9 @@ export const ProjectList: React.FC = () => {
const navigate = useNavigate();
const { appUser } = useAuth();
const { showToast } = useToast();
const { organization } = useOrganization();
const { settings } = useSystemSettings();
const logoUrl = settings?.appLogoUrl || organization?.imageUrl;
const logoUrl = settings?.appLogoUrl;
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 { DiluentListModal } from '../components/modals/DiluentListModal';
import { useAuth } from '../context/useAuth';
import { useOrganization } from '@clerk/clerk-react';
import { useSystemSettings } from '../context/SystemSettingsContext';
export const StockDashboard: React.FC = () => {
// ... rest of component
const { isAdmin } = useAuth();
const { organization } = useOrganization();
const { settings } = useSystemSettings();
const [items, setItems] = useState<StockItem[]>([]);
@@ -28,7 +26,7 @@ export const StockDashboard: React.FC = () => {
const [activeTab, setActiveTab] = useState<'PAINT' | 'THINNER'>('PAINT');
const [showDiluentModal, setShowDiluentModal] = useState(false);
const logoUrl = settings?.appLogoUrl || organization?.imageUrl;
const logoUrl = settings?.appLogoUrl;
const fetchItems = async () => {
setLoading(true);

View File

@@ -1,12 +1,14 @@
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 UserSchema = new mongoose.Schema({
clerkId: String,
email: String,
name: String,
email: { type: String, required: true, unique: true },
password: { type: String, required: true },
name: { type: String, required: true },
role: { type: String, enum: ['guest', 'user', 'admin'], default: 'guest' },
organizationId: String,
isBanned: { type: Boolean, default: false }
}, { timestamps: true });
@@ -14,25 +16,41 @@ async function fixAdmin() {
await mongoose.connect(MONGODB_URI);
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
await User.updateOne(
{ email: 'm.reifonas@gmail.com' },
{ $set: { role: 'guest' } }
);
console.log('✅ m.reifonas@gmail.com resetado para guest');
const email = 'admtracksteel@gmail.com';
const password = 'admin'; // Senha padrão temporária
const hashedPassword = await bcrypt.hash(password, 10);
// 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(
{ email: 'admtracksteel@gmail.com', clerkId: { $ne: 'pending' } },
{ $set: { role: 'admin' } }
{ email },
{
$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
const users = await User.find({});
console.log('\n📋 Usuários atualizados:');
console.log('\n📋 Usuários atuais:');
users.forEach((u, i) => {
console.log(` ${i + 1}. ${u.email} | role: ${u.role}`);
});