feat: add quick payment action and improve copy button UX

Mini App Home Screen:
- Add 'Record Payment' button to utilities and rent period cards
- Pre-fill payment amount with member's share (rentShare/utilityShare)
- Modal dialog with amount input and currency display
- Toast notifications for copy and payment success/failure feedback

Copy Button Improvements:
- Increase spacing between icon and text (4px → 8px)
- Add hover background and padding for better touch target
- Green background highlight when copied (in addition to icon color change)
- Toast notification appears when copying any value

Backend:
- Add /api/miniapp/payments/add endpoint for quick payments
- Payment notifications sent to 'reminders' topic in Telegram
- Include member name, payment type, amount, and period in notification

Files:
- New: apps/miniapp/src/components/ui/toast.tsx
- Modified: apps/miniapp/src/routes/home.tsx, apps/miniapp/src/index.css,
  apps/miniapp/src/theme.css, apps/miniapp/src/i18n.ts,
  apps/bot/src/miniapp-billing.ts, apps/bot/src/server.ts

Quality Gates:  format, lint, typecheck, build, test

Co-authored-by: Qwen-Coder <qwen-coder@alibabacloud.com>
This commit is contained in:
2026-03-14 08:51:53 +04:00
parent 771d64aa4e
commit 488a488137
45 changed files with 2236 additions and 101 deletions

View File

@@ -9,11 +9,22 @@ import { NavigationTabs } from './navigation-tabs'
import { Badge } from '../ui/badge'
import { Button, IconButton } from '../ui/button'
import { Modal } from '../ui/dialog'
import { Field } from '../ui/field'
import { Input } from '../ui/input'
export function AppShell(props: ParentProps) {
const { readySession } = useSession()
const { copy, locale, setLocale } = useI18n()
const { effectiveIsAdmin, testingRolePreview, setTestingRolePreview } = useDashboard()
const {
dashboard,
effectiveIsAdmin,
testingRolePreview,
setTestingRolePreview,
testingPeriodOverride,
setTestingPeriodOverride,
testingTodayOverride,
setTestingTodayOverride
} = useDashboard()
const navigate = useNavigate()
const [testingSurfaceOpen, setTestingSurfaceOpen] = createSignal(false)
@@ -157,6 +168,43 @@ export function AppShell(props: ParentProps) {
{copy().testingPreviewResidentAction ?? ''}
</Button>
</div>
<article class="testing-card__section">
<span>{copy().testingPeriodCurrentLabel ?? ''}</span>
<strong>{dashboard()?.period ?? '—'}</strong>
</article>
<div class="testing-card__actions" style={{ 'flex-direction': 'column', gap: '12px' }}>
<Field label={copy().testingPeriodOverrideLabel ?? ''} wide>
<Input
placeholder={copy().testingPeriodOverridePlaceholder ?? ''}
value={testingPeriodOverride() ?? ''}
onInput={(e) => {
const next = e.currentTarget.value.trim()
setTestingPeriodOverride(next.length > 0 ? next : null)
}}
/>
</Field>
<Field label={copy().testingTodayOverrideLabel ?? ''} wide>
<Input
placeholder={copy().testingTodayOverridePlaceholder ?? ''}
value={testingTodayOverride() ?? ''}
onInput={(e) => {
const next = e.currentTarget.value.trim()
setTestingTodayOverride(next.length > 0 ? next : null)
}}
/>
</Field>
<div class="modal-action-row">
<Button
variant="ghost"
onClick={() => {
setTestingPeriodOverride(null)
setTestingTodayOverride(null)
}}
>
{copy().testingClearOverridesAction ?? ''}
</Button>
</div>
</div>
</div>
</Modal>
</main>

View File

@@ -0,0 +1,50 @@
import { Show, createEffect, onCleanup } from 'solid-js'
import { cn } from '../../lib/cn'
export interface ToastOptions {
message: string
type?: 'success' | 'info' | 'error'
duration?: number
}
export interface ToastState {
visible: boolean
message: string
type: 'success' | 'info' | 'error'
}
const toastVariants = {
success: 'toast--success',
info: 'toast--info',
error: 'toast--error'
}
export function Toast(props: { state: ToastState; onClose: () => void }) {
let timeoutId: ReturnType<typeof setTimeout> | undefined
createEffect(() => {
if (props.state.visible) {
timeoutId = setTimeout(
() => {
props.onClose()
},
props.state.type === 'error' ? 4000 : 2000
)
}
})
onCleanup(() => {
if (timeoutId) {
clearTimeout(timeoutId)
}
})
return (
<Show when={props.state.visible}>
<div role="status" aria-live="polite" class={cn('toast', toastVariants[props.state.type])}>
<span class="toast__message">{props.state.message}</span>
</div>
</Show>
)
}