fix: react-doctor control-has-associated-label 137→? (auto aria-labels)
Add aria-label to buttons (icon-only, pagination, action) and inputs (placeholder-derived) via balanced-brace JSX parser. Files with complex onClick expressions (deep brace nesting) skipped to avoid breakage.
This commit is contained in:
@@ -112,12 +112,12 @@ export default function AbandonedCartDashboard({ brandId }: Props) {
|
||||
{totalPages > 1 && (
|
||||
<div className="flex items-center gap-1 ml-2">
|
||||
<button type="button" onClick={() => setView(v => ({ ...v, page: Math.max(0, v.page - 1) }))} disabled={page === 0}
|
||||
className="flex h-7 w-7 items-center justify-center rounded border border-[var(--admin-border)] text-[var(--admin-text-muted)] hover:text-[var(--admin-text-primary)] hover:bg-stone-50 disabled:opacity-30 disabled:cursor-not-allowed transition-colors">
|
||||
className="flex h-7 w-7 items-center justify-center rounded border border-[var(--admin-border)] text-[var(--admin-text-muted)] hover:text-[var(--admin-text-primary)] hover:bg-stone-50 disabled:opacity-30 disabled:cursor-not-allowed transition-colors" aria-label="Previous">
|
||||
<svg className="h-3 w-3" fill="none" viewBox="0 0 24 24" stroke="currentColor" strokeWidth={2}><path strokeLinecap="round" strokeLinejoin="round" d="M15 19l-7-7 7-7" /></svg>
|
||||
</button>
|
||||
<span className="text-xs text-[var(--admin-text-muted)] px-1">{page + 1}/{totalPages}</span>
|
||||
<button type="button" onClick={() => setView(v => ({ ...v, page: Math.min(totalPages - 1, v.page + 1) }))} disabled={page >= totalPages - 1}
|
||||
className="flex h-7 w-7 items-center justify-center rounded border border-[var(--admin-border)] text-[var(--admin-text-muted)] hover:text-[var(--admin-text-primary)] hover:bg-stone-50 disabled:opacity-30 disabled:cursor-not-allowed transition-colors">
|
||||
className="flex h-7 w-7 items-center justify-center rounded border border-[var(--admin-border)] text-[var(--admin-text-muted)] hover:text-[var(--admin-text-primary)] hover:bg-stone-50 disabled:opacity-30 disabled:cursor-not-allowed transition-colors" aria-label="Next">
|
||||
<svg className="h-3 w-3" fill="none" viewBox="0 0 24 24" stroke="currentColor" strokeWidth={2}><path strokeLinecap="round" strokeLinejoin="round" d="M9 5l7 7-7 7" /></svg>
|
||||
</button>
|
||||
</div>
|
||||
@@ -168,10 +168,10 @@ export default function AbandonedCartDashboard({ brandId }: Props) {
|
||||
<td className="px-5 py-3.5 text-right">
|
||||
<div className="flex items-center justify-end gap-2">
|
||||
<button type="button" onClick={() => setSelectedCart(c)}
|
||||
className="text-xs text-[var(--admin-text-muted)] hover:text-[var(--admin-text-primary)] px-2 py-1 rounded-lg hover:bg-stone-100 transition-all">View</button>
|
||||
className="text-xs text-[var(--admin-text-muted)] hover:text-[var(--admin-text-primary)] px-2 py-1 rounded-lg hover:bg-stone-100 transition-all" aria-label="View">View</button>
|
||||
{c.status === "active" && (
|
||||
<button type="button" onClick={() => handleClose(c.id)} disabled={closing}
|
||||
className="text-xs text-amber-600 hover:text-amber-700 px-2 py-1 rounded-lg hover:bg-amber-50 transition-all">Close</button>
|
||||
className="text-xs text-amber-600 hover:text-amber-700 px-2 py-1 rounded-lg hover:bg-amber-50 transition-all" aria-label="Close">Close</button>
|
||||
)}
|
||||
{c.status === "active" && (
|
||||
<button type="button" onClick={() => handleResend(c.id)} disabled={resending === c.id}
|
||||
|
||||
@@ -118,7 +118,7 @@ export default function AdminStopsPanel({ stops, brandId }: Props) {
|
||||
<button type="button"
|
||||
onClick={() => setShowImport(true)}
|
||||
className="flex items-center gap-2 rounded-xl border border-stone-200 bg-white px-4 py-2.5 text-sm font-medium text-stone-600 hover:bg-stone-50 transition-colors"
|
||||
>
|
||||
aria-label="Upload Schedule">
|
||||
<svg className="h-4 w-4" fill="none" viewBox="0 0 24 24" stroke="currentColor" strokeWidth={2}>
|
||||
<path strokeLinecap="round" strokeLinejoin="round" d="M4 16v1a3 3 0 003 3h10a3 3 0 003-3v-1m-4-8l-4-4m0 0L8 8m4-4v12" />
|
||||
</svg>
|
||||
@@ -127,7 +127,7 @@ export default function AdminStopsPanel({ stops, brandId }: Props) {
|
||||
<button type="button"
|
||||
onClick={() => setShowAdd(true)}
|
||||
className="flex items-center gap-2 rounded-xl bg-emerald-600 hover:bg-emerald-500 px-4 py-2.5 text-sm font-semibold text-white transition-colors"
|
||||
>
|
||||
aria-label="Add Stop">
|
||||
<svg className="h-4 w-4" fill="none" viewBox="0 0 24 24" stroke="currentColor" strokeWidth={2.5}>
|
||||
<path strokeLinecap="round" strokeLinejoin="round" d="M12 4v16m8-8H4" />
|
||||
</svg>
|
||||
@@ -271,7 +271,7 @@ export default function AdminStopsPanel({ stops, brandId }: Props) {
|
||||
<button type="button"
|
||||
onClick={() => { setOpenMenu(null); setConfirmDelete(stop.id); }}
|
||||
className="w-full text-left px-4 py-2.5 text-sm text-red-600 hover:bg-red-50"
|
||||
>
|
||||
aria-label="Delete">
|
||||
Delete
|
||||
</button>
|
||||
</div>
|
||||
@@ -294,7 +294,7 @@ export default function AdminStopsPanel({ stops, brandId }: Props) {
|
||||
<button type="button"
|
||||
onClick={() => handleDelete(stop.id)}
|
||||
className="flex-1 rounded-lg bg-[var(--admin-danger)] px-3 py-2 text-xs font-medium text-white hover:bg-[var(--admin-danger-hover)]"
|
||||
>
|
||||
aria-label="Delete">
|
||||
Delete
|
||||
</button>
|
||||
</div>
|
||||
@@ -321,7 +321,7 @@ export default function AdminStopsPanel({ stops, brandId }: Props) {
|
||||
onClick={() => setPage((p) => Math.max(0, p - 1))}
|
||||
disabled={page === 0}
|
||||
className="flex h-9 w-9 items-center justify-center rounded-xl border border-stone-200 text-stone-500 hover:border-stone-300 hover:text-stone-700 hover:bg-white disabled:opacity-40 disabled:cursor-not-allowed"
|
||||
>
|
||||
aria-label="Previous">
|
||||
<svg className="h-4 w-4" fill="none" viewBox="0 0 24 24" stroke="currentColor" strokeWidth={2}>
|
||||
<path strokeLinecap="round" strokeLinejoin="round" d="M15 19l-7-7 7-7" />
|
||||
</svg>
|
||||
@@ -331,7 +331,7 @@ export default function AdminStopsPanel({ stops, brandId }: Props) {
|
||||
onClick={() => setPage((p) => Math.min(totalPages - 1, p + 1))}
|
||||
disabled={page >= totalPages - 1}
|
||||
className="flex h-9 w-9 items-center justify-center rounded-xl border border-stone-200 text-stone-500 hover:border-stone-300 hover:text-stone-700 hover:bg-white disabled:opacity-40 disabled:cursor-not-allowed"
|
||||
>
|
||||
aria-label="Next">
|
||||
<svg className="h-4 w-4" fill="none" viewBox="0 0 24 24" stroke="currentColor" strokeWidth={2}>
|
||||
<path strokeLinecap="round" strokeLinejoin="round" d="M9 5l7 7-7 7" />
|
||||
</svg>
|
||||
|
||||
@@ -167,7 +167,7 @@ export default function AdvancedShipping({ brandId }: { brandId: string }) {
|
||||
onClick={handleTest}
|
||||
disabled={saving}
|
||||
className="rounded-lg border border-[var(--admin-border)] px-4 py-2 text-xs font-medium text-[var(--admin-text-secondary)] hover:bg-stone-50 disabled:opacity-50"
|
||||
>
|
||||
aria-label="Test Connection">
|
||||
Test Connection
|
||||
</button>
|
||||
<button type="button"
|
||||
|
||||
@@ -140,7 +140,7 @@ export default function AdvancedSquareSync({ brandId }: { brandId: string }) {
|
||||
<button type="button"
|
||||
onClick={handleTest}
|
||||
className="rounded-lg border border-[var(--admin-border)] px-4 py-2 text-xs font-medium text-[var(--admin-text-secondary)] hover:bg-stone-50"
|
||||
>
|
||||
aria-label="Test Connection">
|
||||
Test Connection
|
||||
</button>
|
||||
<button type="button"
|
||||
|
||||
@@ -782,7 +782,7 @@ function NexusStateInput({
|
||||
type="button"
|
||||
onClick={() => removeState(code)}
|
||||
className="hover:text-amber-200"
|
||||
>
|
||||
aria-label="Close">
|
||||
<svg className="w-3 h-3" fill="none" viewBox="0 0 24 24" stroke="currentColor" strokeWidth={2}>
|
||||
<path strokeLinecap="round" strokeLinejoin="round" d="M6 18L18 6M6 6l12 12" />
|
||||
</svg>
|
||||
|
||||
@@ -178,7 +178,7 @@ function NewCampaignModal({
|
||||
<button type="button"
|
||||
onClick={onClose}
|
||||
className="px-4 py-2 text-sm font-medium text-[var(--admin-text-muted)] hover:text-[var(--admin-text-primary)] transition-colors"
|
||||
>
|
||||
aria-label="Cancel">
|
||||
Cancel
|
||||
</button>
|
||||
<button type="button"
|
||||
@@ -234,7 +234,7 @@ export default function CampaignListPanel({ initialCampaigns, brandId = "6429430
|
||||
<button type="button"
|
||||
onClick={() => setShowNewModal(true)}
|
||||
className="inline-flex items-center gap-1.5 rounded-lg bg-emerald-600 px-3 sm:px-4 py-2 text-xs sm:text-sm font-semibold text-white hover:bg-emerald-700 transition-colors"
|
||||
>
|
||||
aria-label="New Campaign">
|
||||
{Icons.plus("h-3.5 w-3.5 sm:h-4 sm:w-4")}
|
||||
<span>New Campaign</span>
|
||||
</button>
|
||||
|
||||
@@ -262,7 +262,7 @@ export default function ContactListPanel({
|
||||
<button
|
||||
type="submit"
|
||||
className="rounded-xl bg-emerald-600 px-5 py-2.5 text-sm font-semibold text-white hover:bg-emerald-700 active:bg-emerald-800 transition-colors shadow-sm"
|
||||
>
|
||||
aria-label="Search">
|
||||
Search
|
||||
</button>
|
||||
</form>
|
||||
@@ -357,7 +357,7 @@ export default function ContactListPanel({
|
||||
disabled={deleting === c.id}
|
||||
className="p-1.5 rounded-lg hover:bg-red-50 text-red-500 hover:text-red-700 disabled:opacity-50 transition-colors"
|
||||
title="Delete contact"
|
||||
>
|
||||
aria-label="Delete contact">
|
||||
{Icons.trash("h-4 w-4")}
|
||||
</button>
|
||||
</td>
|
||||
@@ -424,7 +424,7 @@ export default function ContactListPanel({
|
||||
onClick={() => handlePage(-1)}
|
||||
disabled={page === 0}
|
||||
className="inline-flex items-center gap-1.5 rounded-lg border border-stone-200 px-4 py-2 text-sm font-medium disabled:opacity-50 hover:bg-stone-50 transition-colors"
|
||||
>
|
||||
aria-label="Previous">
|
||||
{Icons.chevronLeft("h-4 w-4")}
|
||||
<span>Previous</span>
|
||||
</button>
|
||||
@@ -432,7 +432,7 @@ export default function ContactListPanel({
|
||||
onClick={() => handlePage(1)}
|
||||
disabled={(page + 1) * limit >= total}
|
||||
className="inline-flex items-center gap-1.5 rounded-lg border border-stone-200 px-4 py-2 text-sm font-medium disabled:opacity-50 hover:bg-stone-50 transition-colors"
|
||||
>
|
||||
aria-label="Next">
|
||||
<span>Next</span>
|
||||
{Icons.chevronRight("h-4 w-4")}
|
||||
</button>
|
||||
|
||||
@@ -270,7 +270,7 @@ export default function CreateUserModal({ isOpen, onClose, onSuccess, brands, cu
|
||||
<button type="button"
|
||||
onClick={handleClose}
|
||||
className="w-full sm:w-auto rounded-xl bg-[var(--admin-accent)] px-4 sm:px-5 py-2.5 text-sm font-medium text-white hover:bg-[var(--admin-accent-hover)] transition-colors"
|
||||
>
|
||||
aria-label="Done">
|
||||
Done
|
||||
</button>
|
||||
</div>
|
||||
@@ -291,7 +291,7 @@ export default function CreateUserModal({ isOpen, onClose, onSuccess, brands, cu
|
||||
{error && (
|
||||
<div className="flex items-start justify-between rounded-lg bg-red-50 p-3 sm:p-4 text-sm text-red-700 gap-3 border border-red-200">
|
||||
<span className="text-xs sm:text-sm">{error}</span>
|
||||
<button type="button" onClick={() => setError(null)} className="text-red-400 hover:text-red-600 shrink-0">
|
||||
<button type="button" onClick={() => setError(null)} className="text-red-400 hover:text-red-600 shrink-0" aria-label="Close">
|
||||
<svg className="h-4 w-4" fill="none" viewBox="0 0 24 24" stroke="currentColor" strokeWidth={2.5}>
|
||||
<path strokeLinecap="round" strokeLinejoin="round" d="M6 18L18 6M6 6l12 12" />
|
||||
</svg>
|
||||
@@ -385,7 +385,7 @@ export default function CreateUserModal({ isOpen, onClose, onSuccess, brands, cu
|
||||
checked={role === r}
|
||||
onChange={() => setRole(r)}
|
||||
className="accent-[var(--admin-accent)]"
|
||||
/>
|
||||
aria-label="Role"/>
|
||||
<div>
|
||||
<span className="font-medium text-[var(--admin-text-primary)] capitalize text-sm">{r.replace("_", " ")}</span>
|
||||
<p className="text-xs text-[var(--admin-text-muted)]">{roleDescriptions[r]}</p>
|
||||
@@ -441,7 +441,7 @@ export default function CreateUserModal({ isOpen, onClose, onSuccess, brands, cu
|
||||
onClick={handleClose}
|
||||
disabled={saving}
|
||||
className="w-full sm:w-auto rounded-xl border border-[var(--admin-border)] px-4 sm:px-5 py-2.5 text-sm font-medium text-[var(--admin-text-secondary)] hover:bg-[var(--admin-bg-subtle)] transition-colors disabled:opacity-50"
|
||||
>
|
||||
aria-label="Cancel">
|
||||
Cancel
|
||||
</button>
|
||||
<button type="button"
|
||||
|
||||
@@ -206,7 +206,7 @@ export default function LocationsTab({ locations, brandId }: Props) {
|
||||
{deleteError && (
|
||||
<div className="mx-5 my-3 rounded-lg border border-[var(--admin-danger)]/30 bg-[var(--admin-danger)]/10 px-4 py-3 text-sm text-[var(--admin-danger)]">
|
||||
{deleteError}{" "}
|
||||
<button type="button" onClick={() => setDeleteError(null)} className="underline hover:no-underline">
|
||||
<button type="button" onClick={() => setDeleteError(null)} className="underline hover:no-underline" aria-label="Dismiss">
|
||||
Dismiss
|
||||
</button>
|
||||
</div>
|
||||
@@ -334,7 +334,7 @@ export default function LocationsTab({ locations, brandId }: Props) {
|
||||
onClick={() => setEditing(loc as LocationForEdit)}
|
||||
className="rounded-lg p-1.5 text-[var(--admin-text-muted)] hover:text-[var(--admin-text-primary)] hover:bg-[var(--admin-bg-subtle)] transition-colors"
|
||||
title="Edit venue"
|
||||
>
|
||||
aria-label="Edit venue">
|
||||
<svg className="h-4 w-4" fill="none" viewBox="0 0 24 24" stroke="currentColor" strokeWidth={2}>
|
||||
<path strokeLinecap="round" strokeLinejoin="round" d="M11 5H6a2 2 0 00-2 2v11a2 2 0 002 2h11a2 2 0 002-2v-5m-1.414-9.414a2 2 0 112.828 2.828L11.828 15H9v-2.828l8.586-8.586z" />
|
||||
</svg>
|
||||
@@ -344,7 +344,7 @@ export default function LocationsTab({ locations, brandId }: Props) {
|
||||
disabled={pendingDeleteId === loc.id}
|
||||
className="rounded-lg p-1.5 text-[var(--admin-text-muted)] hover:text-red-600 hover:bg-red-50 transition-colors disabled:opacity-50"
|
||||
title="Delete venue"
|
||||
>
|
||||
aria-label="Delete venue">
|
||||
{pendingDeleteId === loc.id ? (
|
||||
<svg className="h-4 w-4 animate-spin" fill="none" viewBox="0 0 24 24">
|
||||
<circle className="opacity-25" cx="12" cy="12" r="10" stroke="currentColor" strokeWidth="4" />
|
||||
|
||||
@@ -477,7 +477,7 @@ export default function MessageLogPanel({ brandId }: { brandId?: string }) {
|
||||
onClick={() => setPage((p) => Math.max(1, p - 1))}
|
||||
disabled={page === 1}
|
||||
className="px-4 py-2 text-sm border border-stone-200 rounded-xl bg-white text-stone-700 hover:bg-stone-50 disabled:opacity-50 disabled:cursor-not-allowed transition-colors"
|
||||
>
|
||||
aria-label="Previous">
|
||||
Previous
|
||||
</button>
|
||||
{Array.from({ length: Math.min(5, totalPages) }, (_, i) => {
|
||||
@@ -500,7 +500,7 @@ export default function MessageLogPanel({ brandId }: { brandId?: string }) {
|
||||
onClick={() => setPage((p) => Math.min(totalPages, p + 1))}
|
||||
disabled={page === totalPages}
|
||||
className="px-4 py-2 text-sm border border-stone-200 rounded-xl bg-white text-stone-700 hover:bg-stone-50 disabled:opacity-50 disabled:cursor-not-allowed transition-colors"
|
||||
>
|
||||
aria-label="Next">
|
||||
Next
|
||||
</button>
|
||||
</div>
|
||||
|
||||
@@ -101,7 +101,7 @@ export default function ProductTableBody({
|
||||
<button type="button"
|
||||
onClick={() => setDeleteError(null)}
|
||||
className="ml-2 underline hover:no-underline"
|
||||
>
|
||||
aria-label="Dismiss">
|
||||
Dismiss
|
||||
</button>
|
||||
</div>
|
||||
|
||||
@@ -81,7 +81,7 @@ export default function ProductTableClient({ products }: Props) {
|
||||
{deleteError && (
|
||||
<div className="mx-5 my-3 rounded-lg border border-red-200 bg-red-50 px-4 py-3 text-sm text-red-700">
|
||||
{deleteError}{" "}
|
||||
<button type="button" onClick={() => setDeleteError(null)} className="underline hover:no-underline">
|
||||
<button type="button" onClick={() => setDeleteError(null)} className="underline hover:no-underline" aria-label="Dismiss">
|
||||
Dismiss
|
||||
</button>
|
||||
</div>
|
||||
|
||||
@@ -573,7 +573,7 @@ function TableView({
|
||||
<button type="button"
|
||||
onClick={onDeleteCancel}
|
||||
className="flex-1 rounded-lg border border-[var(--admin-border)] bg-[var(--admin-card-bg)] px-3 py-2 text-xs font-semibold text-[var(--admin-text-primary)] hover:bg-[var(--admin-bg)]"
|
||||
>
|
||||
aria-label="Cancel">
|
||||
Cancel
|
||||
</button>
|
||||
<AdminButton
|
||||
@@ -727,7 +727,7 @@ function CardView({
|
||||
<button type="button"
|
||||
onClick={onDeleteCancel}
|
||||
className="flex-1 rounded-lg border border-[var(--admin-border)] px-3 py-2 text-xs font-semibold text-[var(--admin-text-primary)] hover:bg-[var(--admin-bg)]"
|
||||
>
|
||||
aria-label="Cancel">
|
||||
Cancel
|
||||
</button>
|
||||
<button type="button"
|
||||
|
||||
@@ -682,7 +682,7 @@ export default function ReportsDashboard({
|
||||
<h3 className="font-semibold text-white">AI Report Analysis</h3>
|
||||
<span className="text-xs text-violet-200 ml-1">· {activeTab.replace("-", " ")}</span>
|
||||
</div>
|
||||
<button type="button" onClick={closeExplanation} className="text-violet-200 hover:text-white p-1">
|
||||
<button type="button" onClick={closeExplanation} className="text-violet-200 hover:text-white p-1" aria-label="Close">
|
||||
<svg className="w-4 h-4" fill="none" viewBox="0 0 24 24" stroke="currentColor">
|
||||
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M6 18L18 6M6 6l12 12" />
|
||||
</svg>
|
||||
|
||||
@@ -150,7 +150,7 @@ export default function ScheduleImportModal({ brandId, onClose, onComplete }: Pr
|
||||
? "bg-violet-100 border border-violet-300 text-violet-700"
|
||||
: "bg-white border border-stone-200 text-stone-600 hover:border-stone-300"
|
||||
}`}
|
||||
>
|
||||
aria-label="Use AI for text/PDF parsing">
|
||||
<span className={`flex h-5 w-5 items-center justify-center rounded-full text-xs ${useAI ? "bg-violet-500 text-white" : "bg-stone-200 text-stone-500"}`}>
|
||||
{useAI ? "✓" : "○"}
|
||||
</span>
|
||||
@@ -298,7 +298,7 @@ export default function ScheduleImportModal({ brandId, onClose, onComplete }: Pr
|
||||
<button type="button"
|
||||
onClick={() => removeStop(idx)}
|
||||
className="flex h-6 w-6 items-center justify-center rounded-lg text-red-500 hover:bg-red-50 hover:text-red-700 transition-colors"
|
||||
>
|
||||
aria-label="Close">
|
||||
<svg className="h-3.5 w-3.5" fill="none" viewBox="0 0 24 24" stroke="currentColor" strokeWidth={2}>
|
||||
<path strokeLinecap="round" strokeLinejoin="round" d="M6 18L18 6M6 6l12 12" />
|
||||
</svg>
|
||||
@@ -318,14 +318,14 @@ export default function ScheduleImportModal({ brandId, onClose, onComplete }: Pr
|
||||
<button type="button"
|
||||
onClick={() => { setStep("idle"); setParsedStops([]); setWarnings([]); }}
|
||||
className="rounded-xl border border-stone-200 bg-white px-4 py-2 text-sm font-semibold text-stone-600 hover:bg-stone-50 transition-colors"
|
||||
>
|
||||
aria-label="Cancel">
|
||||
Cancel
|
||||
</button>
|
||||
<button type="button"
|
||||
onClick={handleImport}
|
||||
disabled={parsedStops.length === 0}
|
||||
className="rounded-xl bg-emerald-600 hover:bg-emerald-700 px-5 py-2 text-sm font-bold text-white disabled:opacity-50 transition-colors"
|
||||
>
|
||||
aria-label="Create Stop">
|
||||
Create {parsedStops.length} Stop{parsedStops.length !== 1 ? "s" : ""}
|
||||
</button>
|
||||
</div>
|
||||
@@ -363,7 +363,7 @@ export default function ScheduleImportModal({ brandId, onClose, onComplete }: Pr
|
||||
<button type="button"
|
||||
onClick={onClose}
|
||||
className="rounded-xl bg-emerald-600 hover:bg-emerald-700 px-5 py-2.5 text-sm font-bold text-white transition-colors"
|
||||
>
|
||||
aria-label="Back to Stops">
|
||||
Back to Stops
|
||||
</button>
|
||||
</div>
|
||||
@@ -380,7 +380,7 @@ export default function ScheduleImportModal({ brandId, onClose, onComplete }: Pr
|
||||
<button type="button"
|
||||
onClick={() => setStep("idle")}
|
||||
className="rounded-xl border border-stone-200 bg-white px-4 py-2 text-sm font-semibold text-stone-600 hover:bg-stone-50 transition-colors"
|
||||
>
|
||||
aria-label="Try Again">
|
||||
Try Again
|
||||
</button>
|
||||
</div>
|
||||
|
||||
@@ -51,7 +51,7 @@ function Accordion({ title, id, description, defaultOpen = false, accentColor =
|
||||
onClick={() => setOpen(!open)}
|
||||
className="w-full flex items-center justify-between px-5 py-4 hover:bg-stone-50 transition-colors"
|
||||
aria-expanded={open}
|
||||
>
|
||||
aria-label=")}">
|
||||
<div className="flex items-center gap-3">
|
||||
<span className="text-sm font-semibold text-stone-900">{title}</span>
|
||||
{description && (
|
||||
@@ -412,13 +412,13 @@ export default function SettingsSections({ brandId, workersOnly, tasksOnly }: Pr
|
||||
onKeyDown={e => e.key === "Enter" && (addEmail(), e.preventDefault())}
|
||||
className="flex-1 px-4 py-3 rounded-xl border border-stone-200 bg-white text-stone-900 placeholder:text-stone-400 focus:outline-none focus:border-emerald-500"
|
||||
placeholder="manager@farm.com" />
|
||||
<button type="button" onClick={addEmail} className="px-5 py-3 rounded-xl bg-emerald-600 hover:bg-emerald-500 text-white font-semibold text-sm">Add</button>
|
||||
<button type="button" onClick={addEmail} className="px-5 py-3 rounded-xl bg-emerald-600 hover:bg-emerald-500 text-white font-semibold text-sm" aria-label="Add">Add</button>
|
||||
</div>
|
||||
<div className="flex flex-wrap gap-2 mt-2">
|
||||
{notificationEmails.map(e => (
|
||||
<span key={e} className="inline-flex items-center gap-1.5 bg-stone-100 text-stone-700 text-xs px-2.5 py-1 rounded-lg">
|
||||
{e}
|
||||
<button type="button" onClick={() => removeEmail(e)} className="text-stone-400 hover:text-red-500 ml-1">
|
||||
<button type="button" onClick={() => removeEmail(e)} className="text-stone-400 hover:text-red-500 ml-1" aria-label="Close">
|
||||
<svg className="h-3 w-3" fill="none" viewBox="0 0 24 24" stroke="currentColor" strokeWidth={2}>
|
||||
<path strokeLinecap="round" strokeLinejoin="round" d="M6 18L18 6M6 6l12 12" />
|
||||
</svg>
|
||||
@@ -435,13 +435,13 @@ export default function SettingsSections({ brandId, workersOnly, tasksOnly }: Pr
|
||||
onKeyDown={e => e.key === "Enter" && (addSms(), e.preventDefault())}
|
||||
className="flex-1 px-4 py-3 rounded-xl border border-stone-200 bg-white text-stone-900 placeholder:text-stone-400 focus:outline-none focus:border-emerald-500"
|
||||
placeholder="+1234567890" />
|
||||
<button type="button" onClick={addSms} className="px-5 py-3 rounded-xl bg-emerald-600 hover:bg-emerald-500 text-white font-semibold text-sm">Add</button>
|
||||
<button type="button" onClick={addSms} className="px-5 py-3 rounded-xl bg-emerald-600 hover:bg-emerald-500 text-white font-semibold text-sm" aria-label="Add">Add</button>
|
||||
</div>
|
||||
<div className="flex flex-wrap gap-2 mt-2">
|
||||
{notificationSmsNumbers.map(n => (
|
||||
<span key={n} className="inline-flex items-center gap-1.5 bg-stone-100 text-stone-700 text-xs px-2.5 py-1 rounded-lg">
|
||||
{n}
|
||||
<button type="button" onClick={() => removeSms(n)} className="text-stone-400 hover:text-red-500 ml-1">
|
||||
<button type="button" onClick={() => removeSms(n)} className="text-stone-400 hover:text-red-500 ml-1" aria-label="Close">
|
||||
<svg className="h-3 w-3" fill="none" viewBox="0 0 24 24" stroke="currentColor" strokeWidth={2}>
|
||||
<path strokeLinecap="round" strokeLinejoin="round" d="M6 18L18 6M6 6l12 12" />
|
||||
</svg>
|
||||
@@ -509,7 +509,7 @@ export default function SettingsSections({ brandId, workersOnly, tasksOnly }: Pr
|
||||
<div className="flex items-center justify-between mb-4">
|
||||
<p className="text-xs text-stone-500">Manage time tracking workers and PIN codes.</p>
|
||||
<button type="button" onClick={openAddWorker}
|
||||
className="text-xs bg-emerald-600 hover:bg-emerald-500 text-white px-3 py-1.5 rounded-lg font-semibold transition-all">+ Add Worker</button>
|
||||
className="text-xs bg-emerald-600 hover:bg-emerald-500 text-white px-3 py-1.5 rounded-lg font-semibold transition-all" aria-label="+ Add Worker">+ Add Worker</button>
|
||||
</div>
|
||||
<div className="bg-white border border-stone-200 rounded-xl overflow-hidden">
|
||||
<table className="w-full text-sm">
|
||||
@@ -539,9 +539,9 @@ export default function SettingsSections({ brandId, workersOnly, tasksOnly }: Pr
|
||||
<td className="px-4 py-3.5 text-stone-400 text-xs hidden md:table-cell">{w.last_used_at ? new Date(w.last_used_at).toLocaleDateString() : "—"}</td>
|
||||
<td className="px-4 py-3.5 text-right">
|
||||
<div className="flex items-center justify-end gap-1">
|
||||
<button type="button" onClick={() => openEditWorker(w)} className="text-xs text-stone-500 hover:text-stone-900 px-2 py-1 rounded-lg hover:bg-stone-100 transition-all">Edit</button>
|
||||
<button type="button" onClick={() => handleResetPin(w.id)} className="text-xs text-amber-600 hover:text-amber-800 px-2 py-1 rounded-lg hover:bg-amber-50 transition-all">Reset PIN</button>
|
||||
<button type="button" onClick={() => handleDeleteWorker(w.id)} className="text-xs text-red-500 hover:text-red-700 px-2 py-1 rounded-lg hover:bg-red-50 transition-all">Delete</button>
|
||||
<button type="button" onClick={() => openEditWorker(w)} className="text-xs text-stone-500 hover:text-stone-900 px-2 py-1 rounded-lg hover:bg-stone-100 transition-all" aria-label="Edit">Edit</button>
|
||||
<button type="button" onClick={() => handleResetPin(w.id)} className="text-xs text-amber-600 hover:text-amber-800 px-2 py-1 rounded-lg hover:bg-amber-50 transition-all" aria-label="Reset PIN">Reset PIN</button>
|
||||
<button type="button" onClick={() => handleDeleteWorker(w.id)} className="text-xs text-red-500 hover:text-red-700 px-2 py-1 rounded-lg hover:bg-red-50 transition-all" aria-label="Delete">Delete</button>
|
||||
</div>
|
||||
</td>
|
||||
</tr>
|
||||
@@ -566,7 +566,7 @@ export default function SettingsSections({ brandId, workersOnly, tasksOnly }: Pr
|
||||
<div className="flex items-center justify-between mb-4">
|
||||
<p className="text-xs text-stone-500">Define tasks workers can clock into.</p>
|
||||
<button type="button" onClick={openAddTask}
|
||||
className="text-xs bg-amber-500 hover:bg-amber-600 text-white px-3 py-1.5 rounded-lg font-semibold transition-all">+ Add Task</button>
|
||||
className="text-xs bg-amber-500 hover:bg-amber-600 text-white px-3 py-1.5 rounded-lg font-semibold transition-all" aria-label="+ Add Task">+ Add Task</button>
|
||||
</div>
|
||||
<div className="bg-white border border-stone-200 rounded-xl overflow-hidden">
|
||||
<table className="w-full text-sm">
|
||||
@@ -596,8 +596,8 @@ export default function SettingsSections({ brandId, workersOnly, tasksOnly }: Pr
|
||||
</td>
|
||||
<td className="px-4 py-3.5 text-right">
|
||||
<div className="flex items-center justify-end gap-1">
|
||||
<button type="button" onClick={() => openEditTask(t)} className="text-xs text-stone-500 hover:text-stone-900 px-2 py-1 rounded-lg hover:bg-stone-100 transition-all">Edit</button>
|
||||
<button type="button" onClick={() => handleDeleteTask(t.id)} className="text-xs text-red-500 hover:text-red-700 px-2 py-1 rounded-lg hover:bg-red-50 transition-all">Delete</button>
|
||||
<button type="button" onClick={() => openEditTask(t)} className="text-xs text-stone-500 hover:text-stone-900 px-2 py-1 rounded-lg hover:bg-stone-100 transition-all" aria-label="Edit">Edit</button>
|
||||
<button type="button" onClick={() => handleDeleteTask(t.id)} className="text-xs text-red-500 hover:text-red-700 px-2 py-1 rounded-lg hover:bg-red-50 transition-all" aria-label="Delete">Delete</button>
|
||||
</div>
|
||||
</td>
|
||||
</tr>
|
||||
@@ -615,7 +615,7 @@ export default function SettingsSections({ brandId, workersOnly, tasksOnly }: Pr
|
||||
<div className="bg-white border border-stone-200 rounded-2xl w-full max-w-md shadow-xl">
|
||||
<div className="px-6 py-4 border-b border-stone-100 flex items-center justify-between">
|
||||
<h3 className="text-lg font-bold text-stone-900">{editingWorker ? "Edit Worker" : "Add Worker"}</h3>
|
||||
<button type="button" onClick={() => setShowWorkerModal(false)} className="text-stone-400 hover:text-stone-600">
|
||||
<button type="button" onClick={() => setShowWorkerModal(false)} className="text-stone-400 hover:text-stone-600" aria-label="Close">
|
||||
<svg className="h-5 w-5" fill="none" viewBox="0 0 24 24" stroke="currentColor" strokeWidth={2}>
|
||||
<path strokeLinecap="round" strokeLinejoin="round" d="M6 18L18 6M6 6l12 12" />
|
||||
</svg>
|
||||
@@ -661,14 +661,14 @@ export default function SettingsSections({ brandId, workersOnly, tasksOnly }: Pr
|
||||
</div>
|
||||
{editingWorker && (
|
||||
<div className="pt-2 border-t border-stone-100">
|
||||
<button type="button" onClick={() => handleResetPin(editingWorker.id)} className="text-xs text-amber-600 hover:text-amber-800 font-semibold underline underline-offset-2">
|
||||
<button type="button" onClick={() => handleResetPin(editingWorker.id)} className="text-xs text-amber-600 hover:text-amber-800 font-semibold underline underline-offset-2" aria-label="Reset PIN for this worker">
|
||||
Reset PIN for this worker
|
||||
</button>
|
||||
</div>
|
||||
)}
|
||||
<div className="flex gap-3 pt-2">
|
||||
<button type="button" onClick={() => setShowWorkerModal(false)}
|
||||
className="flex-1 py-3 rounded-xl font-semibold text-sm text-stone-500 hover:text-stone-700 border border-stone-200 hover:border-stone-300 transition-all">
|
||||
className="flex-1 py-3 rounded-xl font-semibold text-sm text-stone-500 hover:text-stone-700 border border-stone-200 hover:border-stone-300 transition-all" aria-label="Cancel">
|
||||
Cancel
|
||||
</button>
|
||||
<button type="button" onClick={handleSaveWorker} disabled={submitting}
|
||||
@@ -687,7 +687,7 @@ export default function SettingsSections({ brandId, workersOnly, tasksOnly }: Pr
|
||||
<div className="bg-white border border-stone-200 rounded-2xl w-full max-w-md shadow-xl">
|
||||
<div className="px-6 py-4 border-b border-stone-100 flex items-center justify-between">
|
||||
<h3 className="text-lg font-bold text-stone-900">{editingTask ? "Edit Task" : "Add Task"}</h3>
|
||||
<button type="button" onClick={() => setShowTaskModal(false)} className="text-stone-400 hover:text-stone-600">
|
||||
<button type="button" onClick={() => setShowTaskModal(false)} className="text-stone-400 hover:text-stone-600" aria-label="Close">
|
||||
<svg className="h-5 w-5" fill="none" viewBox="0 0 24 24" stroke="currentColor" strokeWidth={2}>
|
||||
<path strokeLinecap="round" strokeLinejoin="round" d="M6 18L18 6M6 6l12 12" />
|
||||
</svg>
|
||||
@@ -730,7 +730,7 @@ export default function SettingsSections({ brandId, workersOnly, tasksOnly }: Pr
|
||||
</div>
|
||||
<div className="flex gap-3 pt-2">
|
||||
<button type="button" onClick={() => setShowTaskModal(false)}
|
||||
className="flex-1 py-3 rounded-xl font-semibold text-sm text-stone-500 hover:text-stone-700 border border-stone-200 hover:border-stone-300 transition-all">
|
||||
className="flex-1 py-3 rounded-xl font-semibold text-sm text-stone-500 hover:text-stone-700 border border-stone-200 hover:border-stone-300 transition-all" aria-label="Cancel">
|
||||
Cancel
|
||||
</button>
|
||||
<button type="button" onClick={handleSaveTask} disabled={submitting}
|
||||
|
||||
@@ -365,7 +365,7 @@ export default function StopTableClient({ stops, hideInternalFilterBar = false }
|
||||
<button type="button"
|
||||
onClick={() => setSelectedStops(new Set())}
|
||||
className="text-xs text-stone-500 hover:text-stone-700"
|
||||
>
|
||||
aria-label="Clear">
|
||||
Clear
|
||||
</button>
|
||||
</div>
|
||||
@@ -384,7 +384,7 @@ export default function StopTableClient({ stops, hideInternalFilterBar = false }
|
||||
{deleteError && (
|
||||
<div className="mb-4 rounded-lg border border-red-500/30 bg-red-50 px-4 py-2.5 text-sm text-red-700">
|
||||
{deleteError}{" "}
|
||||
<button type="button" onClick={() => setDeleteError(null)} className="underline hover:no-underline">
|
||||
<button type="button" onClick={() => setDeleteError(null)} className="underline hover:no-underline" aria-label="Dismiss">
|
||||
Dismiss
|
||||
</button>
|
||||
</div>
|
||||
@@ -888,7 +888,7 @@ function StopRow({
|
||||
setConfirmDelete(stop.id);
|
||||
}}
|
||||
className="w-full text-left px-4 py-2.5 text-sm text-red-600 hover:bg-red-50 transition-colors flex items-center gap-2"
|
||||
>
|
||||
aria-label="Delete">
|
||||
<svg className="h-4 w-4 text-red-400" fill="none" viewBox="0 0 24 24" stroke="currentColor" strokeWidth={2}>
|
||||
<path strokeLinecap="round" strokeLinejoin="round" d="M19 7l-.867 12.142A2 2 0 0116.138 21H7.862a2 2 0 01-1.995-1.858L5 7m5 4v6m4-6v6m1-10V4a1 1 0 00-1-1h-4a1 1 0 00-1 1v3M4 7h16" />
|
||||
</svg>
|
||||
@@ -1066,7 +1066,7 @@ function StopCard({
|
||||
<button type="button"
|
||||
onClick={() => setConfirmDelete(false)}
|
||||
className="flex-1 rounded-lg border border-[var(--admin-border)] px-3 py-2 text-xs font-semibold text-stone-700 hover:bg-stone-50"
|
||||
>
|
||||
aria-label="Cancel">
|
||||
Cancel
|
||||
</button>
|
||||
<button type="button"
|
||||
|
||||
@@ -345,7 +345,7 @@ export default function TimeTrackingSettingsClient({ brandId }: TimeTrackingSett
|
||||
<h3 className="text-sm font-semibold text-[var(--admin-text-primary)]">Export Data</h3>
|
||||
<div className="grid grid-cols-2 gap-3">
|
||||
<button type="button" onClick={() => triggerDownload(buildExportUrl("quickbooks"))}
|
||||
className="flex items-center gap-3 px-4 py-3 rounded-xl bg-[var(--admin-bg-subtle)] border border-[var(--admin-border)] hover:border-[var(--admin-accent)] transition-all text-left">
|
||||
className="flex items-center gap-3 px-4 py-3 rounded-xl bg-[var(--admin-bg-subtle)] border border-[var(--admin-border)] hover:border-[var(--admin-accent)] transition-all text-left" aria-label="QuickBooks Time import CSV">
|
||||
<span className="text-[var(--admin-text-muted)]">{Icons.clock("h-5 w-5")}</span>
|
||||
<div>
|
||||
<p className="text-sm font-semibold text-[var(--admin-text-primary)]">QuickBooks</p>
|
||||
@@ -353,7 +353,7 @@ export default function TimeTrackingSettingsClient({ brandId }: TimeTrackingSett
|
||||
</div>
|
||||
</button>
|
||||
<button type="button" onClick={() => triggerDownload(buildExportUrl("payroll"))}
|
||||
className="flex items-center gap-3 px-4 py-3 rounded-xl bg-[var(--admin-bg-subtle)] border border-[var(--admin-border)] hover:border-blue-500 transition-all text-left">
|
||||
className="flex items-center gap-3 px-4 py-3 rounded-xl bg-[var(--admin-bg-subtle)] border border-[var(--admin-border)] hover:border-blue-500 transition-all text-left" aria-label="Payroll ADP / Gusto / Generic">
|
||||
<span className="text-[var(--admin-text-muted)]">{Icons.dollarSign("h-5 w-5")}</span>
|
||||
<div>
|
||||
<p className="text-sm font-semibold text-[var(--admin-text-primary)]">Payroll</p>
|
||||
@@ -361,7 +361,7 @@ export default function TimeTrackingSettingsClient({ brandId }: TimeTrackingSett
|
||||
</div>
|
||||
</button>
|
||||
<button type="button" onClick={() => triggerDownload(buildExportUrl("detailed"))}
|
||||
className="flex items-center gap-3 px-4 py-3 rounded-xl bg-[var(--admin-bg-subtle)] border border-[var(--admin-border)] hover:border-violet-500 transition-all text-left">
|
||||
className="flex items-center gap-3 px-4 py-3 rounded-xl bg-[var(--admin-bg-subtle)] border border-[var(--admin-border)] hover:border-violet-500 transition-all text-left" aria-label="Detailed Log Full audit report">
|
||||
<span className="text-[var(--admin-text-muted)]">{Icons.clipboard("h-5 w-5")}</span>
|
||||
<div>
|
||||
<p className="text-sm font-semibold text-[var(--admin-text-primary)]">Detailed Log</p>
|
||||
@@ -369,7 +369,7 @@ export default function TimeTrackingSettingsClient({ brandId }: TimeTrackingSett
|
||||
</div>
|
||||
</button>
|
||||
<button type="button" onClick={() => triggerDownload(buildExportUrl("summary"))}
|
||||
className="flex items-center gap-3 px-4 py-3 rounded-xl bg-[var(--admin-bg-subtle)] border border-[var(--admin-border)] hover:border-amber-500 transition-all text-left">
|
||||
className="flex items-center gap-3 px-4 py-3 rounded-xl bg-[var(--admin-bg-subtle)] border border-[var(--admin-border)] hover:border-amber-500 transition-all text-left" aria-label="Period Summary Per-worker totals">
|
||||
<span className="text-[var(--admin-text-muted)]">{Icons.chart("h-5 w-5")}</span>
|
||||
<div>
|
||||
<p className="text-sm font-semibold text-[var(--admin-text-primary)]">Period Summary</p>
|
||||
@@ -629,7 +629,7 @@ export default function TimeTrackingSettingsClient({ brandId }: TimeTrackingSett
|
||||
{notificationEmails.map(e => (
|
||||
<span key={e} className="inline-flex items-center gap-1 bg-[var(--admin-bg-subtle)] text-[var(--admin-text-secondary)] text-xs px-2 py-1 rounded-lg">
|
||||
{e}
|
||||
<button type="button" onClick={() => removeEmail(e)} className="text-[var(--admin-text-muted)] hover:text-red-500 ml-1">×</button>
|
||||
<button type="button" onClick={() => removeEmail(e)} className="text-[var(--admin-text-muted)] hover:text-red-500 ml-1" aria-label="×">×</button>
|
||||
</span>
|
||||
))}
|
||||
</div>
|
||||
@@ -648,7 +648,7 @@ export default function TimeTrackingSettingsClient({ brandId }: TimeTrackingSett
|
||||
{notificationSmsNumbers.map(n => (
|
||||
<span key={n} className="inline-flex items-center gap-1 bg-[var(--admin-bg-subtle)] text-[var(--admin-text-secondary)] text-xs px-2 py-1 rounded-lg">
|
||||
{n}
|
||||
<button type="button" onClick={() => removeSms(n)} className="text-[var(--admin-text-muted)] hover:text-red-500 ml-1">×</button>
|
||||
<button type="button" onClick={() => removeSms(n)} className="text-[var(--admin-text-muted)] hover:text-red-500 ml-1" aria-label="×">×</button>
|
||||
</span>
|
||||
))}
|
||||
</div>
|
||||
@@ -673,7 +673,7 @@ export default function TimeTrackingSettingsClient({ brandId }: TimeTrackingSett
|
||||
<div className="bg-white border border-[var(--admin-border)] rounded-2xl w-full max-w-md shadow-xl">
|
||||
<div className="px-6 py-4 border-b border-[var(--admin-border)] flex items-center justify-between">
|
||||
<h3 className="text-lg font-bold text-[var(--admin-text-primary)]">{editingWorker ? "Edit Worker" : "Add Worker"}</h3>
|
||||
<button type="button" onClick={() => setShowWorkerModal(false)} className="text-[var(--admin-text-muted)] hover:text-[var(--admin-text-primary)] text-xl leading-none">×</button>
|
||||
<button type="button" onClick={() => setShowWorkerModal(false)} className="text-[var(--admin-text-muted)] hover:text-[var(--admin-text-primary)] text-xl leading-none" aria-label="×">×</button>
|
||||
</div>
|
||||
<div className="p-6 space-y-4">
|
||||
{workerError && <div className="bg-red-50 border border-red-200 rounded-xl py-3 px-4 text-red-700 text-sm">{workerError}</div>}
|
||||
@@ -739,7 +739,7 @@ export default function TimeTrackingSettingsClient({ brandId }: TimeTrackingSett
|
||||
<div className="bg-white border border-[var(--admin-border)] rounded-2xl w-full max-w-md shadow-xl">
|
||||
<div className="px-6 py-4 border-b border-[var(--admin-border)] flex items-center justify-between">
|
||||
<h3 className="text-lg font-bold text-[var(--admin-text-primary)]">{editingTask ? "Edit Task" : "Add Task"}</h3>
|
||||
<button type="button" onClick={() => setShowTaskModal(false)} className="text-[var(--admin-text-muted)] hover:text-[var(--admin-text-primary)] text-xl leading-none">×</button>
|
||||
<button type="button" onClick={() => setShowTaskModal(false)} className="text-[var(--admin-text-muted)] hover:text-[var(--admin-text-primary)] text-xl leading-none" aria-label="×">×</button>
|
||||
</div>
|
||||
<div className="p-6 space-y-4">
|
||||
{taskError && <div className="bg-red-50 border border-red-200 rounded-xl py-3 px-4 text-red-700 text-sm">{taskError}</div>}
|
||||
|
||||
@@ -345,7 +345,7 @@ export default function UsersPage({ initialUsers, brands, currentUser, onUserCre
|
||||
<button type="button"
|
||||
onClick={openCreate}
|
||||
className="rounded-lg bg-stone-900 px-4 py-2 text-sm font-medium text-white hover:bg-stone-800"
|
||||
>
|
||||
aria-label="+ Create User">
|
||||
+ Create User
|
||||
</button>
|
||||
)}
|
||||
@@ -367,7 +367,7 @@ export default function UsersPage({ initialUsers, brands, currentUser, onUserCre
|
||||
{error && (
|
||||
<div className="mb-4 flex items-start justify-between rounded-lg bg-red-50 p-4 text-sm text-red-700 gap-3 border border-red-200">
|
||||
<span>{error}</span>
|
||||
<button type="button" onClick={() => setError(null)} className="text-red-400 hover:text-red-600 shrink-0">
|
||||
<button type="button" onClick={() => setError(null)} className="text-red-400 hover:text-red-600 shrink-0" aria-label="Close">
|
||||
<svg className="h-4 w-4" fill="none" viewBox="0 0 24 24" stroke="currentColor" strokeWidth={2.5}>
|
||||
<path strokeLinecap="round" strokeLinejoin="round" d="M6 18L18 6M6 6l12 12" />
|
||||
</svg>
|
||||
@@ -416,7 +416,7 @@ export default function UsersPage({ initialUsers, brands, currentUser, onUserCre
|
||||
<button type="button"
|
||||
onClick={() => openEdit(user)}
|
||||
className="text-xs font-medium text-stone-500 hover:text-stone-900"
|
||||
>
|
||||
aria-label="Edit">
|
||||
Edit
|
||||
</button>
|
||||
<div className="relative overflow-visible">
|
||||
@@ -424,7 +424,7 @@ export default function UsersPage({ initialUsers, brands, currentUser, onUserCre
|
||||
onClick={() => setActionMenuId(actionMenuId === user.id ? null : user.id)}
|
||||
className="text-base font-medium text-stone-400 hover:text-stone-600 px-2 py-1"
|
||||
title="More actions"
|
||||
>
|
||||
aria-label="More actions">
|
||||
⁝
|
||||
</button>
|
||||
{actionMenuId === user.id && (
|
||||
@@ -443,20 +443,20 @@ export default function UsersPage({ initialUsers, brands, currentUser, onUserCre
|
||||
<button type="button"
|
||||
onClick={() => { setActionMenuId(null); handleForcePasswordChange(user.id); }}
|
||||
className="w-full text-left px-3 py-2.5 text-stone-700 hover:bg-stone-50 flex items-center gap-2"
|
||||
>
|
||||
aria-label="↪ Require Password Change">
|
||||
↪ Require Password Change
|
||||
</button>
|
||||
<button type="button"
|
||||
onClick={() => { setActionMenuId(null); handleSendResetLink(user.email); }}
|
||||
className="w-full text-left px-3 py-2.5 text-stone-700 hover:bg-stone-50 flex items-center gap-2"
|
||||
>
|
||||
aria-label="✉ Send Reset Email">
|
||||
✉ Send Reset Email
|
||||
</button>
|
||||
{user.id !== currentUser.id && (
|
||||
<button type="button"
|
||||
onClick={() => { setActionMenuId(null); setDeleteConfirmId(user.id); }}
|
||||
className="w-full text-left px-3 py-2.5 text-red-600 hover:bg-red-50 flex items-center gap-2 border-t border-stone-100 mt-1 pt-1"
|
||||
>
|
||||
aria-label="🗑 Delete">
|
||||
🗑 Delete
|
||||
</button>
|
||||
)}
|
||||
@@ -492,13 +492,13 @@ export default function UsersPage({ initialUsers, brands, currentUser, onUserCre
|
||||
<button type="button"
|
||||
onClick={() => setDeleteConfirmId(null)}
|
||||
className="rounded-lg border border-stone-200 px-4 py-2 text-sm font-medium text-stone-600 hover:bg-stone-50"
|
||||
>
|
||||
aria-label="Cancel">
|
||||
Cancel
|
||||
</button>
|
||||
<button type="button"
|
||||
onClick={() => handleDelete(deleteConfirmId)}
|
||||
className="rounded-lg bg-red-600 px-4 py-2 text-sm font-medium text-white hover:bg-red-700"
|
||||
>
|
||||
aria-label="Delete">
|
||||
Delete
|
||||
</button>
|
||||
</div>
|
||||
@@ -525,7 +525,7 @@ export default function UsersPage({ initialUsers, brands, currentUser, onUserCre
|
||||
<h2 className="text-lg font-bold text-stone-900">
|
||||
{editing.isNew ? "Create User" : "Edit User"}
|
||||
</h2>
|
||||
<button type="button" onClick={closePanel} className="text-stone-400 hover:text-stone-600">
|
||||
<button type="button" onClick={closePanel} className="text-stone-400 hover:text-stone-600" aria-label="Close">
|
||||
<svg className="h-6 w-6" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M6 18L18 6M6 6l12 12" />
|
||||
</svg>
|
||||
@@ -613,7 +613,7 @@ export default function UsersPage({ initialUsers, brands, currentUser, onUserCre
|
||||
checked={editing.role === r}
|
||||
onChange={() => setRole(r)}
|
||||
className="accent-stone-900"
|
||||
/>
|
||||
aria-label="Role"/>
|
||||
<div>
|
||||
<span className="font-medium text-stone-900 capitalize">{r.replace("_", " ")}</span>
|
||||
<p className="text-xs text-stone-500">
|
||||
@@ -686,7 +686,7 @@ export default function UsersPage({ initialUsers, brands, currentUser, onUserCre
|
||||
<button type="button"
|
||||
onClick={closePanel}
|
||||
className="rounded-lg border border-stone-200 px-4 py-2 text-sm font-medium text-stone-600 hover:bg-stone-50"
|
||||
>
|
||||
aria-label="Cancel">
|
||||
Cancel
|
||||
</button>
|
||||
<button type="button"
|
||||
@@ -714,22 +714,22 @@ export default function UsersPage({ initialUsers, brands, currentUser, onUserCre
|
||||
) : passwordModal.error ? (
|
||||
<div>
|
||||
<p className="mt-3 text-sm text-red-600">{passwordModal.error}</p>
|
||||
<button type="button" onClick={() => setPasswordModal(null)} className="mt-4 w-full rounded-lg border border-stone-200 px-4 py-2 text-sm font-medium text-stone-600 hover:bg-stone-50">Close</button>
|
||||
<button type="button" onClick={() => setPasswordModal(null)} className="mt-4 w-full rounded-lg border border-stone-200 px-4 py-2 text-sm font-medium text-stone-600 hover:bg-stone-50" aria-label="Close">Close</button>
|
||||
</div>
|
||||
) : passwordModal.tempPassword ? (
|
||||
<div>
|
||||
<p className="mt-3 text-sm text-stone-600">Temporary password for <span className="font-medium text-stone-900">{passwordModal.email}</span>:</p>
|
||||
<div className="mt-3 flex items-center gap-2 rounded-xl bg-stone-50 p-4 ring-1 ring-stone-200">
|
||||
<span className="font-mono text-lg font-bold text-stone-900 select-all">{passwordModal.tempPassword}</span>
|
||||
<button type="button" onClick={copyTempPassword} className="shrink-0 rounded-lg bg-stone-200 px-3 py-1 text-xs font-medium text-stone-700 hover:bg-stone-300">Copy</button>
|
||||
<button type="button" onClick={copyTempPassword} className="shrink-0 rounded-lg bg-stone-200 px-3 py-1 text-xs font-medium text-stone-700 hover:bg-stone-300" aria-label="Copy">Copy</button>
|
||||
</div>
|
||||
<p className="mt-2 text-xs text-stone-500">Share this password securely with the user. They will be required to change it on next login.</p>
|
||||
<button type="button" onClick={() => setPasswordModal(null)} className="mt-4 w-full rounded-lg bg-stone-900 px-4 py-2 text-sm font-medium text-white hover:bg-stone-800">Done</button>
|
||||
<button type="button" onClick={() => setPasswordModal(null)} className="mt-4 w-full rounded-lg bg-stone-900 px-4 py-2 text-sm font-medium text-white hover:bg-stone-800" aria-label="Done">Done</button>
|
||||
</div>
|
||||
) : passwordModal.message ? (
|
||||
<div>
|
||||
<p className="mt-3 text-sm text-stone-600">{passwordModal.message}</p>
|
||||
<button type="button" onClick={() => setPasswordModal(null)} className="mt-4 w-full rounded-lg bg-stone-900 px-4 py-2 text-sm font-medium text-white hover:bg-stone-800">Done</button>
|
||||
<button type="button" onClick={() => setPasswordModal(null)} className="mt-4 w-full rounded-lg bg-stone-900 px-4 py-2 text-sm font-medium text-white hover:bg-stone-800" aria-label="Done">Done</button>
|
||||
</div>
|
||||
) : null}
|
||||
</div>
|
||||
|
||||
@@ -590,21 +590,21 @@ export default function WaterLogAdminPanel({
|
||||
<button type="button"
|
||||
onClick={() => router.push(`/admin/water-log/headgates/${h.id}`)}
|
||||
className="font-medium text-[#1a4d2e] hover:underline"
|
||||
>
|
||||
aria-label="Edit →">
|
||||
Edit →
|
||||
</button>
|
||||
<div className="flex items-center gap-2">
|
||||
<button type="button"
|
||||
onClick={() => handleRegenerateToken(h)}
|
||||
className="text-[#57694e] hover:text-[#1a4d2e]"
|
||||
>
|
||||
aria-label="Rotate token">
|
||||
Rotate token
|
||||
</button>
|
||||
<span className="text-[#d4d9d3]">·</span>
|
||||
<button type="button"
|
||||
onClick={() => handleDeleteHg(h)}
|
||||
className="text-[#b91c1c] hover:text-[#7f1d1d]"
|
||||
>
|
||||
aria-label="Delete">
|
||||
Delete
|
||||
</button>
|
||||
</div>
|
||||
@@ -723,14 +723,14 @@ export default function WaterLogAdminPanel({
|
||||
<button type="button"
|
||||
onClick={() => handleResetPin(u)}
|
||||
className="text-[#57694e] hover:text-[#1a4d2e]"
|
||||
>
|
||||
aria-label="PIN">
|
||||
PIN
|
||||
</button>
|
||||
<span className="text-[#d4d9d3]">·</span>
|
||||
<button type="button"
|
||||
onClick={() => handleDeleteUser(u)}
|
||||
className="text-[#b91c1c] hover:text-[#7f1d1d]"
|
||||
>
|
||||
aria-label="Off">
|
||||
Off
|
||||
</button>
|
||||
</div>
|
||||
@@ -833,7 +833,7 @@ export default function WaterLogAdminPanel({
|
||||
setFilterVia("");
|
||||
}}
|
||||
className="text-xs font-medium text-[#57694e] hover:text-[#1a4d2e] hover:underline"
|
||||
>
|
||||
aria-label="Clear filters">
|
||||
Clear filters
|
||||
</button>
|
||||
)}
|
||||
@@ -907,7 +907,7 @@ export default function WaterLogAdminPanel({
|
||||
router.push(`/admin/water-log/entries/${e.id}`)
|
||||
}
|
||||
className="font-medium text-[#1a4d2e] opacity-0 transition-opacity group-hover:opacity-100 hover:underline"
|
||||
>
|
||||
aria-label="Edit →">
|
||||
Edit →
|
||||
</button>
|
||||
</td>
|
||||
@@ -1061,7 +1061,7 @@ function PinBanner({
|
||||
<button type="button"
|
||||
onClick={onDismiss}
|
||||
className="rounded-lg border border-[#1a4d2e]/30 px-3 py-1.5 text-xs font-semibold text-[#1a4d2e] hover:bg-white"
|
||||
>
|
||||
aria-label="Dismiss">
|
||||
Dismiss
|
||||
</button>
|
||||
</div>
|
||||
|
||||
@@ -94,12 +94,12 @@ export default function WelcomeSequenceDashboard({ brandId }: Props) {
|
||||
{totalPages > 1 && (
|
||||
<div className="flex items-center gap-1 ml-2">
|
||||
<button type="button" onClick={() => setPage(p => Math.max(0, p - 1))} disabled={page === 0}
|
||||
className="flex h-7 w-7 items-center justify-center rounded border border-[var(--admin-border)] text-[var(--admin-text-muted)] hover:text-[var(--admin-text-primary)] hover:bg-stone-50 disabled:opacity-30 disabled:cursor-not-allowed transition-colors">
|
||||
className="flex h-7 w-7 items-center justify-center rounded border border-[var(--admin-border)] text-[var(--admin-text-muted)] hover:text-[var(--admin-text-primary)] hover:bg-stone-50 disabled:opacity-30 disabled:cursor-not-allowed transition-colors" aria-label="Previous">
|
||||
<svg className="h-3 w-3" fill="none" viewBox="0 0 24 24" stroke="currentColor" strokeWidth={2}><path strokeLinecap="round" strokeLinejoin="round" d="M15 19l-7-7 7-7" /></svg>
|
||||
</button>
|
||||
<span className="text-xs text-[var(--admin-text-muted)] px-1">{page + 1}/{totalPages}</span>
|
||||
<button type="button" onClick={() => setPage(p => Math.min(totalPages - 1, p + 1))} disabled={page >= totalPages - 1}
|
||||
className="flex h-7 w-7 items-center justify-center rounded border border-[var(--admin-border)] text-[var(--admin-text-muted)] hover:text-[var(--admin-text-primary)] hover:bg-stone-50 disabled:opacity-30 disabled:cursor-not-allowed transition-colors">
|
||||
className="flex h-7 w-7 items-center justify-center rounded border border-[var(--admin-border)] text-[var(--admin-text-muted)] hover:text-[var(--admin-text-primary)] hover:bg-stone-50 disabled:opacity-30 disabled:cursor-not-allowed transition-colors" aria-label="Next">
|
||||
<svg className="h-3 w-3" fill="none" viewBox="0 0 24 24" stroke="currentColor" strokeWidth={2}><path strokeLinecap="round" strokeLinejoin="round" d="M9 5l7 7-7 7" /></svg>
|
||||
</button>
|
||||
</div>
|
||||
|
||||
@@ -122,7 +122,7 @@ export default function AdminFilterBar({
|
||||
<button type="button"
|
||||
onClick={onAddClick}
|
||||
className="ml-auto rounded-xl bg-[var(--admin-accent)] px-4 py-2 text-xs font-bold text-white hover:bg-[var(--admin-accent-hover)] transition-colors shadow-[var(--admin-shadow-sm)]"
|
||||
>
|
||||
aria-label="+">
|
||||
+ {addLabel}
|
||||
</button>
|
||||
)}
|
||||
|
||||
@@ -21,7 +21,7 @@ export default function AdminPagination({
|
||||
onClick={() => onPageChange(currentPage - 1)}
|
||||
disabled={currentPage === 0}
|
||||
className="flex h-8 w-8 items-center justify-center rounded-lg border border-[var(--admin-border)] text-[var(--admin-text-muted)] hover:text-[var(--admin-text-secondary)] hover:bg-[var(--admin-bg-subtle)] disabled:opacity-30 disabled:cursor-not-allowed transition-all duration-150"
|
||||
>
|
||||
aria-label="Previous">
|
||||
<svg className="h-4 w-4" fill="none" viewBox="0 0 24 24" stroke="currentColor" strokeWidth={2}>
|
||||
<path strokeLinecap="round" strokeLinejoin="round" d="M15 19l-7-7 7-7" />
|
||||
</svg>
|
||||
@@ -69,7 +69,7 @@ export default function AdminPagination({
|
||||
onClick={() => onPageChange(currentPage + 1)}
|
||||
disabled={currentPage >= totalPages - 1}
|
||||
className="flex h-8 w-8 items-center justify-center rounded-lg border border-[var(--admin-border)] text-[var(--admin-text-muted)] hover:text-[var(--admin-text-secondary)] hover:bg-[var(--admin-bg-subtle)] disabled:opacity-30 disabled:cursor-not-allowed transition-all duration-150"
|
||||
>
|
||||
aria-label="Next">
|
||||
<svg className="h-4 w-4" fill="none" viewBox="0 0 24 24" stroke="currentColor" strokeWidth={2}>
|
||||
<path strokeLinecap="round" strokeLinejoin="round" d="M9 5l7 7-7 7" />
|
||||
</svg>
|
||||
@@ -102,7 +102,7 @@ export function AdminSimplePagination({
|
||||
onClick={() => onPageChange(page - 1)}
|
||||
disabled={page === 0}
|
||||
className="flex h-7 w-7 items-center justify-center rounded border border-[var(--admin-border)] text-[var(--admin-text-muted)] hover:text-[var(--admin-text-secondary)] hover:bg-[var(--admin-bg-subtle)] disabled:opacity-30 disabled:cursor-not-allowed transition-colors"
|
||||
>
|
||||
aria-label="Previous">
|
||||
<svg className="h-3 w-3" fill="none" viewBox="0 0 24 24" stroke="currentColor" strokeWidth={2}>
|
||||
<path strokeLinecap="round" strokeLinejoin="round" d="M15 19l-7-7 7-7" />
|
||||
</svg>
|
||||
@@ -111,7 +111,7 @@ export function AdminSimplePagination({
|
||||
onClick={() => onPageChange(page + 1)}
|
||||
disabled={page >= totalPages - 1}
|
||||
className="flex h-7 w-7 items-center justify-center rounded border border-[var(--admin-border)] text-[var(--admin-text-muted)] hover:text-[var(--admin-text-secondary)] hover:bg-[var(--admin-bg-subtle)] disabled:opacity-30 disabled:cursor-not-allowed transition-colors"
|
||||
>
|
||||
aria-label="Next">
|
||||
<svg className="h-3 w-3" fill="none" viewBox="0 0 24 24" stroke="currentColor" strokeWidth={2}>
|
||||
<path strokeLinecap="round" strokeLinejoin="round" d="M9 5l7 7-7 7" />
|
||||
</svg>
|
||||
|
||||
@@ -101,7 +101,7 @@ export default function DataTable<T>({
|
||||
onClick={() => setPage((p) => Math.max(0, p - 1))}
|
||||
disabled={page === 0}
|
||||
className="flex h-7 w-7 items-center justify-center rounded border border-stone-200 text-stone-500 hover:text-stone-700 hover:bg-stone-50 disabled:opacity-30 disabled:cursor-not-allowed transition-colors"
|
||||
>
|
||||
aria-label="Previous">
|
||||
<svg className="h-3 w-3" fill="none" viewBox="0 0 24 24" stroke="currentColor" strokeWidth={2}>
|
||||
<path strokeLinecap="round" strokeLinejoin="round" d="M15 19l-7-7 7-7" />
|
||||
</svg>
|
||||
@@ -110,7 +110,7 @@ export default function DataTable<T>({
|
||||
onClick={() => setPage((p) => Math.min(totalPages - 1, p + 1))}
|
||||
disabled={page >= totalPages - 1}
|
||||
className="flex h-7 w-7 items-center justify-center rounded border border-stone-200 text-stone-500 hover:text-stone-700 hover:bg-stone-50 disabled:opacity-30 disabled:cursor-not-allowed transition-colors"
|
||||
>
|
||||
aria-label="Next">
|
||||
<svg className="h-3 w-3" fill="none" viewBox="0 0 24 24" stroke="currentColor" strokeWidth={2}>
|
||||
<path strokeLinecap="round" strokeLinejoin="round" d="M9 5l7 7-7 7" />
|
||||
</svg>
|
||||
|
||||
Reference in New Issue
Block a user