Skip to content

Commit b316287

Browse files
Add credit to admin users
1 parent 432e812 commit b316287

File tree

10 files changed

+387
-30
lines changed

10 files changed

+387
-30
lines changed

backend/docs/swagger/swagger.yaml

Lines changed: 11 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -2913,9 +2913,17 @@ paths:
29132913
"200":
29142914
description: OK
29152915
schema:
2916-
items:
2917-
$ref: "#/definitions/services.UserWithUSDBalance"
2918-
type: array
2916+
allOf:
2917+
- $ref: "#/definitions/handlers.APIResponse"
2918+
- properties:
2919+
data:
2920+
type: object
2921+
properties:
2922+
users:
2923+
items:
2924+
$ref: "#/definitions/services.UserWithUSDBalance"
2925+
type: array
2926+
type: object
29192927
"500":
29202928
description: Internal Server Error
29212929
schema:

frontend/kubecloud-v2/assets/scss/global.scss

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -131,6 +131,14 @@ body {
131131
font-size: 12px !important;
132132
opacity: 0.5 !important;
133133
}
134+
135+
tbody {
136+
> tr {
137+
td {
138+
--v-border-opacity: 0.08;
139+
}
140+
}
141+
}
134142
}
135143

136144
.v-container--fluid {
Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,36 @@
1+
<template>
2+
<v-card :style="{ padding: '0 !important' }">
3+
<v-card-title class="pa-8 d-flex align-center justify-space-between border-b">
4+
<div>
5+
<div class="d-flex ga-2 align-baseline">
6+
<v-icon v-if="icon" :icon="icon" size="small" color="success" />
7+
<slot v-else name="icon" />
8+
9+
<span v-if="title" class="text-h5 font-weight-bold" v-text="title" />
10+
<slot v-else name="title" />
11+
</div>
12+
<p v-if="description" class="text-subtitle-2 opacity-50" v-text="description" />
13+
<slot v-else name="description" />
14+
</div>
15+
16+
<v-btn
17+
icon
18+
size="small"
19+
variant="plain"
20+
:style="{ borderRadius: '50% !important' }"
21+
@click="$emit('cancel')"
22+
>
23+
<v-icon icon="mdi-close" size="large" />
24+
</v-btn>
25+
</v-card-title>
26+
27+
<v-card-text>
28+
<slot />
29+
</v-card-text>
30+
</v-card>
31+
</template>
32+
33+
<script setup lang="ts">
34+
defineProps<{ title?: string; description?: string; icon?: string }>()
35+
defineEmits<{ (e: "cancel"): void }>()
36+
</script>
Lines changed: 100 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,100 @@
1+
<template>
2+
<DialogCardLayout title="Credit Balance" icon="mdi-cash-plus">
3+
<template #description>
4+
<div>
5+
<span class="text-subtitle-2 opacity-50">Apply credits to user:</span>&nbsp;
6+
<span
7+
class="text-subtitle-2 text-primary px-2 py-1 rounded-lg"
8+
:style="{
9+
backgroundColor: 'rgba(var(--v-theme-primary), var(--v-border-opacity))',
10+
}"
11+
>{{ user?.email ?? "N/A" }}</span
12+
>
13+
</div>
14+
</template>
15+
16+
<v-form ref="form" @submit.prevent="submit()">
17+
<v-row>
18+
<v-col cols="12">
19+
<div class="d-flex justify-space-between align-center">
20+
<p class="text-h6">Credit Details</p>
21+
<p class="text-subtitle-2 opacity-50">Apply credits to user accounts</p>
22+
</div>
23+
</v-col>
24+
25+
<v-col cols="12">
26+
<v-text-field
27+
label="Amount"
28+
type="number"
29+
name="amount"
30+
variant="outlined"
31+
min="0"
32+
step="0.01"
33+
prepend-inner-icon="mdi-currency-usd"
34+
autofocus
35+
:rules="[
36+
(v) => !!v || 'Amount is required',
37+
(v) => !v.includes('e') || 'Amount is invalid',
38+
(v) => v > 0 || 'Amount must be greater than 0',
39+
(v) => v < 10000 || 'Amount must be less than 10,000',
40+
(v) => !isNaN(parseFloat(v)) || 'Amount must be a number',
41+
(v) =>
42+
!v.includes('.') ||
43+
(v.includes('.') && v.split('.')[1].length <= 2) ||
44+
'Amount can only have 2 decimal places',
45+
]"
46+
>
47+
<template #append-inner>
48+
<p>USD</p>
49+
</template>
50+
</v-text-field>
51+
</v-col>
52+
53+
<v-col cols="12">
54+
<v-textarea
55+
rows="4"
56+
label="Reason / Memo"
57+
name="memo"
58+
variant="outlined"
59+
prepend-inner-icon="mdi-file-document-outline"
60+
no-resize
61+
counter="255"
62+
persistent-counter
63+
:rules="[
64+
(v) => !!v || 'Reason is required',
65+
(v) => v.length >= 3 || 'Reason must be at least 3 characters',
66+
(v) => v.length <= 255 || 'Reason must be less than 255 characters',
67+
]"
68+
/>
69+
</v-col>
70+
71+
<v-col cols="12">
72+
<v-btn
73+
type="submit"
74+
block
75+
size="x-large"
76+
class="btn-form"
77+
text="Apply Credits"
78+
prepend-icon="mdi-cash-plus"
79+
variant="outlined"
80+
:disabled="!form?.isValid"
81+
/>
82+
</v-col>
83+
</v-row>
84+
</v-form>
85+
</DialogCardLayout>
86+
</template>
87+
88+
<script setup lang="ts">
89+
import type { VForm } from "vuetify/components/VForm"
90+
import type { ServicesUserWithUSDBalance } from "../generated/api"
91+
92+
defineProps<{ user?: ServicesUserWithUSDBalance }>()
93+
const emit = defineEmits<{ (e: "submit", event: { amount: number; memo: string }): void }>()
94+
95+
const form = ref<VForm>()
96+
function submit() {
97+
const f = new FormData(form.value!.$el as HTMLFormElement)
98+
emit("submit", { amount: parseFloat(f.get("amount") as string), memo: f.get("memo") as string })
99+
}
100+
</script>
Lines changed: 114 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,114 @@
1+
<template>
2+
<tr class="text-no-wrap py-2">
3+
<td class="text-subtitle-2 text-center">
4+
<span class="opacity-50">{{ user.id }}</span>
5+
</td>
6+
7+
<td class="text-subtitle-2 text-center">
8+
<span class="opacity-50">{{ user.username }}</span>
9+
</td>
10+
11+
<td class="text-subtitle-2 text-center">
12+
<span class="opacity-50">{{ user.email }}</span>
13+
</td>
14+
15+
<td class="text-subtitle-2 text-center">
16+
<span class="opacity-50"> ${{ Math.round(user.balance! * 100) / 100 }} </span>
17+
</td>
18+
19+
<td class="text-subtitle-2 text-center">
20+
<span class="opacity-50">{{ createdAt }}</span>
21+
</td>
22+
23+
<td>
24+
<div class="d-flex align-center justify-end ga-4">
25+
<v-btn
26+
variant="text"
27+
class="border"
28+
prepend-icon="mdi-cash-plus"
29+
size="small"
30+
text="Credit Balance"
31+
:disabled="!user.verified"
32+
:loading="isCreditLoading"
33+
@click="onCredit()"
34+
/>
35+
36+
<v-btn
37+
variant="text"
38+
class="border"
39+
color="warning"
40+
prepend-icon="mdi-water-remove"
41+
size="small"
42+
text="Drain"
43+
/>
44+
45+
<v-btn
46+
variant="text"
47+
class="border"
48+
prepend-icon="mdi-trash-can-outline"
49+
color="error"
50+
size="small"
51+
text="Remove"
52+
/>
53+
</div>
54+
</td>
55+
</tr>
56+
</template>
57+
58+
<script setup lang="ts">
59+
import type { HandlersCreditRequestInput, ServicesUserWithUSDBalance } from "../generated/api"
60+
61+
const props = defineProps<{ user: ServicesUserWithUSDBalance }>()
62+
63+
const createdAt = useDateFormat(() => props.user.created_at, "DD/MM/YYYY, HH:mm")
64+
65+
const ctx = inject(UserDialogCtxKey)!
66+
const api = useApi()
67+
const toast = useToast()
68+
69+
const { execute: handleCredit, isLoading: isCreditLoading } = useAsyncState(
70+
async (body: HandlersCreditRequestInput) => {
71+
const { data } = await api.admin.creditUser(props.user.id!.toString(), body)
72+
toast.success({ message: data.message })
73+
},
74+
null,
75+
{ immediate: false }
76+
)
77+
78+
async function onCredit() {
79+
const result = await ctx.credit(props.user)
80+
if (result) {
81+
handleCredit(undefined, result)
82+
}
83+
}
84+
85+
// async function onRemove() {
86+
// const result = await emit("remove")
87+
// console.log({ result })
88+
// }
89+
90+
/*
91+
const { execute: onCredit } = useAsyncState(
92+
async (user: ServicesUserWithUSDBalance) => {
93+
console.log("credit", user)
94+
},
95+
null,
96+
{ immediate: false }
97+
)
98+
99+
const { execute: onDrain } = useAsyncState(
100+
async (user: ServicesUserWithUSDBalance) => {
101+
console.log("drain", user)
102+
},
103+
null,
104+
{ immediate: false }
105+
)
106+
107+
const { execute: onRemove } = useAsyncState(
108+
async (user: ServicesUserWithUSDBalance) => {
109+
console.log("remove", user)
110+
},
111+
null,
112+
{ immediate: false }
113+
) */
114+
</script>

