Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
31 changes: 17 additions & 14 deletions apps/dashboard/src/components/admin/RolesSettings.tsx
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import { useMemo, useState } from 'react';
import { Plus, Trash2, Lock } from 'lucide-react';
import { useTranslation } from 'react-i18next';
import Modal from '../ui/Modal';
import {
useRoles,
Expand All @@ -10,6 +11,7 @@ import {
} from '../../hooks/useAdmin';

export default function RolesSettings({ propertyId }: { propertyId: string }) {
const { t } = useTranslation();
const { data: roles = [] } = useRoles(propertyId);
const { data: catalog = [] } = usePermissionCatalog(propertyId);
const { create, remove, setPermissions } = useRoleMutations(propertyId);
Expand Down Expand Up @@ -47,9 +49,9 @@ export default function RolesSettings({ propertyId }: { propertyId: string }) {
{/* Role list */}
<div className="bg-white rounded-xl shadow-sm overflow-hidden">
<div className="px-4 py-3 border-b border-gray-100 flex items-center justify-between">
<h2 className="text-sm font-semibold text-telivity-navy">Roles</h2>
<h2 className="text-sm font-semibold text-telivity-navy">{t('admin.roles')}</h2>
<button onClick={() => setCreateOpen(true)} className="flex items-center gap-1 text-telivity-teal hover:text-telivity-light-teal text-sm font-semibold">
<Plus size={15} /> New
<Plus size={15} /> {t('admin.new')}
</button>
</div>
<ul>
Expand All @@ -65,7 +67,7 @@ export default function RolesSettings({ propertyId }: { propertyId: string }) {
<span className="text-sm font-medium text-telivity-navy">{r.name}</span>
{r.isSystem && <Lock size={12} className="text-telivity-mid-grey" />}
</div>
<p className="text-xs text-telivity-mid-grey">{r.permissions.length} permissions</p>
<p className="text-xs text-telivity-mid-grey">{t('admin.permissionsCount', { count: r.permissions.length })}</p>
</button>
</li>
))}
Expand All @@ -87,18 +89,18 @@ export default function RolesSettings({ propertyId }: { propertyId: string }) {
}
/>
) : (
<div className="bg-white rounded-xl shadow-sm p-8 text-center text-sm text-telivity-mid-grey">Select a role</div>
<div className="bg-white rounded-xl shadow-sm p-8 text-center text-sm text-telivity-mid-grey">{t('admin.selectARole')}</div>
)}
</div>

