Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
22 commits
Select commit Hold shift + click to select a range
fe0ac3e
Proof of Concept and Implementation Plan
code-with-jov Sep 6, 2025
628a582
feat: Add pay period support to months module
cursoragent Sep 6, 2025
1fb9c1a
Refactor: Move pay period logic to dedicated module
cursoragent Sep 6, 2025
732d02f
feat: Add pay period configuration table and types
cursoragent Sep 6, 2025
bc43614
Shared date utility
code-with-jov Sep 6, 2025
b12640e
Implement pay period support in month utilities
code-with-jov Sep 8, 2025
7917508
Merge pull request #12 from code-with-jov/cursor/implement-pay-period…
code-with-jov Sep 8, 2025
3fa12ad
Quick Correction
code-with-jov Sep 8, 2025
ad08a65
Merge pull request #15 from code-with-jov/cursor/implement-pay-period…
code-with-jov Sep 8, 2025
6049602
Merge pay_periods_13_99 into cursor/implement-pay-period-phases-1-2-a…
code-with-jov Sep 8, 2025
52b8436
feat: Add pay period settings and feature flag
cursoragent Sep 8, 2025
ea4fb58
feat: Add pay period display and configuration
cursoragent Sep 9, 2025
fbe3d19
Update implementation plan for pay period UI components
cursoragent Sep 9, 2025
f45c2c8
Refactor pay period settings and UI components for improved user expe…
code-with-jov Sep 10, 2025
6af7adb
Merge pull request #16 from code-with-jov/cursor/review-and-complete-…
code-with-jov Sep 10, 2025
896494b
Merge pull request #13 from code-with-jov/cursor/implement-pay-period…
code-with-jov Sep 10, 2025
b94f0b8
Remove all proof of concept related files and tests, including config…
code-with-jov Sep 10, 2025
2dbfa85
Merge latest changes
code-with-jov Sep 13, 2025
a8127aa
Refactor Pay Period display logic in budget components
code-with-jov Sep 15, 2025
73a038f
Remove console.log and add additional pay period test
code-with-jov Sep 15, 2025
c35833c
Merge branch 'pay_periods_13_99' into pull_in_latest_changes
code-with-jov Sep 21, 2025
7ec1081
Merge pull request #20 from code-with-jov/pull_in_latest_changes
code-with-jov Sep 21, 2025
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
16 changes: 16 additions & 0 deletions packages/desktop-client/src/components/budget/BudgetPageHeader.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,8 @@
import { getScrollbarWidth } from './util';

import { useGlobalPref } from '@desktop-client/hooks/useGlobalPref';
import { useFeatureFlag } from '@desktop-client/hooks/useFeatureFlag';
import { useSyncedPref } from '@desktop-client/hooks/useSyncedPref';

type BudgetPageHeaderProps = {
startMonth: string;
Expand All @@ -20,6 +22,8 @@
const [categoryExpandedStatePref] = useGlobalPref('categoryExpandedState');
const categoryExpandedState = categoryExpandedStatePref ?? 0;
const offsetMultipleMonths = numMonths === 1 ? 4 : 0;
const payPeriodFeatureFlagEnabled = useFeatureFlag('payPeriodsEnabled');
const [payPeriodViewEnabled, setPayPeriodViewEnabled] = useSyncedPref('showPayPeriods');

return (
<View
Expand All @@ -29,6 +33,18 @@
flexShrink: 0,
}}
>
{payPeriodFeatureFlagEnabled && (
<View style={{ alignItems: 'center', marginBottom: 5 }}>
<label style={{ display: 'flex', alignItems: 'center', gap: 6 }}>
<input
type="checkbox"
checked={String(payPeriodViewEnabled) === 'true'}
onChange={e => setPayPeriodViewEnabled(e.target.checked ? 'true' : 'false')}
/>
<span>Show pay periods</span>
</label>
</View>
)}

Check failure on line 47 in packages/desktop-client/src/components/budget/BudgetPageHeader.tsx

View workflow job for this annotation

GitHub Actions / autofix