frontend/kubecloud-v2/components/WorkflowRow.vue

Lines changed: 8 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -26,10 +26,14 @@
2626
</div>
2727
</td>
2828

29-
<td class="text-subtitle-2 opacity-50 text-center">{{ workflow.step_name }}</td>
30-
<td class="text-subtitle-2 opacity-50 text-center">{{ workflow.user_id || "-" }}</td>
31-
<td class="text-subtitle-2 opacity-50 text-center">
32-
{{ createdAt }}
29+
<td class="text-subtitle-2 text-center">
30+
<span class="opacity-50">{{ workflow.step_name }}</span>
31+
</td>
32+
<td class="text-subtitle-2 text-center">
33+
<span class="opacity-50">{{ workflow.user_id || "-" }}</span>
34+
</td>
35+
<td class="text-subtitle-2 text-center">
36+
<span class="opacity-50">{{ createdAt }}</span>
3337
</td>
3438
<td>
3539
<v-btn
Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,31 @@
1+
import type { HandlersCreditRequestInput, ServicesUserWithUSDBalance } from "../generated/api"
2+
3+
// Dashboard Layout Context
4+
export interface DashboardLayoutCtx {
5+
drawer: DrawerCtx
6+
container: ContainerCtx
7+
}
8+
9+
export interface DrawerCtx {
10+
isOpen: Ref<boolean>
11+
open(): void
12+
close(): void
13+
}
14+
15+
export interface ContainerCtx {
16+
isFluid: Ref<boolean>
17+
fluidize(): void
18+
containerize(): void
19+
}
20+
21+
export const DashboardLayoutCtxKey: InjectionKey<DashboardLayoutCtx> = Symbol("DashboardLayoutCtx")
22+
23+
// User Dialog Context
24+
25+
export interface UserDialogCtx {
26+
credit(user: ServicesUserWithUSDBalance): Promise<HandlersCreditRequestInput | undefined>
27+
drain(user: ServicesUserWithUSDBalance): Promise<boolean>
28+
remove(user: ServicesUserWithUSDBalance): Promise<boolean>
29+
}
30+
31+
export const UserDialogCtxKey: InjectionKey<UserDialogCtx> = Symbol("UserDialogCtx")

frontend/kubecloud-v2/composables/dashboardLayoutCtx.ts

Lines changed: 0 additions & 18 deletions
This file was deleted.

frontend/kubecloud-v2/composables/dialog.ts

Lines changed: 7 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,11 @@
1-
export const useDialog = <T>() => {
2-
const data = ref<T>()
3-
const { onReveal, ...reset } = useConfirmDialog()
1+
// import type { UseConfirmDialogRevealResult } from "@vueuse/core"
42

5-
onReveal((d) => (data.value = d))
3+
export const useDialog = <A = undefined, B = undefined, C = undefined>() => {
4+
const data = ref<A>()
5+
const { onReveal, ...reset } = useConfirmDialog<A, B, C>()
6+
7+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
8+
onReveal(((d: A) => (data.value = d)) as any)
69

710
return {
811
data,

0 commit comments

Comments
 (0)