<Modal open={createOpen} onClose={() => setCreateOpen(false)} title="New Role">
<Modal open={createOpen} onClose={() => setCreateOpen(false)} title={t('admin.newRole')}>
<div className="space-y-4">
<div><label className="block text-xs font-medium text-telivity-mid-grey mb-1">Key * (lowercase, underscores)</label><input value={key} onChange={(e) => setKey(e.target.value)} placeholder="spa_manager" className="w-full border border-gray-200 rounded-lg px-3 py-2 text-sm focus:outline-none focus:border-telivity-teal" /></div>
<div><label className="block text-xs font-medium text-telivity-mid-grey mb-1">Name *</label><input value={name} onChange={(e) => setName(e.target.value)} placeholder="Spa Manager" className="w-full border border-gray-200 rounded-lg px-3 py-2 text-sm focus:outline-none focus:border-telivity-teal" /></div>
<div><label className="block text-xs font-medium text-telivity-mid-grey mb-1">Description</label><input value={description} onChange={(e) => setDescription(e.target.value)} className="w-full border border-gray-200 rounded-lg px-3 py-2 text-sm focus:outline-none focus:border-telivity-teal" /></div>
{create.isError && <p className="text-xs text-red-500">Could not create role (key may already exist).</p>}
<div><label className="block text-xs font-medium text-telivity-mid-grey mb-1">{t('admin.keyLabel')}</label><input value={key} onChange={(e) => setKey(e.target.value)} placeholder={t('admin.keyPlaceholder')} className="w-full border border-gray-200 rounded-lg px-3 py-2 text-sm focus:outline-none focus:border-telivity-teal" /></div>
<div><label className="block text-xs font-medium text-telivity-mid-grey mb-1">{t('admin.name')} {t('admin.required')}</label><input value={name} onChange={(e) => setName(e.target.value)} placeholder={t('admin.namePlaceholder')} className="w-full border border-gray-200 rounded-lg px-3 py-2 text-sm focus:outline-none focus:border-telivity-teal" /></div>
<div><label className="block text-xs font-medium text-telivity-mid-grey mb-1">{t('admin.description')}</label><input value={description} onChange={(e) => setDescription(e.target.value)} className="w-full border border-gray-200 rounded-lg px-3 py-2 text-sm focus:outline-none focus:border-telivity-teal" /></div>
{create.isError && <p className="text-xs text-red-500">{t('admin.couldNotCreateRole')}</p>}
<button onClick={submitCreate} disabled={!key.trim() || !name.trim() || create.isPending} className="w-full bg-telivity-teal text-white rounded-lg px-4 py-2 text-sm font-semibold disabled:opacity-50">
{create.isPending ? 'Creating…' : 'Create Role'}
{create.isPending ? t('admin.creating') : t('admin.createRole')}
</button>
</div>
</Modal>
Expand All @@ -121,6 +123,7 @@ function PermissionMatrix({
onSave: (keys: string[]) => void;
onDelete: () => void;
}) {
const { t } = useTranslation();
const [selected, setSelected] = useState<string[]>(role.permissions);
const readOnly = role.isSystem;
const toggle = (key: string) =>
Expand All @@ -136,13 +139,13 @@ function PermissionMatrix({
<div>
<h2 className="text-sm font-semibold text-telivity-navy flex items-center gap-2">
{role.name}
{readOnly && <span className="text-[10px] text-telivity-mid-grey uppercase tracking-wide flex items-center gap-1"><Lock size={11} /> system role (read-only)</span>}
{readOnly && <span className="text-[10px] text-telivity-mid-grey uppercase tracking-wide flex items-center gap-1"><Lock size={11} /> {t('admin.systemRoleReadOnly')}</span>}
</h2>
{role.description && <p className="text-xs text-telivity-mid-grey mt-0.5">{role.description}</p>}
</div>
{!readOnly && (
<button onClick={onDelete} disabled={deleting} className="inline-flex items-center gap-1 text-red-500 hover:text-red-600 text-sm font-medium disabled:opacity-50">
<Trash2 size={14} /> Delete
<Trash2 size={14} /> {t('admin.delete')}
</button>
)}
</div>
Expand Down Expand Up @@ -172,9 +175,9 @@ function PermissionMatrix({
{!readOnly && (
<div className="px-5 py-3 border-t border-gray-100 flex items-center gap-3">
<button onClick={() => onSave(selected)} disabled={!dirty || saving} className="bg-telivity-teal text-white rounded-lg px-5 py-2 text-sm font-semibold disabled:opacity-50">
{saving ? 'Saving…' : 'Save Permissions'}
{saving ? t('admin.saving') : t('admin.savePermissions')}
</button>
{dirty && <span className="text-xs text-telivity-orange">Unsaved changes</span>}
{dirty && <span className="text-xs text-telivity-orange">{t('admin.unsavedChanges')}</span>}
</div>
)}
</div>
Expand Down
46 changes: 25 additions & 21 deletions apps/dashboard/src/components/admin/UserSettings.tsx
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import { useState } from 'react';
import { UserPlus, ShieldCheck, Ban } from 'lucide-react';
import { useTranslation } from 'react-i18next';
import Modal from '../ui/Modal';
import {
useUsers,
Expand All @@ -10,6 +11,7 @@ import {
} from '../../hooks/useAdmin';

export default function UserSettings({ propertyId }: { propertyId: string }) {
const { t } = useTranslation();
const { data: users = [], isLoading } = useUsers(propertyId);
const { data: roles = [] } = useRoles(propertyId);
const { create, disable, assignRoles } = useUserMutations(propertyId);
Expand Down Expand Up @@ -39,20 +41,20 @@ export default function UserSettings({ propertyId }: { propertyId: string }) {
return (
<div className="bg-white rounded-xl shadow-sm overflow-hidden">
<div className="px-5 py-3 border-b border-gray-100 flex items-center justify-between">
<h2 className="text-sm font-semibold text-telivity-navy">Users</h2>
<h2 className="text-sm font-semibold text-telivity-navy">{t('admin.users')}</h2>
<button onClick={() => setCreateOpen(true)} className="flex items-center gap-2 bg-telivity-teal text-white rounded-lg px-3 py-1.5 text-sm font-semibold">
<UserPlus size={15} /> Add User
<UserPlus size={15} /> {t('admin.addUser')}
</button>
</div>

<table className="w-full">
<thead>
<tr className="bg-telivity-teal/5 border-b border-gray-100">
<th className="px-4 py-3 text-left text-xs font-semibold text-telivity-slate uppercase">Name</th>
<th className="px-4 py-3 text-left text-xs font-semibold text-telivity-slate uppercase">Email</th>
<th className="px-4 py-3 text-left text-xs font-semibold text-telivity-slate uppercase">Status</th>
<th className="px-4 py-3 text-left text-xs font-semibold text-telivity-slate uppercase">Roles</th>
<th className="px-4 py-3 text-right text-xs font-semibold text-telivity-slate uppercase">Actions</th>
<th className="px-4 py-3 text-left text-xs font-semibold text-telivity-slate uppercase">{t('admin.name')}</th>
<th className="px-4 py-3 text-left text-xs font-semibold text-telivity-slate uppercase">{t('admin.email')}</th>
<th className="px-4 py-3 text-left text-xs font-semibold text-telivity-slate uppercase">{t('admin.status')}</th>
<th className="px-4 py-3 text-left text-xs font-semibold text-telivity-slate uppercase">{t('admin.roles')}</th>
<th className="px-4 py-3 text-right text-xs font-semibold text-telivity-slate uppercase">{t('admin.actions')}</th>
</tr>
</thead>
<tbody>
Expand All @@ -76,41 +78,41 @@ export default function UserSettings({ propertyId }: { propertyId: string }) {
</div>
</td>
<td className="px-4 py-3 text-right whitespace-nowrap">
<button onClick={() => setEditUser(u)} title="Edit roles" className="inline-flex items-center gap-1 text-telivity-teal hover:text-telivity-light-teal text-sm font-medium mr-3">
<ShieldCheck size={14} /> Roles
<button onClick={() => setEditUser(u)} title={t('admin.editRoles')} className="inline-flex items-center gap-1 text-telivity-teal hover:text-telivity-light-teal text-sm font-medium mr-3">
<ShieldCheck size={14} /> {t('admin.roles')}
</button>
{u.status !== 'disabled' && (
<button onClick={() => disable.mutate(u.id)} title="Disable user" className="inline-flex items-center gap-1 text-red-500 hover:text-red-600 text-sm font-medium">
<Ban size={14} /> Disable
<button onClick={() => disable.mutate(u.id)} title={t('admin.disableUser')} className="inline-flex items-center gap-1 text-red-500 hover:text-red-600 text-sm font-medium">
<Ban size={14} /> {t('admin.disable')}
</button>
)}
</td>
</tr>
))}
{!isLoading && users.length === 0 && (
<tr><td colSpan={5} className="px-4 py-8 text-center text-sm text-telivity-mid-grey">No users yet</td></tr>
<tr><td colSpan={5} className="px-4 py-8 text-center text-sm text-telivity-mid-grey">{t('admin.noUsersYet')}</td></tr>
)}
</tbody>
</table>

{/* Create user */}
<Modal open={createOpen} onClose={() => setCreateOpen(false)} title="Add User">
<Modal open={createOpen} onClose={() => setCreateOpen(false)} title={t('admin.addUser')}>
<div className="space-y-4">
<div><label className="block text-xs font-medium text-telivity-mid-grey mb-1">Name *</label><input value={name} onChange={(e) => setName(e.target.value)} className="w-full border border-gray-200 rounded-lg px-3 py-2 text-sm focus:outline-none focus:border-telivity-teal" /></div>
<div><label className="block text-xs font-medium text-telivity-mid-grey mb-1">Email *</label><input type="email" value={email} onChange={(e) => setEmail(e.target.value)} className="w-full border border-gray-200 rounded-lg px-3 py-2 text-sm focus:outline-none focus:border-telivity-teal" /></div>
<div><label className="block text-xs font-medium text-telivity-mid-grey mb-1">{t('admin.name')} {t('admin.required')}</label><input value={name} onChange={(e) => setName(e.target.value)} className="w-full border border-gray-200 rounded-lg px-3 py-2 text-sm focus:outline-none focus:border-telivity-teal" /></div>
<div><label className="block text-xs font-medium text-telivity-mid-grey mb-1">{t('admin.email')} {t('admin.required')}</label><input type="email" value={email} onChange={(e) => setEmail(e.target.value)} className="w-full border border-gray-200 rounded-lg px-3 py-2 text-sm focus:outline-none focus:border-telivity-teal" /></div>
<div>
<label className="block text-xs font-medium text-telivity-mid-grey mb-1">Roles</label>
<label className="block text-xs font-medium text-telivity-mid-grey mb-1">{t('admin.roles')}</label>
<RoleChecklist roles={roles} selected={newRoleIds} onToggle={(id) => setNewRoleIds((s) => toggle(s, id))} />
</div>
{create.isError && <p className="text-xs text-red-500">Could not create user (email may already exist).</p>}
{create.isError && <p className="text-xs text-red-500">{t('admin.couldNotCreateUser')}</p>}
<button onClick={submitCreate} disabled={!name.trim() || !email.trim() || create.isPending} className="w-full bg-telivity-teal text-white rounded-lg px-4 py-2 text-sm font-semibold disabled:opacity-50">
{create.isPending ? 'Creating…' : 'Create User'}
{create.isPending ? t('admin.creating') : t('admin.createUser')}
</button>
</div>
</Modal>

{/* Edit roles */}
<Modal open={!!editUser} onClose={() => setEditUser(null)} title={editUser ? `Roles — ${editUser.name}` : ''}>
<Modal open={!!editUser} onClose={() => setEditUser(null)} title={editUser ? t('admin.rolesForUser', { name: editUser.name }) : ''}>
{editUser && (
<EditRoles
roles={roles}
Expand All @@ -127,27 +129,29 @@ export default function UserSettings({ propertyId }: { propertyId: string }) {
}

function RoleChecklist({ roles, selected, onToggle }: { roles: AdminRole[]; selected: string[]; onToggle: (id: string) => void }) {
const { t } = useTranslation();
return (
<div className="space-y-1 max-h-48 overflow-y-auto border border-gray-100 rounded-lg p-2">
{roles.map((r) => (
<label key={r.id} className="flex items-center gap-2 text-sm py-1 cursor-pointer">
<input type="checkbox" checked={selected.includes(r.id)} onChange={() => onToggle(r.id)} className="accent-telivity-teal" />
<span className="font-medium text-telivity-navy">{r.name}</span>
{r.isSystem && <span className="text-[10px] text-telivity-mid-grey uppercase tracking-wide">system</span>}
{r.isSystem && <span className="text-[10px] text-telivity-mid-grey uppercase tracking-wide">{t('admin.system')}</span>}
</label>
))}
</div>
);
}

function EditRoles({ roles, initial, pending, onSave }: { roles: AdminRole[]; initial: string[]; pending: boolean; onSave: (roleIds: string[]) => void }) {
const { t } = useTranslation();
const [selected, setSelected] = useState<string[]>(initial);
const toggle = (id: string) => setSelected((s) => (s.includes(id) ? s.filter((x) => x !== id) : [...s, id]));
return (
<div className="space-y-4">
<RoleChecklist roles={roles} selected={selected} onToggle={toggle} />
<button onClick={() => onSave(selected)} disabled={pending} className="w-full bg-telivity-teal text-white rounded-lg px-4 py-2 text-sm font-semibold disabled:opacity-50">
{pending ? 'Saving…' : 'Save Roles'}
{pending ? t('admin.saving') : t('admin.saveRoles')}
</button>
</div>
);
Expand Down
Loading
Loading