fix(miniapp): address review feedback

This commit is contained in:
2026-03-12 02:28:56 +04:00
parent 135a2301ca
commit 789854358e
6 changed files with 103 additions and 75 deletions

View File

@@ -1,6 +1,7 @@
import { Match, Show, Switch, createMemo, createSignal, onMount } from 'solid-js' import { Match, Show, Switch, createMemo, createSignal, onMount } from 'solid-js'
import { dictionary, type Locale } from './i18n' import { dictionary, type Locale } from './i18n'
import { majorStringToMinor, minorToMajorString } from './lib/money'
import { import {
fetchAdminSettingsQuery, fetchAdminSettingsQuery,
fetchBillingCycleQuery, fetchBillingCycleQuery,
@@ -124,6 +125,7 @@ type PaymentDraft = {
type TestingRolePreview = 'admin' | 'resident' type TestingRolePreview = 'admin' | 'resident'
const chartPalette = ['#f7b389', '#6fd3c0', '#f06a8d', '#94a8ff', '#f3d36f', '#7dc96d'] as const const chartPalette = ['#f7b389', '#6fd3c0', '#f06a8d', '#94a8ff', '#f3d36f', '#7dc96d'] as const
const TESTING_ROLE_TAP_WINDOW_MS = 30 * 60 * 1000
const demoSession: Extract<SessionState, { status: 'ready' }> = { const demoSession: Extract<SessionState, { status: 'ready' }> = {
status: 'ready', status: 'ready',
@@ -178,27 +180,6 @@ function defaultCyclePeriod(): string {
return new Date().toISOString().slice(0, 7) return new Date().toISOString().slice(0, 7)
} }
function majorStringToMinor(value: string): bigint {
const trimmed = value.trim()
const negative = trimmed.startsWith('-')
const normalized = negative ? trimmed.slice(1) : trimmed
const [whole = '0', fraction = ''] = normalized.split('.')
const major = BigInt(whole || '0')
const cents = BigInt((fraction.padEnd(2, '0').slice(0, 2) || '00').replace(/\D/g, '') || '0')
const minor = major * 100n + cents
return negative ? -minor : minor
}
function minorToMajorString(value: bigint): string {
const negative = value < 0n
const absolute = negative ? -value : value
const whole = absolute / 100n
const fraction = String(absolute % 100n).padStart(2, '0')
return `${negative ? '-' : ''}${whole.toString()}.${fraction}`
}
function absoluteMinor(value: bigint): bigint { function absoluteMinor(value: bigint): bigint {
return value < 0n ? -value : value return value < 0n ? -value : value
} }
@@ -691,7 +672,10 @@ function App() {
} }
const now = Date.now() const now = Date.now()
const nextHistory = [...roleChipTapHistory().filter((timestamp) => now - timestamp < 1800), now] const nextHistory = [
...roleChipTapHistory().filter((timestamp) => now - timestamp < TESTING_ROLE_TAP_WINDOW_MS),
now
]
if (nextHistory.length >= 5) { if (nextHistory.length >= 5) {
setRoleChipTapHistory([]) setRoleChipTapHistory([])
@@ -2209,25 +2193,33 @@ function App() {
})) }))
} }
onBillingRentDueDayChange={(value) => onBillingRentDueDayChange={(value) =>
setBillingForm((current) => ({ value === null
? undefined
: setBillingForm((current) => ({
...current, ...current,
rentDueDay: value rentDueDay: value
})) }))
} }
onBillingRentWarningDayChange={(value) => onBillingRentWarningDayChange={(value) =>
setBillingForm((current) => ({ value === null
? undefined
: setBillingForm((current) => ({
...current, ...current,
rentWarningDay: value rentWarningDay: value
})) }))
} }
onBillingUtilitiesDueDayChange={(value) => onBillingUtilitiesDueDayChange={(value) =>
setBillingForm((current) => ({ value === null
? undefined
: setBillingForm((current) => ({
...current, ...current,
utilitiesDueDay: value utilitiesDueDay: value
})) }))
} }
onBillingUtilitiesReminderDayChange={(value) => onBillingUtilitiesReminderDayChange={(value) =>
setBillingForm((current) => ({ value === null
? undefined
: setBillingForm((current) => ({
...current, ...current,
utilitiesReminderDay: value utilitiesReminderDay: value
})) }))

View File

@@ -1,6 +1,7 @@
import { For, Show } from 'solid-js' import { For, Show } from 'solid-js'
import { cn } from '../../lib/cn' import { cn } from '../../lib/cn'
import { majorStringToMinor, sumMajorStrings } from '../../lib/money'
import type { MiniAppDashboard } from '../../miniapp-api' import type { MiniAppDashboard } from '../../miniapp-api'
import { MiniChip, StatCard } from '../ui' import { MiniChip, StatCard } from '../ui'
@@ -11,31 +12,6 @@ type Props = {
detail?: boolean detail?: boolean
} }
function majorStringToMinor(value: string): bigint {
const trimmed = value.trim()
const negative = trimmed.startsWith('-')
const normalized = negative ? trimmed.slice(1) : trimmed
const [whole = '0', fraction = ''] = normalized.split('.')
const major = BigInt(whole || '0')
const cents = BigInt((fraction.padEnd(2, '0').slice(0, 2) || '00').replace(/\D/g, '') || '0')
const minor = major * 100n + cents
return negative ? -minor : minor
}
function minorToMajorString(value: bigint): string {
const negative = value < 0n
const absolute = negative ? -value : value
const whole = absolute / 100n
const fraction = String(absolute % 100n).padStart(2, '0')
return `${negative ? '-' : ''}${whole.toString()}.${fraction}`
}
function sumMajorStrings(left: string, right: string): string {
return minorToMajorString(majorStringToMinor(left) + majorStringToMinor(right))
}
export function MemberBalanceCard(props: Props) { export function MemberBalanceCard(props: Props) {
const utilitiesAdjustedMajor = () => const utilitiesAdjustedMajor = () =>
sumMajorStrings(props.member.utilityShareMajor, props.member.purchaseOffsetMajor) sumMajorStrings(props.member.utilityShareMajor, props.member.purchaseOffsetMajor)

View File

@@ -31,6 +31,8 @@ export const dictionary = {
language: 'Language', language: 'Language',
householdLanguage: 'Household language', householdLanguage: 'Household language',
savingLanguage: 'Saving…', savingLanguage: 'Saving…',
onLabel: 'On',
offLabel: 'Off',
home: 'Home', home: 'Home',
balances: 'Balances', balances: 'Balances',
ledger: 'Ledger', ledger: 'Ledger',
@@ -273,6 +275,8 @@ export const dictionary = {
language: 'Язык', language: 'Язык',
householdLanguage: 'Язык дома', householdLanguage: 'Язык дома',
savingLanguage: 'Сохраняем…', savingLanguage: 'Сохраняем…',
onLabel: 'Вкл',
offLabel: 'Выкл',
home: 'Главная', home: 'Главная',
balances: 'Баланс', balances: 'Баланс',
ledger: 'Леджер', ledger: 'Леджер',

View File

@@ -884,6 +884,13 @@ button {
} }
.mini-chip-button { .mini-chip-button {
appearance: none;
-webkit-appearance: none;
border: 0;
background: transparent;
padding: 0;
font: inherit;
line-height: inherit;
cursor: pointer; cursor: pointer;
} }

View File

@@ -0,0 +1,24 @@
export function majorStringToMinor(value: string): bigint {
const trimmed = value.trim()
const negative = trimmed.startsWith('-')
const normalized = negative ? trimmed.slice(1) : trimmed
const [whole = '0', fraction = ''] = normalized.split('.')
const major = BigInt(whole || '0')
const cents = BigInt((fraction.padEnd(2, '0').slice(0, 2) || '00').replace(/\D/g, '') || '0')
const minor = major * 100n + cents
return negative ? -minor : minor
}
export function minorToMajorString(value: bigint): string {
const negative = value < 0n
const absolute = negative ? -value : value
const whole = absolute / 100n
const fraction = String(absolute % 100n).padStart(2, '0')
return `${negative ? '-' : ''}${whole.toString()}.${fraction}`
}
export function sumMajorStrings(left: string, right: string): string {
return minorToMajorString(majorStringToMinor(left) + majorStringToMinor(right))
}

View File

@@ -116,10 +116,10 @@ type Props = {
onBillingAdjustmentPolicyChange: (value: 'utilities' | 'rent' | 'separate') => void onBillingAdjustmentPolicyChange: (value: 'utilities' | 'rent' | 'separate') => void
onBillingRentAmountChange: (value: string) => void onBillingRentAmountChange: (value: string) => void
onBillingRentCurrencyChange: (value: 'USD' | 'GEL') => void onBillingRentCurrencyChange: (value: 'USD' | 'GEL') => void
onBillingRentDueDayChange: (value: number) => void onBillingRentDueDayChange: (value: number | null) => void
onBillingRentWarningDayChange: (value: number) => void onBillingRentWarningDayChange: (value: number | null) => void
onBillingUtilitiesDueDayChange: (value: number) => void onBillingUtilitiesDueDayChange: (value: number | null) => void
onBillingUtilitiesReminderDayChange: (value: number) => void onBillingUtilitiesReminderDayChange: (value: number | null) => void
onBillingTimezoneChange: (value: string) => void onBillingTimezoneChange: (value: string) => void
onOpenAddUtilityBill: () => void onOpenAddUtilityBill: () => void
onCloseAddUtilityBill: () => void onCloseAddUtilityBill: () => void
@@ -165,6 +165,23 @@ type Props = {
} }
export function HouseScreen(props: Props) { export function HouseScreen(props: Props) {
function parseBillingDayInput(value: string): number | null {
const trimmed = value.trim()
if (trimmed.length === 0) {
return null
}
const parsed = Number.parseInt(trimmed, 10)
if (!Number.isFinite(parsed) || !Number.isInteger(parsed) || parsed < 1 || parsed > 31) {
return null
}
return parsed
}
const enabledLabel = () => props.copy.onLabel ?? 'ON'
const disabledLabel = () => props.copy.offLabel ?? 'OFF'
return ( return (
<Show <Show
when={props.readyIsAdmin} when={props.readyIsAdmin}
@@ -446,7 +463,9 @@ export function HouseScreen(props: Props) {
max="31" max="31"
value={String(props.billingForm.rentDueDay)} value={String(props.billingForm.rentDueDay)}
onInput={(event) => onInput={(event) =>
props.onBillingRentDueDayChange(Number(event.currentTarget.value)) props.onBillingRentDueDayChange(
parseBillingDayInput(event.currentTarget.value)
)
} }
/> />
</Field> </Field>
@@ -457,7 +476,9 @@ export function HouseScreen(props: Props) {
max="31" max="31"
value={String(props.billingForm.rentWarningDay)} value={String(props.billingForm.rentWarningDay)}
onInput={(event) => onInput={(event) =>
props.onBillingRentWarningDayChange(Number(event.currentTarget.value)) props.onBillingRentWarningDayChange(
parseBillingDayInput(event.currentTarget.value)
)
} }
/> />
</Field> </Field>
@@ -468,7 +489,9 @@ export function HouseScreen(props: Props) {
max="31" max="31"
value={String(props.billingForm.utilitiesDueDay)} value={String(props.billingForm.utilitiesDueDay)}
onInput={(event) => onInput={(event) =>
props.onBillingUtilitiesDueDayChange(Number(event.currentTarget.value)) props.onBillingUtilitiesDueDayChange(
parseBillingDayInput(event.currentTarget.value)
)
} }
/> />
</Field> </Field>
@@ -479,7 +502,9 @@ export function HouseScreen(props: Props) {
max="31" max="31"
value={String(props.billingForm.utilitiesReminderDay)} value={String(props.billingForm.utilitiesReminderDay)}
onInput={(event) => onInput={(event) =>
props.onBillingUtilitiesReminderDayChange(Number(event.currentTarget.value)) props.onBillingUtilitiesReminderDayChange(
parseBillingDayInput(event.currentTarget.value)
)
} }
/> />
</Field> </Field>
@@ -562,14 +587,14 @@ export function HouseScreen(props: Props) {
<div class="ledger-compact-card__main"> <div class="ledger-compact-card__main">
<header> <header>
<strong>{category.name}</strong> <strong>{category.name}</strong>
<span>{category.isActive ? 'ON' : 'OFF'}</span> <span>{category.isActive ? enabledLabel() : disabledLabel()}</span>
</header> </header>
<p>{props.copy.utilityCategoryName ?? ''}</p> <p>{props.copy.utilityCategoryName ?? ''}</p>
<div class="ledger-compact-card__meta"> <div class="ledger-compact-card__meta">
<span <span
class={`mini-chip ${category.isActive ? '' : 'mini-chip--muted'}`} class={`mini-chip ${category.isActive ? '' : 'mini-chip--muted'}`}
> >
{category.isActive ? 'ON' : 'OFF'} {category.isActive ? enabledLabel() : disabledLabel()}
</span> </span>
</div> </div>
</div> </div>
@@ -815,8 +840,8 @@ export function HouseScreen(props: Props) {
) )
} }
> >
<option value="true">ON</option> <option value="true">{enabledLabel()}</option>
<option value="false">OFF</option> <option value="false">{disabledLabel()}</option>
</select> </select>
</Field> </Field>
</div> </div>