new kw for it services & sectors alignment & viewer access partial fixed
This commit is contained in:
@@ -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 & 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 & edit all sites</li>
|
||||
<li>✓ Manage content & 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 & 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}
|
||||
>
|
||||
|
||||
@@ -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;
|
||||
|
||||
Reference in New Issue
Block a user