sem clerk
This commit is contained in:
123
package-lock.json
generated
123
package-lock.json
generated
@@ -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",
|
||||||
|
|||||||
@@ -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",
|
||||||
|
|||||||
@@ -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}
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
|
|||||||
@@ -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 ? (
|
||||||
|
|||||||
@@ -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) {
|
||||||
|
|||||||
@@ -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>
|
||||||
|
|||||||
@@ -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>
|
||||||
|
|||||||
@@ -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>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -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';
|
||||||
|
|
||||||
|
|||||||
@@ -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);
|
||||||
|
|||||||
@@ -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}`);
|
||||||
});
|
});
|
||||||
|
|||||||
Reference in New Issue
Block a user