mirror of
https://github.com/whekin/household-bot.git
synced 2026-03-31 12:04:02 +00:00
fix(miniapp): address review feedback
This commit is contained in:
@@ -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
|
||||||
}))
|
}))
|
||||||
|
|||||||
@@ -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)
|
||||||
|
|||||||
@@ -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: 'Леджер',
|
||||||
|
|||||||
@@ -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;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
24
apps/miniapp/src/lib/money.ts
Normal file
24
apps/miniapp/src/lib/money.ts
Normal 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))
|
||||||
|
}
|
||||||
@@ -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>
|
||||||
|
|||||||
Reference in New Issue
Block a user