mirror of
https://git.meshkee.com/Meshkee/dashboards.git
synced 2026-08-11 22:30:58 +04:30
Improve product cards/pagination, Farsi locale fonts, and super-admin migrate UI.
Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
co-authored by
Cursor
parent
2e40d5eb4c
commit
f5b2193ba1
@@ -23,6 +23,7 @@ import {
|
||||
updateUserRole,
|
||||
} from '../services/userService'
|
||||
import { useToast } from '../context/ToastContext'
|
||||
import { Pagination } from '@meshkee/dashboard-ui'
|
||||
import pageStyles from '../components/PageContent.module.css'
|
||||
import tableStyles from './BusinessesPage.module.css'
|
||||
import styles from './UsersPage.module.css'
|
||||
@@ -35,14 +36,6 @@ function formatDate(value: string) {
|
||||
return d.toLocaleDateString('en-US', { year: 'numeric', month: 'short', day: '2-digit' })
|
||||
}
|
||||
|
||||
function buildPageNumbers(page: number, totalPages: number) {
|
||||
const start = Math.max(1, page - 2)
|
||||
const end = Math.min(totalPages, page + 2)
|
||||
const numbers: number[] = []
|
||||
for (let i = start; i <= end; i++) numbers.push(i)
|
||||
return numbers
|
||||
}
|
||||
|
||||
function displayName(user: UserListItem) {
|
||||
const name = [user.firstName, user.lastName].filter(Boolean).join(' ').trim()
|
||||
return name || '—'
|
||||
@@ -80,17 +73,20 @@ export function UsersPage() {
|
||||
const [editEmail, setEditEmail] = useState('')
|
||||
const [editCell, setEditCell] = useState('')
|
||||
const [editSubmitting, setEditSubmitting] = useState(false)
|
||||
const [editError, setEditError] = useState('')
|
||||
|
||||
const [resetOpen, setResetOpen] = useState(false)
|
||||
const [resetUser, setResetUser] = useState<UserListItem | null>(null)
|
||||
const [resetPassword, setResetPassword] = useState('')
|
||||
const [resetConfirm, setResetConfirm] = useState('')
|
||||
const [resetSubmitting, setResetSubmitting] = useState(false)
|
||||
const [resetError, setResetError] = useState('')
|
||||
|
||||
const [messageOpen, setMessageOpen] = useState(false)
|
||||
const [messageUser, setMessageUser] = useState<UserListItem | null>(null)
|
||||
const [messageText, setMessageText] = useState('')
|
||||
const [messageSubmitting, setMessageSubmitting] = useState(false)
|
||||
const [messageError, setMessageError] = useState('')
|
||||
|
||||
const [removeTarget, setRemoveTarget] = useState<UserListItem | null>(null)
|
||||
|
||||
@@ -99,6 +95,7 @@ export function UsersPage() {
|
||||
const [selectedRoleSlug, setSelectedRoleSlug] = useState('')
|
||||
const [selectedTeamRoleSlug, setSelectedTeamRoleSlug] = useState('')
|
||||
const [roleSubmitting, setRoleSubmitting] = useState(false)
|
||||
const [roleError, setRoleError] = useState('')
|
||||
|
||||
const [createOpen, setCreateOpen] = useState(false)
|
||||
const [createFirstName, setCreateFirstName] = useState('')
|
||||
@@ -107,6 +104,7 @@ export function UsersPage() {
|
||||
const [createPassword, setCreatePassword] = useState('')
|
||||
const [createEmail, setCreateEmail] = useState('')
|
||||
const [createSubmitting, setCreateSubmitting] = useState(false)
|
||||
const [createError, setCreateError] = useState('')
|
||||
const [listVersion, setListVersion] = useState(0)
|
||||
|
||||
const businessFilter = useMemo(() => {
|
||||
@@ -182,8 +180,6 @@ export function UsersPage() {
|
||||
return Math.max(1, Math.ceil(total / PAGE_SIZE))
|
||||
}, [data?.total])
|
||||
|
||||
const pageNumbers = useMemo(() => buildPageNumbers(page, totalPages), [page, totalPages])
|
||||
|
||||
const showingFrom = useMemo(() => {
|
||||
if (!data) return 0
|
||||
return (page - 1) * PAGE_SIZE + 1
|
||||
@@ -222,6 +218,7 @@ export function UsersPage() {
|
||||
setCreateCell('')
|
||||
setCreatePassword('')
|
||||
setCreateEmail('')
|
||||
setCreateError('')
|
||||
}
|
||||
|
||||
function openCreate() {
|
||||
@@ -235,7 +232,7 @@ export function UsersPage() {
|
||||
if (!cellNumber) return
|
||||
|
||||
setCreateSubmitting(true)
|
||||
setError('')
|
||||
setCreateError('')
|
||||
try {
|
||||
await createUser({
|
||||
businessId: businessFilter.businessId,
|
||||
@@ -251,7 +248,7 @@ export function UsersPage() {
|
||||
setPage(1)
|
||||
setListVersion((v) => v + 1)
|
||||
} catch (err) {
|
||||
setError(err instanceof ApiError ? err.message : 'Unable to add user.')
|
||||
setCreateError(err instanceof ApiError ? err.message : 'Unable to add user.')
|
||||
} finally {
|
||||
setCreateSubmitting(false)
|
||||
}
|
||||
@@ -266,6 +263,7 @@ export function UsersPage() {
|
||||
setRoleUser(user)
|
||||
setSelectedRoleSlug(user.roleSlug ?? '')
|
||||
setSelectedTeamRoleSlug(user.teamRole ?? teamRoles[0]?.slug ?? '')
|
||||
setRoleError('')
|
||||
setRoleOpen(true)
|
||||
}
|
||||
|
||||
@@ -280,7 +278,7 @@ export function UsersPage() {
|
||||
async function submitRoleChange() {
|
||||
if (!roleUser || !selectedRoleSlug || !canSaveRole) return
|
||||
setRoleSubmitting(true)
|
||||
setError('')
|
||||
setRoleError('')
|
||||
try {
|
||||
await updateUserRole(roleUser.id, selectedRoleSlug)
|
||||
|
||||
@@ -340,7 +338,7 @@ export function UsersPage() {
|
||||
setRoleUser(null)
|
||||
setSelectedTeamRoleSlug('')
|
||||
} catch (err) {
|
||||
setError(err instanceof ApiError ? err.message : 'Unable to update role.')
|
||||
setRoleError(err instanceof ApiError ? err.message : 'Unable to update role.')
|
||||
} finally {
|
||||
setRoleSubmitting(false)
|
||||
}
|
||||
@@ -352,13 +350,14 @@ export function UsersPage() {
|
||||
setEditLastName(user.lastName ?? '')
|
||||
setEditEmail('')
|
||||
setEditCell(user.cellNumber)
|
||||
setEditError('')
|
||||
setEditOpen(true)
|
||||
}
|
||||
|
||||
async function submitEdit() {
|
||||
if (!editUser) return
|
||||
setEditSubmitting(true)
|
||||
setError('')
|
||||
setEditError('')
|
||||
try {
|
||||
await updateUser(editUser.id, {
|
||||
firstName: editFirstName.trim(),
|
||||
@@ -386,7 +385,7 @@ export function UsersPage() {
|
||||
}
|
||||
})
|
||||
} catch (err) {
|
||||
setError(err instanceof ApiError ? err.message : 'Unable to update user.')
|
||||
setEditError(err instanceof ApiError ? err.message : 'Unable to update user.')
|
||||
} finally {
|
||||
setEditSubmitting(false)
|
||||
}
|
||||
@@ -396,24 +395,25 @@ export function UsersPage() {
|
||||
setResetUser(user)
|
||||
setResetPassword('')
|
||||
setResetConfirm('')
|
||||
setResetError('')
|
||||
setResetOpen(true)
|
||||
}
|
||||
|
||||
async function submitResetPassword() {
|
||||
if (!resetUser) return
|
||||
if (resetPassword !== resetConfirm) {
|
||||
setError('Passwords do not match.')
|
||||
setResetError('Passwords do not match.')
|
||||
return
|
||||
}
|
||||
setResetSubmitting(true)
|
||||
setError('')
|
||||
setResetError('')
|
||||
try {
|
||||
await adminResetUserPassword(resetUser.id, resetPassword)
|
||||
setResetOpen(false)
|
||||
setResetUser(null)
|
||||
showToast(`Password reset for "${displayName(resetUser)}".`, 'success')
|
||||
} catch (err) {
|
||||
setError(err instanceof ApiError ? err.message : 'Unable to reset password.')
|
||||
setResetError(err instanceof ApiError ? err.message : 'Unable to reset password.')
|
||||
} finally {
|
||||
setResetSubmitting(false)
|
||||
}
|
||||
@@ -422,20 +422,21 @@ export function UsersPage() {
|
||||
function openSendMessage(user: UserListItem) {
|
||||
setMessageUser(user)
|
||||
setMessageText('')
|
||||
setMessageError('')
|
||||
setMessageOpen(true)
|
||||
}
|
||||
|
||||
async function submitSendMessage() {
|
||||
if (!messageUser) return
|
||||
setMessageSubmitting(true)
|
||||
setError('')
|
||||
setMessageError('')
|
||||
try {
|
||||
const result = await sendUserMessage(messageUser.id, messageText.trim())
|
||||
setMessageOpen(false)
|
||||
setMessageUser(null)
|
||||
showToast(result.message, result.enabled ? 'success' : 'info')
|
||||
} catch (err) {
|
||||
setError(err instanceof ApiError ? err.message : 'Unable to send message.')
|
||||
setMessageError(err instanceof ApiError ? err.message : 'Unable to send message.')
|
||||
} finally {
|
||||
setMessageSubmitting(false)
|
||||
}
|
||||
@@ -693,37 +694,15 @@ export function UsersPage() {
|
||||
|
||||
<div className={tableStyles.pagination}>
|
||||
<div>
|
||||
Page {page} / {totalPages}
|
||||
</div>
|
||||
<div className={tableStyles.pagerBtns}>
|
||||
<button
|
||||
type="button"
|
||||
className={tableStyles.pageBtn}
|
||||
onClick={() => setPage((p) => Math.max(1, p - 1))}
|
||||
disabled={page <= 1 || loading}
|
||||
>
|
||||
Prev
|
||||
</button>
|
||||
{pageNumbers.map((n) => (
|
||||
<button
|
||||
key={n}
|
||||
type="button"
|
||||
className={`${tableStyles.pageBtn} ${n === page ? tableStyles.pageBtnActive : ''}`}
|
||||
onClick={() => setPage(n)}
|
||||
disabled={loading}
|
||||
>
|
||||
{n}
|
||||
</button>
|
||||
))}
|
||||
<button
|
||||
type="button"
|
||||
className={tableStyles.pageBtn}
|
||||
onClick={() => setPage((p) => Math.min(totalPages, p + 1))}
|
||||
disabled={page >= totalPages || loading}
|
||||
>
|
||||
Next
|
||||
</button>
|
||||
Page {page} / {totalPages} · {PAGE_SIZE} per page · {data?.total ?? 0} total
|
||||
</div>
|
||||
<Pagination
|
||||
currentPage={page}
|
||||
totalPages={totalPages}
|
||||
onPageChange={setPage}
|
||||
disabled={loading}
|
||||
variant="inline"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -734,8 +713,14 @@ export function UsersPage() {
|
||||
onClose={() => {
|
||||
setEditOpen(false)
|
||||
setEditUser(null)
|
||||
setEditError('')
|
||||
}}
|
||||
>
|
||||
{editError ? (
|
||||
<p className={tableStyles.alertError} role="alert">
|
||||
{editError}
|
||||
</p>
|
||||
) : null}
|
||||
<div className={tableStyles.formGrid}>
|
||||
<div className={tableStyles.field}>
|
||||
<label htmlFor="edit-first-name">First name</label>
|
||||
@@ -808,8 +793,14 @@ export function UsersPage() {
|
||||
onClose={() => {
|
||||
setResetOpen(false)
|
||||
setResetUser(null)
|
||||
setResetError('')
|
||||
}}
|
||||
>
|
||||
{resetError ? (
|
||||
<p className={tableStyles.alertError} role="alert">
|
||||
{resetError}
|
||||
</p>
|
||||
) : null}
|
||||
<p className={tableStyles.meta} style={{ marginBottom: 12 }}>
|
||||
Set a new password for {resetUser ? displayName(resetUser) : 'user'}.
|
||||
</p>
|
||||
@@ -863,8 +854,14 @@ export function UsersPage() {
|
||||
onClose={() => {
|
||||
setMessageOpen(false)
|
||||
setMessageUser(null)
|
||||
setMessageError('')
|
||||
}}
|
||||
>
|
||||
{messageError ? (
|
||||
<p className={tableStyles.alertError} role="alert">
|
||||
{messageError}
|
||||
</p>
|
||||
) : null}
|
||||
<p className={tableStyles.meta} style={{ marginBottom: 12 }}>
|
||||
Send an SMS to {messageUser ? formatCellForDisplay(messageUser.cellNumber) : 'user'}.
|
||||
</p>
|
||||
@@ -906,8 +903,14 @@ export function UsersPage() {
|
||||
setRoleOpen(false)
|
||||
setRoleUser(null)
|
||||
setSelectedTeamRoleSlug('')
|
||||
setRoleError('')
|
||||
}}
|
||||
>
|
||||
{roleError ? (
|
||||
<p className={tableStyles.alertError} role="alert">
|
||||
{roleError}
|
||||
</p>
|
||||
) : null}
|
||||
<p className={tableStyles.meta} style={{ marginBottom: 12 }}>
|
||||
Select a role for {roleUser ? displayName(roleUser) : 'user'}.
|
||||
{businessFilter ? ` Permissions apply to ${businessFilter.businessName}.` : ''}
|
||||
@@ -1021,6 +1024,11 @@ export function UsersPage() {
|
||||
resetCreateForm()
|
||||
}}
|
||||
>
|
||||
{createError ? (
|
||||
<p className={tableStyles.alertError} role="alert">
|
||||
{createError}
|
||||
</p>
|
||||
) : null}
|
||||
<p className={tableStyles.meta} style={{ marginBottom: 12 }}>
|
||||
Creates a new account or links an existing user as a customer of this business.
|
||||
Password is required only for new accounts.
|
||||
|
||||
Reference in New Issue
Block a user