Skip to content

Commit 9052c9f

Browse files
authored
per-user coins data for admin (#82)
* admin user can see per-user coins data * fixes * fix
1 parent a615a45 commit 9052c9f

File tree

9 files changed

+124
-74
lines changed

9 files changed

+124
-74
lines changed

CHANGELOG.md

+10
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,15 @@
11
# Changelog
22

3+
## Version 0.2.4
4+
5+
### Added
6+
7+
* admin can select user to view coins for that user
8+
9+
### Fixed
10+
11+
* fix disable password in demo instance (#74)
12+
313
## Version 0.2.3
414

515
### Fixed

app/actions/data.ts

+11-5
Original file line numberDiff line numberDiff line change
@@ -185,7 +185,7 @@ export async function loadCoinsData(): Promise<CoinsData> {
185185
const data = await loadData<CoinsData>('coins')
186186
return {
187187
...data,
188-
transactions: data.transactions.filter(x => x.userId === user.id)
188+
transactions: user.isAdmin ? data.transactions : data.transactions.filter(x => x.userId === user.id)
189189
}
190190
} catch {
191191
return getDefaultCoinsData()
@@ -194,7 +194,7 @@ export async function loadCoinsData(): Promise<CoinsData> {
194194

195195
export async function saveCoinsData(data: CoinsData): Promise<void> {
196196
const user = await getCurrentUser()
197-
197+
198198
// Create clones of the data
199199
const newData = _.cloneDeep(data)
200200
newData.transactions = newData.transactions.map(transaction => ({
@@ -219,12 +219,14 @@ export async function addCoins({
219219
type = 'MANUAL_ADJUSTMENT',
220220
relatedItemId,
221221
note,
222+
userId,
222223
}: {
223224
amount: number
224225
description: string
225226
type?: TransactionType
226227
relatedItemId?: string
227228
note?: string
229+
userId?: string
228230
}): Promise<CoinsData> {
229231
await verifyPermission('coins', type === 'MANUAL_ADJUSTMENT' ? 'write' : 'interact')
230232
const data = await loadCoinsData()
@@ -235,7 +237,8 @@ export async function addCoins({
235237
description,
236238
timestamp: d2t({ dateTime: getNow({}) }),
237239
...(relatedItemId && { relatedItemId }),
238-
...(note && note.trim() !== '' && { note })
240+
...(note && note.trim() !== '' && { note }),
241+
userId: userId || await getCurrentUserId()
239242
}
240243

241244
const newData: CoinsData = {
@@ -270,12 +273,14 @@ export async function removeCoins({
270273
type = 'MANUAL_ADJUSTMENT',
271274
relatedItemId,
272275
note,
276+
userId,
273277
}: {
274278
amount: number
275279
description: string
276280
type?: TransactionType
277281
relatedItemId?: string
278282
note?: string
283+
userId?: string
279284
}): Promise<CoinsData> {
280285
await verifyPermission('coins', type === 'MANUAL_ADJUSTMENT' ? 'write' : 'interact')
281286
const data = await loadCoinsData()
@@ -286,7 +291,8 @@ export async function removeCoins({
286291
description,
287292
timestamp: d2t({ dateTime: getNow({}) }),
288293
...(relatedItemId && { relatedItemId }),
289-
...(note && note.trim() !== '' && { note })
294+
...(note && note.trim() !== '' && { note }),
295+
userId: userId || await getCurrentUserId()
290296
}
291297

292298
const newData: CoinsData = {
@@ -478,6 +484,6 @@ export async function deleteUser(userId: string): Promise<void> {
478484

479485
export async function loadServerSettings(): Promise<ServerSettings> {
480486
return {
481-
isDemo: !!process.env.NEXT_PUBLIC_DEMO,
487+
isDemo: !!process.env.DEMO,
482488
}
483489
}

components/CoinsManager.tsx

+19-3
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,8 @@ import { TransactionNoteEditor } from './TransactionNoteEditor'
1717
import { useHelpers } from '@/lib/client-helpers'
1818

1919
export default function CoinsManager() {
20+
const { currentUser } = useHelpers()
21+
const [selectedUser, setSelectedUser] = useState<string>()
2022
const {
2123
add,
2224
remove,
@@ -28,14 +30,13 @@ export default function CoinsManager() {
2830
totalSpent,
2931
coinsSpentToday,
3032
transactionsToday
31-
} = useCoins()
33+
} = useCoins({selectedUser})
3234
const [settings] = useAtom(settingsAtom)
3335
const [usersData] = useAtom(usersAtom)
3436
const DEFAULT_AMOUNT = '0'
3537
const [amount, setAmount] = useState(DEFAULT_AMOUNT)
3638
const [pageSize, setPageSize] = useState(50)
3739
const [currentPage, setCurrentPage] = useState(1)
38-
const { currentUser } = useHelpers()
3940

4041
const [note, setNote] = useState('')
4142

@@ -62,7 +63,22 @@ export default function CoinsManager() {
6263

6364
return (
6465
<div className="container mx-auto px-4 py-8">
65-
<h1 className="text-3xl font-bold mb-6">Coins Management</h1>
66+
<div className="flex items-center justify-between mb-6">
67+
<h1 className="text-3xl font-bold mr-6">Coins Management</h1>
68+
{currentUser?.isAdmin && (
69+
<select
70+
className="border rounded p-2"
71+
value={selectedUser}
72+
onChange={(e) => setSelectedUser(e.target.value)}
73+
>
74+
{usersData.users.map(user => (
75+
<option key={user.id} value={user.id}>
76+
{user.username}
77+
</option>
78+
))}
79+
</select>
80+
)}
81+
</div>
6682

6783
<div className="grid gap-6 md:grid-cols-2">
6884
<Card>

components/UserForm.tsx

+1
Original file line numberDiff line numberDiff line change
@@ -251,6 +251,7 @@ export default function UserForm({ userId, onCancel, onSuccess }: UserFormProps)
251251
id="disable-password"
252252
checked={disablePassword}
253253
onCheckedChange={setDisablePassword}
254+
disabled={serverSettings.isDemo}
254255
/>
255256
<Label htmlFor="disable-password">Disable password</Label>
256257
</div>

hooks/useCoins.tsx

+39-23
Original file line numberDiff line numberDiff line change
@@ -1,21 +1,23 @@
11
import { useAtom } from 'jotai'
2-
import { checkPermission } from '@/lib/utils'
2+
import { calculateCoinsEarnedToday, calculateCoinsSpentToday, calculateTotalEarned, calculateTotalSpent, calculateTransactionsToday, checkPermission } from '@/lib/utils'
33
import {
44
coinsAtom,
5-
coinsEarnedTodayAtom,
6-
totalEarnedAtom,
7-
totalSpentAtom,
8-
coinsSpentTodayAtom,
9-
transactionsTodayAtom,
10-
coinsBalanceAtom
5+
// coinsEarnedTodayAtom,
6+
// totalEarnedAtom,
7+
// totalSpentAtom,
8+
// coinsSpentTodayAtom,
9+
// transactionsTodayAtom,
10+
// coinsBalanceAtom,
11+
settingsAtom,
12+
usersAtom
1113
} from '@/lib/atoms'
1214
import { addCoins, removeCoins, saveCoinsData } from '@/app/actions/data'
13-
import { CoinsData } from '@/lib/types'
15+
import { CoinsData, User } from '@/lib/types'
1416
import { toast } from '@/hooks/use-toast'
1517
import { useHelpers } from '@/lib/client-helpers'
1618

1719
function handlePermissionCheck(
18-
user: any,
20+
user: User | undefined,
1921
resource: 'habit' | 'wishlist' | 'coins',
2022
action: 'write' | 'interact'
2123
): boolean {
@@ -40,18 +42,30 @@ function handlePermissionCheck(
4042
return true
4143
}
4244

43-
export function useCoins() {
44-
const { currentUser: user } = useHelpers()
45+
export function useCoins(options?: { selectedUser?: string }) {
4546
const [coins, setCoins] = useAtom(coinsAtom)
46-
const [coinsEarnedToday] = useAtom(coinsEarnedTodayAtom)
47-
const [totalEarned] = useAtom(totalEarnedAtom)
48-
const [totalSpent] = useAtom(totalSpentAtom)
49-
const [coinsSpentToday] = useAtom(coinsSpentTodayAtom)
50-
const [transactionsToday] = useAtom(transactionsTodayAtom)
51-
const [balance] = useAtom(coinsBalanceAtom)
47+
const [settings] = useAtom(settingsAtom)
48+
const [users] = useAtom(usersAtom)
49+
const { currentUser } = useHelpers()
50+
let user: User | undefined;
51+
if (!options?.selectedUser) {
52+
user = currentUser;
53+
} else {
54+
user = users.users.find(u => u.id === options.selectedUser)
55+
}
56+
57+
// Filter transactions for the selectd user
58+
const transactions = coins.transactions.filter(t => t.userId === user?.id)
59+
60+
const balance = transactions.reduce((sum, t) => sum + t.amount, 0)
61+
const coinsEarnedToday = calculateCoinsEarnedToday(transactions, settings.system.timezone)
62+
const totalEarned = calculateTotalEarned(transactions)
63+
const totalSpent = calculateTotalSpent(transactions)
64+
const coinsSpentToday = calculateCoinsSpentToday(transactions, settings.system.timezone)
65+
const transactionsToday = calculateTransactionsToday(transactions, settings.system.timezone)
5266

5367
const add = async (amount: number, description: string, note?: string) => {
54-
if (!handlePermissionCheck(user, 'coins', 'write')) return null
68+
if (!handlePermissionCheck(currentUser, 'coins', 'write')) return null
5569
if (isNaN(amount) || amount <= 0) {
5670
toast({
5771
title: "Invalid amount",
@@ -64,15 +78,16 @@ export function useCoins() {
6478
amount,
6579
description,
6680
type: 'MANUAL_ADJUSTMENT',
67-
note
81+
note,
82+
userId: user?.id
6883
})
6984
setCoins(data)
7085
toast({ title: "Success", description: `Added ${amount} coins` })
7186
return data
7287
}
7388

7489
const remove = async (amount: number, description: string, note?: string) => {
75-
if (!handlePermissionCheck(user, 'coins', 'write')) return null
90+
if (!handlePermissionCheck(currentUser, 'coins', 'write')) return null
7691
const numAmount = Math.abs(amount)
7792
if (isNaN(numAmount) || numAmount <= 0) {
7893
toast({
@@ -86,15 +101,16 @@ export function useCoins() {
86101
amount: numAmount,
87102
description,
88103
type: 'MANUAL_ADJUSTMENT',
89-
note
104+
note,
105+
userId: user?.id
90106
})
91107
setCoins(data)
92108
toast({ title: "Success", description: `Removed ${numAmount} coins` })
93109
return data
94110
}
95111

96112
const updateNote = async (transactionId: string, note: string) => {
97-
if (!handlePermissionCheck(user, 'coins', 'write')) return null
113+
if (!handlePermissionCheck(currentUser, 'coins', 'write')) return null
98114
const transaction = coins.transactions.find(t => t.id === transactionId)
99115
if (!transaction) {
100116
toast({
@@ -128,7 +144,7 @@ export function useCoins() {
128144
remove,
129145
updateNote,
130146
balance,
131-
transactions: coins.transactions,
147+
transactions: transactions,
132148
coinsEarnedToday,
133149
totalEarned,
134150
totalSpent,

hooks/useWishlist.tsx

+3-2
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,12 @@
11
import { useAtom } from 'jotai'
2-
import { wishlistAtom, coinsAtom, coinsBalanceAtom } from '@/lib/atoms'
2+
import { wishlistAtom, coinsAtom } from '@/lib/atoms'
33
import { saveWishlistItems, removeCoins } from '@/app/actions/data'
44
import { toast } from '@/hooks/use-toast'
55
import { WishlistItemType } from '@/lib/types'
66
import { celebrations } from '@/utils/celebrations'
77
import { checkPermission } from '@/lib/utils'
88
import { useHelpers } from '@/lib/client-helpers'
9+
import { useCoins } from './useCoins'
910

1011
function handlePermissionCheck(
1112
user: any,
@@ -37,7 +38,7 @@ export function useWishlist() {
3738
const { currentUser: user } = useHelpers()
3839
const [wishlist, setWishlist] = useAtom(wishlistAtom)
3940
const [coins, setCoins] = useAtom(coinsAtom)
40-
const [balance] = useAtom(coinsBalanceAtom)
41+
const { balance } = useCoins()
4142

4243
const addWishlistItem = async (item: Omit<WishlistItemType, 'id'>) => {
4344
if (!handlePermissionCheck(user, 'wishlist', 'write')) return

lib/atoms.ts

+38-38
Original file line numberDiff line numberDiff line change
@@ -49,44 +49,44 @@ export const coinsAtom = atom(getDefaultCoinsData());
4949
export const wishlistAtom = atom(getDefaultWishlistData());
5050
export const serverSettingsAtom = atom(getDefaultServerSettings());
5151

52-
// Derived atom for coins earned today
53-
export const coinsEarnedTodayAtom = atom((get) => {
54-
const coins = get(coinsAtom);
55-
const settings = get(settingsAtom);
56-
return calculateCoinsEarnedToday(coins.transactions, settings.system.timezone);
57-
});
58-
59-
// Derived atom for total earned
60-
export const totalEarnedAtom = atom((get) => {
61-
const coins = get(coinsAtom);
62-
return calculateTotalEarned(coins.transactions);
63-
});
64-
65-
// Derived atom for total spent
66-
export const totalSpentAtom = atom((get) => {
67-
const coins = get(coinsAtom);
68-
return calculateTotalSpent(coins.transactions);
69-
});
70-
71-
// Derived atom for coins spent today
72-
export const coinsSpentTodayAtom = atom((get) => {
73-
const coins = get(coinsAtom);
74-
const settings = get(settingsAtom);
75-
return calculateCoinsSpentToday(coins.transactions, settings.system.timezone);
76-
});
77-
78-
// Derived atom for transactions today
79-
export const transactionsTodayAtom = atom((get) => {
80-
const coins = get(coinsAtom);
81-
const settings = get(settingsAtom);
82-
return calculateTransactionsToday(coins.transactions, settings.system.timezone);
83-
});
84-
85-
// Derived atom for current balance from all transactions
86-
export const coinsBalanceAtom = atom((get) => {
87-
const coins = get(coinsAtom);
88-
return coins.transactions.reduce((sum, transaction) => sum + transaction.amount, 0);
89-
});
52+
// // Derived atom for coins earned today
53+
// export const coinsEarnedTodayAtom = atom((get) => {
54+
// const coins = get(coinsAtom);
55+
// const settings = get(settingsAtom);
56+
// return calculateCoinsEarnedToday(coins.transactions, settings.system.timezone);
57+
// });
58+
59+
// // Derived atom for total earned
60+
// export const totalEarnedAtom = atom((get) => {
61+
// const coins = get(coinsAtom);
62+
// return calculateTotalEarned(coins.transactions);
63+
// });
64+
65+
// // Derived atom for total spent
66+
// export const totalSpentAtom = atom((get) => {
67+
// const coins = get(coinsAtom);
68+
// return calculateTotalSpent(coins.transactions);
69+
// });
70+
71+
// // Derived atom for coins spent today
72+
// export const coinsSpentTodayAtom = atom((get) => {
73+
// const coins = get(coinsAtom);
74+
// const settings = get(settingsAtom);
75+
// return calculateCoinsSpentToday(coins.transactions, settings.system.timezone);
76+
// });
77+
78+
// // Derived atom for transactions today
79+
// export const transactionsTodayAtom = atom((get) => {
80+
// const coins = get(coinsAtom);
81+
// const settings = get(settingsAtom);
82+
// return calculateTransactionsToday(coins.transactions, settings.system.timezone);
83+
// });
84+
85+
// // Derived atom for current balance from all transactions
86+
// export const coinsBalanceAtom = atom((get) => {
87+
// const coins = get(coinsAtom);
88+
// return coins.transactions.reduce((sum, transaction) => sum + transaction.amount, 0);
89+
// });
9090

9191
/* transient atoms */
9292
interface PomodoroAtom {

lib/env.server.ts

+2-2
Original file line numberDiff line numberDiff line change
@@ -2,13 +2,13 @@ import { z } from "zod"
22

33
const zodEnv = z.object({
44
AUTH_SECRET: z.string(),
5-
NEXT_PUBLIC_DEMO: z.string().optional(),
5+
DEMO: z.string().optional(),
66
})
77

88
declare global {
99
interface ProcessEnv extends z.TypeOf<typeof zodEnv> {
1010
AUTH_SECRET: string;
11-
NEXT_PUBLIC_DEMO?: string;
11+
DEMO?: string;
1212
}
1313
}
1414

package.json

+1-1
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
{
22
"name": "habittrove",
3-
"version": "0.2.3",
3+
"version": "0.2.4",
44
"private": true,
55
"scripts": {
66
"dev": "next dev --turbopack",

0 commit comments

Comments
 (0)