new kw for it services & sectors alignment & viewer access partial fixed

This commit is contained in:
IGNY8 VPS (Salman)
2026-02-20 23:29:51 +00:00
parent 2011e48145
commit 341f7c5bc7
110 changed files with 4768 additions and 36 deletions

View File

@@ -34,6 +34,7 @@ import {
type AccountSettings,
type TeamMember,
} from '../../services/billing.api';
import { fetchSites, type Site } from '../../services/api';
export default function AccountSettingsPage() {
const toast = useToast();
@@ -92,13 +93,17 @@ export default function AccountSettingsPage() {
email: '',
first_name: '',
last_name: '',
role: 'viewer' as 'admin' | 'viewer',
site_ids: [] as number[],
});
const [accountSites, setAccountSites] = useState<Site[]>([]);
useEffect(() => {
loadData();
loadTeamMembers();
loadProfile();
loadCountries();
loadAccountSites();
}, []);
// Load profile from auth store user data (fallback if API not ready)
@@ -152,6 +157,15 @@ export default function AccountSettingsPage() {
}
};
const loadAccountSites = async () => {
try {
const response = await fetchSites();
setAccountSites(response.results || []);
} catch (error: any) {
console.error('Failed to load sites:', error);
}
};
const loadCountries = async () => {
try {
setCountriesLoading(true);
@@ -252,13 +266,23 @@ export default function AccountSettingsPage() {
toast.error('Email is required');
return;
}
if (inviteForm.role === 'viewer' && inviteForm.site_ids.length === 0) {
toast.error('Please select at least one site for the viewer');
return;
}
try {
setInviting(true);
const result = await inviteTeamMember(inviteForm);
const result = await inviteTeamMember({
email: inviteForm.email,
first_name: inviteForm.first_name,
last_name: inviteForm.last_name,
role: inviteForm.role,
site_ids: inviteForm.role === 'viewer' ? inviteForm.site_ids : undefined,
});
toast.success(result.message || 'Team member invited successfully');
setShowInviteModal(false);
setInviteForm({ email: '', first_name: '', last_name: '' });
setInviteForm({ email: '', first_name: '', last_name: '', role: 'viewer', site_ids: [] });
await loadTeamMembers();
} catch (error: any) {
toast.error(`Failed to invite team member: ${error.message}`);
@@ -677,7 +701,12 @@ export default function AccountSettingsPage() {
</Badge>
</td>
<td className="py-3 px-4 text-sm text-gray-600 dark:text-gray-400">
{member.is_staff ? 'Admin' : 'Member'}
<Badge
variant="light"
color={member.role === 'owner' ? 'warning' : member.role === 'admin' ? 'primary' : 'info'}
>
{member.role === 'owner' ? 'Owner' : member.role === 'admin' ? 'Admin' : member.role === 'developer' ? 'Developer' : 'Viewer'}
</Badge>
</td>
<td className="py-3 px-4 text-sm text-gray-600 dark:text-gray-400">
{member.date_joined ? new Date(member.date_joined).toLocaleDateString() : 'N/A'}
@@ -717,27 +746,42 @@ export default function AccountSettingsPage() {
</span>
}
>
<div className="grid grid-cols-1 md:grid-cols-2 gap-4">
<div className="grid grid-cols-1 md:grid-cols-3 gap-4">
<div className="p-4 border-l-4 border-warning-500 bg-warning-50/50 dark:bg-warning-900/10 rounded-r-lg">
<div className="flex items-center justify-between mb-2">
<h4 className="font-semibold text-gray-900 dark:text-white">Owner</h4>
<Badge variant="light" color="warning">Full Access</Badge>
</div>
<ul className="text-sm text-gray-600 dark:text-gray-400 space-y-1">
<li> Full account control</li>
<li> Add / delete sites</li>
<li> Manage billing &amp; team</li>
<li> All content operations</li>
</ul>
</div>
<div className="p-4 border-l-4 border-brand-500 bg-brand-50/50 dark:bg-brand-900/10 rounded-r-lg">
<div className="flex items-center justify-between mb-2">
<h4 className="font-semibold text-gray-900 dark:text-white">Admin</h4>
<Badge variant="light" color="primary">High Access</Badge>
</div>
<ul className="text-sm text-gray-600 dark:text-gray-400 space-y-1">
<li> Manage all sites and content</li>
<li> Invite team members</li>
<li> View &amp; edit all sites</li>
<li> Manage content &amp; settings</li>
<li> Run automations</li>
<li> Cannot add/delete sites</li>
<li> Cannot manage billing</li>
</ul>
</div>
<div className="p-4 border-l-4 border-info-500 bg-info-50/50 dark:bg-info-900/10 rounded-r-lg">
<div className="flex items-center justify-between mb-2">
<h4 className="font-semibold text-gray-900 dark:text-white">Member</h4>
<Badge variant="light" color="info">Standard Access</Badge>
<h4 className="font-semibold text-gray-900 dark:text-white">Viewer</h4>
<Badge variant="light" color="info">Read Only</Badge>
</div>
<ul className="text-sm text-gray-600 dark:text-gray-400 space-y-1">
<li> Create and edit content</li>
<li> View analytics</li>
<li> Cannot invite users</li>
<li> View assigned sites only</li>
<li> View content &amp; analytics</li>
<li> Cannot edit anything</li>
<li> Cannot run automations</li>
</ul>
</div>
</div>
@@ -750,7 +794,7 @@ export default function AccountSettingsPage() {
isOpen={showInviteModal}
onClose={() => {
setShowInviteModal(false);
setInviteForm({ email: '', first_name: '', last_name: '' });
setInviteForm({ email: '', first_name: '', last_name: '', role: 'viewer', site_ids: [] });
}}
>
<div className="p-6">
@@ -769,16 +813,13 @@ export default function AccountSettingsPage() {
/>
</div>
<div>
<div className="grid grid-cols-2 gap-4">
<InputField
type="text"
label="First Name"
value={inviteForm.first_name}
onChange={(e) => setInviteForm(prev => ({ ...prev, first_name: e.target.value }))}
/>
</div>
<div>
<InputField
type="text"
label="Last Name"
@@ -786,6 +827,60 @@ export default function AccountSettingsPage() {
onChange={(e) => setInviteForm(prev => ({ ...prev, last_name: e.target.value }))}
/>
</div>
<div>
<label className="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-2">
Role *
</label>
<Select
options={[
{ value: 'viewer', label: 'Viewer — Read-only access to assigned sites' },
{ value: 'admin', label: 'Admin — Full access to all sites (no billing)' },
]}
defaultValue={inviteForm.role}
onChange={(value) => setInviteForm(prev => ({ ...prev, role: value as 'admin' | 'viewer', site_ids: value === 'admin' ? [] : prev.site_ids }))}
/>
</div>
{inviteForm.role === 'viewer' && (
<div>
<label className="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-2">
Grant Access to Sites *
</label>
{accountSites.length === 0 ? (
<p className="text-sm text-gray-500 dark:text-gray-400">No sites available. Create a site first.</p>
) : (
<div className="space-y-2 max-h-48 overflow-y-auto border border-gray-200 dark:border-gray-700 rounded-lg p-3">
{accountSites.map((site) => (
<label key={site.id} className="flex items-center gap-3 py-1 cursor-pointer hover:bg-gray-50 dark:hover:bg-gray-800 rounded px-2 -mx-2">
<input
type="checkbox"
checked={inviteForm.site_ids.includes(site.id)}
onChange={(e) => {
setInviteForm(prev => ({
...prev,
site_ids: e.target.checked
? [...prev.site_ids, site.id]
: prev.site_ids.filter(id => id !== site.id)
}));
}}
className="rounded border-gray-300 text-brand-500 focus:ring-brand-500"
/>
<span className="text-sm text-gray-900 dark:text-white">{site.name}</span>
{site.domain && (
<span className="text-xs text-gray-500 dark:text-gray-400">{site.domain}</span>
)}
</label>
))}
</div>
)}
{inviteForm.site_ids.length > 0 && (
<p className="text-xs text-gray-500 dark:text-gray-400 mt-1">
{inviteForm.site_ids.length} site{inviteForm.site_ids.length !== 1 ? 's' : ''} selected
</p>
)}
</div>
)}
</div>
<div className="mt-6 flex justify-end gap-3">
@@ -794,7 +889,7 @@ export default function AccountSettingsPage() {
tone="neutral"
onClick={() => {
setShowInviteModal(false);
setInviteForm({ email: '', first_name: '', last_name: '' });
setInviteForm({ email: '', first_name: '', last_name: '', role: 'viewer', site_ids: [] });
}}
disabled={inviting}
>

View File

@@ -668,6 +668,7 @@ export async function inviteTeamMember(data: {
first_name?: string;
last_name?: string;
role?: string;
site_ids?: number[];
}): Promise<{
message: string;
user?: TeamMember;