Improve product cards/pagination, Farsi locale fonts, and super-admin migrate UI.

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
Alireza Hassani
2026-07-29 16:10:43 +03:30
co-authored by Cursor
parent 2e40d5eb4c
commit f5b2193ba1
108 changed files with 2421 additions and 812 deletions
+59 -51
View File
@@ -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.