diff --git a/apps/miniapp/src/App.tsx b/apps/miniapp/src/App.tsx index 3ed8619..e2a67eb 100644 --- a/apps/miniapp/src/App.tsx +++ b/apps/miniapp/src/App.tsx @@ -431,6 +431,7 @@ function App() { const editingPaymentEntry = createMemo( () => paymentLedger().find((entry) => entry.id === editingPaymentId()) ?? null ) + const defaultPaymentMemberId = createMemo(() => adminSettings()?.members[0]?.id ?? '') const editingUtilityBill = createMemo( () => cycleState()?.utilityBills.find((bill) => bill.id === editingUtilityBillId()) ?? null ) @@ -777,7 +778,10 @@ function App() { }) setPaymentForm((current) => ({ ...current, - memberId: current.memberId || payload.members[0]?.id || '', + memberId: + (current.memberId && payload.members.some((member) => member.id === current.memberId) + ? current.memberId + : payload.members[0]?.id) ?? '', currency: payload.settings.settlementCurrency })) } catch (error) { @@ -1214,6 +1218,7 @@ function App() { rentCurrency: settings.rentCurrency, utilityCurrency: settings.settlementCurrency })) + await refreshHouseholdData(initData, true, true) setBillingSettingsOpen(false) } finally { setSavingBillingSettings(false) @@ -1241,6 +1246,7 @@ function App() { period: state.cycle?.period ?? current.period, utilityCurrency: billingForm().settlementCurrency })) + await refreshHouseholdData(initData, true, true) setCycleRentOpen(false) } finally { setOpeningCycle(false) @@ -1268,6 +1274,7 @@ function App() { }) setCycleState(state) setUtilityBillDrafts(cycleUtilityBillDrafts(state.utilityBills)) + await refreshHouseholdData(initData, true, true) setCycleRentOpen(false) } finally { setSavingCycleRent(false) @@ -1304,6 +1311,7 @@ function App() { ...current, utilityAmountMajor: '' })) + await refreshHouseholdData(initData, true, true) setAddingUtilityBillOpen(false) } finally { setSavingUtilityBill(false) @@ -1337,6 +1345,7 @@ function App() { }) setCycleState(state) setUtilityBillDrafts(cycleUtilityBillDrafts(state.utilityBills)) + await refreshHouseholdData(initData, true, true) setEditingUtilityBillId(null) } finally { setSavingUtilityBillId(null) @@ -1356,6 +1365,7 @@ function App() { const state = await deleteMiniAppUtilityBill(initData, billId) setCycleState(state) setUtilityBillDrafts(cycleUtilityBillDrafts(state.utilityBills)) + await refreshHouseholdData(initData, true, true) setEditingUtilityBillId((current) => (current === billId ? null : current)) } finally { setDeletingUtilityBillId(null) @@ -1437,11 +1447,12 @@ function App() { const initData = webApp?.initData?.trim() const currentReady = readySession() const draft = paymentForm() + const memberId = draft.memberId.trim() || defaultPaymentMemberId() if ( !initData || currentReady?.mode !== 'live' || !currentReady.member.isAdmin || - draft.memberId.trim().length === 0 || + memberId.length === 0 || draft.amountMajor.trim().length === 0 ) { return @@ -1450,9 +1461,13 @@ function App() { setAddingPayment(true) try { - await addMiniAppPayment(initData, draft) + await addMiniAppPayment(initData, { + ...draft, + memberId + }) setPaymentForm((current) => ({ ...current, + memberId, amountMajor: '' })) await refreshHouseholdData(initData, true, true) @@ -1582,7 +1597,11 @@ function App() { } } - async function handleSaveRentWeight(memberId: string, closeEditor = true) { + async function handleSaveRentWeight( + memberId: string, + closeEditor = true, + refreshAfterSave = true + ) { const initData = webApp?.initData?.trim() const currentReady = readySession() const nextWeight = Number(rentWeightDrafts()[memberId] ?? '') @@ -1612,6 +1631,9 @@ function App() { ...current, [member.id]: String(member.rentShareWeight) })) + if (refreshAfterSave) { + await refreshHouseholdData(initData, true, true) + } if (closeEditor) { setEditingMemberId(null) } @@ -1620,7 +1642,11 @@ function App() { } } - async function handleSaveMemberStatus(memberId: string, closeEditor = true) { + async function handleSaveMemberStatus( + memberId: string, + closeEditor = true, + refreshAfterSave = true + ) { const initData = webApp?.initData?.trim() const currentReady = readySession() const nextStatus = memberStatusDrafts()[memberId] @@ -1651,6 +1677,9 @@ function App() { resolvedMemberAbsencePolicy(member.id, member.status).policy ?? defaultAbsencePolicyForStatus(member.status) })) + if (refreshAfterSave) { + await refreshHouseholdData(initData, true, true) + } if (closeEditor) { setEditingMemberId(null) } @@ -1659,7 +1688,11 @@ function App() { } } - async function handleSaveMemberAbsencePolicy(memberId: string, closeEditor = true) { + async function handleSaveMemberAbsencePolicy( + memberId: string, + closeEditor = true, + refreshAfterSave = true + ) { const initData = webApp?.initData?.trim() const currentReady = readySession() const member = adminSettings()?.members.find((entry) => entry.id === memberId) @@ -1702,6 +1735,9 @@ function App() { ...current, [memberId]: savedPolicy.policy })) + if (refreshAfterSave) { + await refreshHouseholdData(initData, true, true) + } if (closeEditor) { setEditingMemberId(null) } @@ -1736,6 +1772,7 @@ function App() { const hasNameChange = nextDisplayName !== member.displayName const hasStatusChange = nextStatus !== member.status const hasWeightChange = nextWeight !== member.rentShareWeight + const requiresDashboardRefresh = hasStatusChange || wantsAwayPolicySave || hasWeightChange if (!hasNameChange && !hasStatusChange && !wantsAwayPolicySave && !hasWeightChange) { return @@ -1749,15 +1786,22 @@ function App() { } if (hasStatusChange) { - await handleSaveMemberStatus(memberId, false) + await handleSaveMemberStatus(memberId, false, false) } if (wantsAwayPolicySave) { - await handleSaveMemberAbsencePolicy(memberId, false) + await handleSaveMemberAbsencePolicy(memberId, false, false) } if (hasWeightChange) { - await handleSaveRentWeight(memberId, false) + await handleSaveRentWeight(memberId, false, false) + } + + if (requiresDashboardRefresh) { + const initData = webApp?.initData?.trim() + if (initData) { + await refreshHouseholdData(initData, true, true) + } } setEditingMemberId(null) @@ -1800,6 +1844,7 @@ function App() { return ( @@ -1808,6 +1853,7 @@ function App() { return ( setAddingPaymentOpen(true)} + onOpenAddPayment={() => { + setPaymentForm((current) => ({ + ...current, + memberId: current.memberId.trim() || defaultPaymentMemberId(), + currency: adminSettings()?.settings.settlementCurrency ?? current.currency + })) + setAddingPaymentOpen(true) + }} onCloseAddPayment={() => setAddingPaymentOpen(false)} onAddPayment={handleAddPayment} onPaymentFormMemberChange={(value) => @@ -1936,6 +1989,7 @@ function App() { return ( + locale: 'en' | 'ru' dashboard: MiniAppDashboard member: MiniAppDashboard['members'][number] detail?: boolean @@ -123,7 +125,8 @@ export function MemberBalanceCard(props: Props) { {(date) => ( - {props.copy.fxEffectiveDateLabel ?? ''}: {date()} + {props.copy.fxEffectiveDateLabel ?? ''}:{' '} + {formatFriendlyDate(date(), props.locale)} )} diff --git a/apps/miniapp/src/components/ui/icons.tsx b/apps/miniapp/src/components/ui/icons.tsx index 736398f..4996d88 100644 --- a/apps/miniapp/src/components/ui/icons.tsx +++ b/apps/miniapp/src/components/ui/icons.tsx @@ -124,3 +124,15 @@ export function XIcon(props: IconProps) { ) } + +export function TrashIcon(props: IconProps) { + return ( + + + + + + + + ) +} diff --git a/apps/miniapp/src/i18n.ts b/apps/miniapp/src/i18n.ts index a880ce3..805c71f 100644 --- a/apps/miniapp/src/i18n.ts +++ b/apps/miniapp/src/i18n.ts @@ -8,7 +8,7 @@ export const dictionary = { loadingBody: 'Validating Telegram session and membership…', loadingBadge: 'Secure session', demoBadge: 'Demo mode', - liveBadge: 'Live household', + liveBadge: 'Connected home', joinTitle: 'Welcome to your household', joinBody: 'You are not a member of {household} yet. Send a join request and wait for admin approval.', @@ -134,11 +134,11 @@ export const dictionary = { paymentAmount: 'Payment amount', paymentMember: 'Member', paymentSaveAction: 'Save payment', - paymentDeleteAction: 'Delete payment', + paymentDeleteAction: 'Delete', paymentEditorBody: 'Review the payment record in one focused editor.', deletingPayment: 'Deleting payment…', purchaseSaveAction: 'Save purchase', - purchaseDeleteAction: 'Delete purchase', + purchaseDeleteAction: 'Delete', deletingPurchase: 'Deleting purchase…', savingPurchase: 'Saving purchase…', editEntryAction: 'Edit entry', @@ -189,7 +189,7 @@ export const dictionary = { addUtilityBillAction: 'Add utility bill', savingUtilityBill: 'Saving utility bill…', saveUtilityBillAction: 'Save utility bill', - deleteUtilityBillAction: 'Delete utility bill', + deleteUtilityBillAction: 'Delete', deletingUtilityBill: 'Deleting utility bill…', utilityBillsEmpty: 'No utility bills recorded for this cycle yet.', rentAmount: 'Rent amount', @@ -270,7 +270,7 @@ export const dictionary = { loadingBody: 'Проверяем Telegram-сессию и членство…', loadingBadge: 'Защищённая сессия', demoBadge: 'Демо режим', - liveBadge: 'Живой household', + liveBadge: 'Подключённый дом', joinTitle: 'Добро пожаловать домой', joinBody: 'Ты пока не участник {household}. Отправь заявку на вступление и дождись подтверждения админа.', @@ -397,11 +397,11 @@ export const dictionary = { paymentAmount: 'Сумма оплаты', paymentMember: 'Участник', paymentSaveAction: 'Сохранить оплату', - paymentDeleteAction: 'Удалить оплату', + paymentDeleteAction: 'Удалить', paymentEditorBody: 'Проверь оплату в отдельном редакторе.', deletingPayment: 'Удаляем оплату…', purchaseSaveAction: 'Сохранить покупку', - purchaseDeleteAction: 'Удалить покупку', + purchaseDeleteAction: 'Удалить', deletingPurchase: 'Удаляем покупку…', savingPurchase: 'Сохраняем покупку…', editEntryAction: 'Редактировать запись', @@ -450,7 +450,7 @@ export const dictionary = { addUtilityBillAction: 'Добавить коммунальный счёт', savingUtilityBill: 'Сохраняем счёт…', saveUtilityBillAction: 'Сохранить счёт', - deleteUtilityBillAction: 'Удалить счёт', + deleteUtilityBillAction: 'Удалить', deletingUtilityBill: 'Удаляем счёт…', utilityBillsEmpty: 'Для этого цикла пока нет коммунальных счетов.', rentAmount: 'Сумма аренды', diff --git a/apps/miniapp/src/lib/dates.ts b/apps/miniapp/src/lib/dates.ts new file mode 100644 index 0000000..31d3ea6 --- /dev/null +++ b/apps/miniapp/src/lib/dates.ts @@ -0,0 +1,38 @@ +import type { Locale } from '../i18n' + +function localeTag(locale: Locale): string { + return locale === 'ru' ? 'ru-RU' : 'en-US' +} + +export function formatFriendlyDate(value: string, locale: Locale): string { + const date = new Date(value) + if (Number.isNaN(date.getTime())) { + return value + } + + const includeYear = date.getUTCFullYear() !== new Date().getUTCFullYear() + + return new Intl.DateTimeFormat(localeTag(locale), { + month: 'long', + day: 'numeric', + ...(includeYear ? { year: 'numeric' } : {}) + }).format(date) +} + +export function formatCyclePeriod(period: string, locale: Locale): string { + const [yearValue, monthValue] = period.split('-') + const year = Number.parseInt(yearValue ?? '', 10) + const month = Number.parseInt(monthValue ?? '', 10) + + if (!Number.isInteger(year) || !Number.isInteger(month) || month < 1 || month > 12) { + return period + } + + const date = new Date(Date.UTC(year, month - 1, 1)) + const includeYear = year !== new Date().getUTCFullYear() + + return new Intl.DateTimeFormat(localeTag(locale), { + month: 'long', + ...(includeYear ? { year: 'numeric' } : {}) + }).format(date) +} diff --git a/apps/miniapp/src/screens/balances-screen.tsx b/apps/miniapp/src/screens/balances-screen.tsx index cc985eb..06cd133 100644 --- a/apps/miniapp/src/screens/balances-screen.tsx +++ b/apps/miniapp/src/screens/balances-screen.tsx @@ -1,10 +1,12 @@ import { Show } from 'solid-js' import { MemberBalanceCard } from '../components/finance/member-balance-card' +import { formatCyclePeriod } from '../lib/dates' import type { MiniAppDashboard } from '../miniapp-api' type Props = { copy: Record + locale: 'en' | 'ru' dashboard: MiniAppDashboard | null currentMemberLine: MiniAppDashboard['members'][number] | null } @@ -25,6 +27,7 @@ export function BalancesScreen(props: Props) { {(member) => (
{props.copy.balanceScreenScopeTitle ?? ''} - {dashboard().period} + {formatCyclePeriod(dashboard().period, props.locale)}

{props.copy.balanceScreenScopeBody ?? ''}

diff --git a/apps/miniapp/src/screens/home-screen.tsx b/apps/miniapp/src/screens/home-screen.tsx index 07ea978..7e60eec 100644 --- a/apps/miniapp/src/screens/home-screen.tsx +++ b/apps/miniapp/src/screens/home-screen.tsx @@ -1,11 +1,13 @@ import { Show } from 'solid-js' import { FinanceSummaryCards } from '../components/finance/finance-summary-cards' +import { formatCyclePeriod } from '../lib/dates' import { sumMajorStrings } from '../lib/money' import type { MiniAppDashboard } from '../miniapp-api' type Props = { copy: Record + locale: 'en' | 'ru' dashboard: MiniAppDashboard | null currentMemberLine: MiniAppDashboard['members'][number] | null utilityTotalMajor: string @@ -85,7 +87,7 @@ export function HomeScreen(props: Props) {
{props.copy.currentCycleLabel ?? ''} - {dashboard().period} + {formatCyclePeriod(dashboard().period, props.locale)}
@@ -122,7 +124,7 @@ export function HomeScreen(props: Props) {
{props.copy.houseSnapshotTitle ?? ''} - {dashboard().period} + {formatCyclePeriod(dashboard().period, props.locale)}

{props.copy.houseSnapshotBody ?? ''}

diff --git a/apps/miniapp/src/screens/house-screen.tsx b/apps/miniapp/src/screens/house-screen.tsx index 2509e7c..2b22b43 100644 --- a/apps/miniapp/src/screens/house-screen.tsx +++ b/apps/miniapp/src/screens/house-screen.tsx @@ -9,8 +9,10 @@ import { Modal, PencilIcon, PlusIcon, - SettingsIcon + SettingsIcon, + TrashIcon } from '../components/ui' +import { formatCyclePeriod, formatFriendlyDate } from '../lib/dates' import type { MiniAppAdminCycleState, MiniAppAdminSettingsPayload, @@ -50,6 +52,7 @@ type CycleForm = { type Props = { copy: Record + locale: 'en' | 'ru' readyIsAdmin: boolean householdDefaultLocale: 'en' | 'ru' dashboard: MiniAppDashboard | null @@ -226,7 +229,9 @@ export function HouseScreen(props: Props) {
{props.copy.billingCycleTitle ?? ''} - {props.cycleState?.cycle?.period ?? props.copy.billingCycleEmpty ?? ''} + {props.cycleState?.cycle?.period + ? formatCyclePeriod(props.cycleState.cycle.period, props.locale) + : (props.copy.billingCycleEmpty ?? '')}

@@ -565,7 +570,7 @@ export function HouseScreen(props: Props) {

{bill.billName} - {bill.createdAt.slice(0, 10)} + {formatFriendlyDate(bill.createdAt, props.locale)}

{props.copy.utilityCategoryName ?? ''}

@@ -714,6 +719,7 @@ export function HouseScreen(props: Props) { variant="danger" onClick={() => void props.onDeleteUtilityBill(bill.id)} > + {props.deletingUtilityBillId === bill.id ? props.copy.deletingUtilityBill : props.copy.deleteUtilityBillAction} @@ -1085,7 +1091,7 @@ export function HouseScreen(props: Props) { resolvedPolicy.effectiveFromPeriod ? (props.copy.absencePolicyEffectiveFrom ?? '').replace( '{period}', - resolvedPolicy.effectiveFromPeriod + formatCyclePeriod(resolvedPolicy.effectiveFromPeriod, props.locale) ) : (props.copy.absencePolicyHint ?? '') } diff --git a/apps/miniapp/src/screens/ledger-screen.tsx b/apps/miniapp/src/screens/ledger-screen.tsx index fe16c6f..c0805d2 100644 --- a/apps/miniapp/src/screens/ledger-screen.tsx +++ b/apps/miniapp/src/screens/ledger-screen.tsx @@ -1,6 +1,7 @@ import { For, Show } from 'solid-js' -import { Button, Field, IconButton, Modal, PencilIcon } from '../components/ui' +import { Button, Field, IconButton, Modal, PencilIcon, PlusIcon, TrashIcon } from '../components/ui' +import { formatFriendlyDate } from '../lib/dates' import type { MiniAppAdminSettingsPayload, MiniAppDashboard } from '../miniapp-api' type PurchaseDraft = { @@ -23,6 +24,7 @@ type PaymentDraft = { type Props = { copy: Record + locale: 'en' | 'ru' dashboard: MiniAppDashboard | null readyIsAdmin: boolean adminMembers: readonly MiniAppAdminSettingsPayload['members'][number][] @@ -147,7 +149,11 @@ export function LedgerScreen(props: Props) {
{entry.title} - {entry.occurredAt?.slice(0, 10) ?? '—'} + + {entry.occurredAt + ? formatFriendlyDate(entry.occurredAt, props.locale) + : '—'} +

{entry.actorDisplayName ?? props.copy.ledgerActorFallback ?? ''}

@@ -194,6 +200,7 @@ export function LedgerScreen(props: Props) { return ( @@ -408,7 +416,11 @@ export function LedgerScreen(props: Props) {
{props.paymentMemberName(entry)} - {entry.occurredAt?.slice(0, 10) ?? '—'} + + {entry.occurredAt + ? formatFriendlyDate(entry.occurredAt, props.locale) + : '—'} +

{props.ledgerTitle(entry)}

@@ -449,7 +461,11 @@ export function LedgerScreen(props: Props) {