Non-translated English string. Wrap in <Trans>
<View
style={{
marginRight: 5 + getScrollbarWidth() - offsetMultipleMonths,
Expand Down
11 changes: 9 additions & 2 deletions packages/desktop-client/src/components/budget/MonthPicker.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -127,7 +127,8 @@ export const MonthPicker = ({
</View>
</Link>
{range.map((month, idx) => {
const monthName = monthUtils.format(month, 'MMM', locale);
const config = monthUtils.getPayPeriodConfig();
const displayLabel = monthUtils.getMonthDisplayName(month, config, locale);
const selected =
idx >= firstSelectedIndex && idx <= lastSelectedIndex;

Expand Down Expand Up @@ -214,7 +215,13 @@ export const MonthPicker = ({
onMouseLeave={() => setHoverId(null)}
>
<View>
{size === 'small' ? monthName[0] : monthName}
{monthUtils.isPayPeriod(month)
? size === 'small'
? `P${String(parseInt(month.slice(5, 7)) - 12)}`
: displayLabel
: size === 'small'
? displayLabel[0]
: displayLabel}
{showYearHeader && (
<View
style={{
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -59,7 +59,8 @@ export const BudgetSummary = memo(({ month }: BudgetSummaryProps) => {
? SvgArrowButtonDown1
: SvgArrowButtonUp1;

const displayMonth = monthUtils.format(month, 'MMMM ‘yy', locale);
const config = monthUtils.getPayPeriodConfig();
const displayMonth = monthUtils.getMonthDateRange(month, config, locale);
const { t } = useTranslation();

return (
Expand Down Expand Up @@ -133,7 +134,7 @@ export const BudgetSummary = memo(({ month }: BudgetSummaryProps) => {
currentMonth === month && { fontWeight: 'bold' },
])}
>
{monthUtils.format(month, 'MMMM', locale)}
{displayMonth}
</div>

<View
Expand Down
51 changes: 45 additions & 6 deletions packages/desktop-client/src/components/budget/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -29,11 +29,12 @@
} from '@desktop-client/budget/budgetSlice';
import { useCategories } from '@desktop-client/hooks/useCategories';
import { useGlobalPref } from '@desktop-client/hooks/useGlobalPref';
import { useFeatureFlag } from '@desktop-client/hooks/useFeatureFlag';
import { useSyncedPref } from '@desktop-client/hooks/useSyncedPref';
import { useLocalPref } from '@desktop-client/hooks/useLocalPref';
import { useNavigate } from '@desktop-client/hooks/useNavigate';
import { SheetNameProvider } from '@desktop-client/hooks/useSheetName';
import { useSpreadsheet } from '@desktop-client/hooks/useSpreadsheet';
import { useSyncedPref } from '@desktop-client/hooks/useSyncedPref';
import { pushModal } from '@desktop-client/modals/modalsSlice';
import { addNotification } from '@desktop-client/notifications/notificationsSlice';
import { useDispatch } from '@desktop-client/redux';
Expand Down Expand Up @@ -80,6 +81,10 @@
end: startMonth,
});
const [budgetType = 'envelope'] = useSyncedPref('budgetType');
const payPeriodFeatureFlagEnabled = useFeatureFlag('payPeriodsEnabled');
const [payPeriodFrequency] = useSyncedPref('payPeriodFrequency');
const [payPeriodStartDate] = useSyncedPref('payPeriodStartDate');
const [payPeriodViewEnabled] = useSyncedPref('showPayPeriods');
const [maxMonthsPref] = useGlobalPref('maxMonths');
const maxMonths = maxMonthsPref || 1;
const [initialized, setInitialized] = useState(false);
Expand All @@ -105,6 +110,24 @@
run();
}, []);

// Wire pay period config from synced prefs into month utils
useEffect(() => {
const enabled = payPeriodFeatureFlagEnabled && String(payPeriodViewEnabled) === 'true';
const frequency = (payPeriodFrequency as any) || 'monthly';
const start = (payPeriodStartDate as any) || `${new Date().getFullYear()}-01-01`;

Check failure on line 117 in packages/desktop-client/src/components/budget/index.tsx

View workflow job for this annotation

GitHub Actions / autofix

Unexpected any. Specify a different type

monthUtils.setPayPeriodConfig({

Check failure on line 119 in packages/desktop-client/src/components/budget/index.tsx

View workflow job for this annotation

GitHub Actions / autofix

Unexpected any. Specify a different type
enabled,
payFrequency: frequency,
startDate: start,
} as any);
}, [
payPeriodFeatureFlagEnabled,

Check failure on line 125 in packages/desktop-client/src/components/budget/index.tsx

View workflow job for this annotation

GitHub Actions / autofix

Unexpected any. Specify a different type
payPeriodViewEnabled,
payPeriodFrequency,
payPeriodStartDate,
]);

useEffect(() => {
send('get-budget-bounds').then(({ start, end }) => {
if (bounds.start !== start || bounds.end !== end) {
Expand Down Expand Up @@ -328,6 +351,22 @@

const { trackingComponents, envelopeComponents } = props;

// Derive the month to render based on pay period view toggle
const derivedStartMonth = useMemo(() => {
const config = monthUtils.getPayPeriodConfig();
const usePayPeriods = config?.enabled;

if (!usePayPeriods) return startMonth;

// If already a pay period id, keep it
const mm = parseInt(startMonth.slice(5, 7));
if (Number.isFinite(mm) && mm >= 13) return startMonth;

// For calendar months, use the current year for pay periods
const currentYear = parseInt(startMonth.slice(0, 4));
return String(currentYear) + '-13';
}, [startMonth, payPeriodViewEnabled]);

if (!initialized || !categoryGroups) {
return null;
}
Expand All @@ -342,8 +381,8 @@
>
<DynamicBudgetTable
type={budgetType}
prewarmStartMonth={startMonth}
startMonth={startMonth}
prewarmStartMonth={derivedStartMonth}
startMonth={derivedStartMonth}
monthBounds={bounds}
maxMonths={maxMonths}
dataComponents={trackingComponents}
Expand All @@ -369,8 +408,8 @@
>
<DynamicBudgetTable
type={budgetType}
prewarmStartMonth={startMonth}
startMonth={startMonth}
prewarmStartMonth={derivedStartMonth}
startMonth={derivedStartMonth}
monthBounds={bounds}
maxMonths={maxMonths}
dataComponents={envelopeComponents}
Expand All @@ -390,7 +429,7 @@
}

return (
<SheetNameProvider name={monthUtils.sheetForMonth(startMonth)}>
<SheetNameProvider name={monthUtils.sheetForMonth(derivedStartMonth)}>
<View style={{ flex: 1 }}>{table}</View>
</SheetNameProvider>
);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -57,7 +57,8 @@ export function BudgetSummary({ month }: BudgetSummaryProps) {
? SvgArrowButtonDown1
: SvgArrowButtonUp1;

const displayMonth = monthUtils.format(month, 'MMMM ‘yy', locale);
const config = monthUtils.getPayPeriodConfig();
const displayMonth = monthUtils.getMonthDateRange(month, config, locale);

return (
<View
Expand Down Expand Up @@ -126,7 +127,7 @@ export function BudgetSummary({ month }: BudgetSummaryProps) {
textDecorationSkip: 'ink',
})}
>
{monthUtils.format(month, 'MMMM', locale)}
{displayMonth}
</div>

<View
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -995,7 +995,7 @@ function MonthSelector({
data-month={month}
>
<Text style={styles.underlinedText}>
{monthUtils.format(month, 'MMMM ‘yy', locale)}
{monthUtils.getMonthDateRange(month, monthUtils.getPayPeriodConfig(), locale)}
</Text>
</Button>
<Button
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -105,6 +105,9 @@ export function ExperimentalFeatures() {
>
<Trans>Currency support</Trans>
</FeatureToggle>
<FeatureToggle flag="payPeriodsEnabled">
<Trans>Pay periods</Trans>
</FeatureToggle>
</View>
) : (
<Link
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,58 @@
import React from 'react';
import { Trans, useTranslation } from 'react-i18next';

import { Select } from '@actual-app/components/select';
import { Text } from '@actual-app/components/text';
import { View } from '@actual-app/components/view';

import { Column, Setting } from './UI';
import { Input } from '@actual-app/components/input';
import { useFeatureFlag } from '@desktop-client/hooks/useFeatureFlag';
import { useSyncedPref } from '@desktop-client/hooks/useSyncedPref';

export function PayPeriodSettings() {
const enabledByFlag = useFeatureFlag('payPeriodsEnabled');
const { t } = useTranslation();

const [frequency, setFrequency] = useSyncedPref('payPeriodFrequency');
const [startDate, setStartDate] = useSyncedPref('payPeriodStartDate');

const frequencyOptions: [string, string][] = [
['weekly', t('Weekly')],
['biweekly', t('Biweekly')],
['monthly', t('Monthly')],
];

return (
<Setting
primaryAction={
<View style={{ display: 'flex', flexDirection: 'row', gap: '1.5em' }}>
<Column title={t('Frequency')}>
<Select
value={frequency || 'monthly'}
onChange={value => setFrequency(value)}
options={frequencyOptions}
disabled={!enabledByFlag}
/>
</Column>

<Column title={t('Start Date')}>
<Input
type="date"
value={startDate || ''}
onChange={e => setStartDate(e.target.value)}
disabled={!enabledByFlag}
/>
</Column>
</View>
}
>
<Text>
<Trans>
<strong>Pay period settings.</strong> Configure how pay periods are generated and displayed.
</Trans>
</Text>
</Setting>
);
}

3 changes: 3 additions & 0 deletions packages/desktop-client/src/components/settings/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@ import { BudgetTypeSettings } from './BudgetTypeSettings';
import { CurrencySettings } from './Currency';
import { EncryptionSettings } from './Encryption';
import { ExperimentalFeatures } from './Experimental';
import { PayPeriodSettings } from './PayPeriodSettings';
import { ExportBudget } from './Export';
import { FormatSettings } from './Format';
import { LanguageSettings } from './LanguageSettings';
Expand Down Expand Up @@ -176,6 +177,7 @@ export function Settings() {
const [budgetName] = useMetadataPref('budgetName');
const dispatch = useDispatch();
const isCurrencyExperimentalEnabled = useFeatureFlag('currency');
const isPayPeriodsEnabled = useFeatureFlag('payPeriodsEnabled');
const [_, setDefaultCurrencyCodePref] = useSyncedPref('defaultCurrencyCode');

const onCloseBudget = () => {
Expand Down Expand Up @@ -238,6 +240,7 @@ export function Settings() {
<ThemeSettings />
<FormatSettings />
{isCurrencyExperimentalEnabled && <CurrencySettings />}
{isPayPeriodsEnabled && <PayPeriodSettings />}
<LanguageSettings />
<AuthSettings />
<EncryptionSettings />
Expand Down
1 change: 1 addition & 0 deletions packages/desktop-client/src/hooks/useFeatureFlag.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ const DEFAULT_FEATURE_FLAG_STATE: Record<FeatureFlag, boolean> = {
goalTemplatesUIEnabled: false,
actionTemplating: false,
currency: false,
payPeriodsEnabled: false,
};

export function useFeatureFlag(name: FeatureFlag): boolean {
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
-- Add pay period configuration table
CREATE TABLE IF NOT EXISTS pay_period_config (
id TEXT PRIMARY KEY,
pay_frequency TEXT DEFAULT 'monthly',
start_date TEXT,
pay_day_of_week INTEGER,
pay_day_of_month INTEGER
);

-- Insert default configuration if not exists
INSERT INTO pay_period_config (id, pay_frequency, start_date)
SELECT 'default', 'monthly', '2025-01-01'
WHERE NOT EXISTS (SELECT 1 FROM pay_period_config WHERE id = 'default');

8 changes: 8 additions & 0 deletions packages/loot-core/src/server/db/types/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -251,6 +251,14 @@ export type DbDashboard = {
tombstone: 1 | 0;
};

export type DbPayPeriodConfig = {
id: string;
pay_frequency: string;
start_date: string;
pay_day_of_week?: number | null;
pay_day_of_month?: number | null;
};

export type DbViewTransactionInternal = {
id: DbTransaction['id'];
is_parent: DbTransaction['isParent'];
Expand Down
Loading
Loading