Expense & expense payment dates
Business view
An expense's date is what the clinic's financial reports use to place it in time — which month it hits the books, which reporting period it belongs to. Previously, anyone who could add or edit an expense could also set that date to anything: yesterday, last month, next week. That made it easy — accidentally or otherwise — for an expense to land in the wrong reporting period.
Now, setting a date other than the default requires the Expenses: Change Creation Date permission:
- Adding a new expense — without the permission, the expense is always
filed under today, no matter what the form displayed or what a client tried to send. With the permission, the picked date is used.
- Editing an existing expense — without the permission, the date is left
exactly as it was; the edit form's date is disabled. With the permission, it can be changed.
- Expense payments work the same way, and share the same permission as
the parent expense (there's no separate "expense payment date" permission). This is also a behavior change on its own: expense payment dates used to be editable by owners only (user.permissions.doAll); now any group with Expenses: Change Creation Date can do it too.
- Bulk expense payments (paying off many expenses at once) follow the
same rule.
In every case the date field stays visible and shows the record's real date — it's only the ability to change it that's gated. This avoids the confusing experience of a field silently vanishing or silently failing to save a value someone tried to change.
Technical view
The permission
EXPENSES_CREATED_AT — see Granting the permission for how it's assigned to groups.
Server: the shared date helper
getPermittedDate(args, request, permission) — /work/repo/packages/server/src/utils/helpers.js:2560:
export const getPermittedDate = (args, request, permission) =>
args.date &&
(request.session.user.group.permissions.includes(permission) || request.session.user.group.permissions.includes('DO_ALL'))
? dayjs(args.date).toDate()
: undefined
It returns the requested date only if the caller's group holds the named permission (or DO_ALL); otherwise undefined. Callers decide what undefined means for their case — see below.
addNewExpense
/work/repo/packages/server/src/resolvers/mutations/actions/expenses/addNewExpense.js:51-52:
const expenseDate = getPermittedDate(args, request, 'EXPENSES_CREATED_AT') || dayjs().toDate()
No permission → getPermittedDate returns undefined → falls back to dayjs().toDate() (today). This same expenseDate is used both for the expense's own date field and, when the expense is created already paid, for the initial payment's createdAt.
editExpense
/work/repo/packages/server/src/resolvers/mutations/actions/expenses/editExpense.js:30-31:
const expenseDate = getPermittedDate(args, request, 'EXPENSES_CREATED_AT')
No fallback here — no permission means expenseDate is undefined, which Prisma treats as "don't touch this field," so the existing date is left alone. The same value feeds the expense's date and (with a || dayjs().toDate() fallback specifically for a newly added payment row inside the edit) the payment's createdAt.
Expense payments
addNewExpensePayment—
/work/repo/packages/server/src/resolvers/mutations/actions/expenses/addNewExpensePayment.js:56 — createdAt: getCreatedAt(args, request, 'EXPENSES_CREATED_AT')
editExpensePayment(incompanyMutations.js) —
/work/repo/packages/server/src/resolvers/mutations/companyMutations.js:1899 — same call. This is the one that used to default to DO_ALL only; it now explicitly passes 'EXPENSES_CREATED_AT', widening who can do it.
addExpensesBulkPayment—
/work/repo/packages/server/src/resolvers/mutations/actions/expenses/addExpensesBulkPayment.js:80 — same call.
These three all use the pre-existing getCreatedAt(args, request, permission = 'DO_ALL') helper (/work/repo/packages/server/src/utils/helpers.js:2566), which behaves like getPermittedDate but keys off args.createdAt instead of args.date and has no default-to-today fallback built in (payments already default sensibly server-side).
Web form (clinic-web)
ExpenseForm.js—
/work/repo/packages/clinic-web/src/components/dashboard/finances/expenses/ExpenseForm.js:315-331: the date Form.Item is wrapped in an antd Tooltip whose title is the permission-denied message, and the MomentDatePicker gets disabled={!user.permissions.expensesCreatedAt}.
ExpensePaymentForm.js—
/work/repo/packages/clinic-web/src/components/dashboard/finances/expenses/ExpensePaymentForm.js:262-273: same pattern; this replaces the previous {user.permissions.doAll && (...)} conditional that hid the field entirely for non-owners.
Mobile form (clinic-mobile)
NewExpenseScreen.js—
/work/repo/packages/clinic-mobile/src/components/dashboard/finances/expenses/NewExpenseScreen.js:302-310
NewExpensePaymentScreen.js—
/work/repo/packages/clinic-mobile/src/components/dashboard/finances/expenses/NewExpensePaymentScreen.js:229-236
Both pass disabled={!user.permissions.expensesCreatedAt} and help={... ? undefined : t('app.noCreatedAtPermission')} to the shared DateField component (/work/repo/packages/clinic-mobile/src/common/controlledFields/DateField.js), which already supported disabled and help props — this PR is the first caller in the expenses screens to use them.
Front-end permission flag
/work/repo/packages/clinic-mobile/src/shared/utils/getUserPermissions.js:334:
expensesCreatedAt: hasFrontendPermission(['EXPENSES_CREATED_AT'], user),
This file is shared between clinic-web and clinic-mobile (web imports it from the mobile package), so both platforms read user.permissions.expensesCreatedAt from the same source.