develop-updateui #1

Merged
lethdat merged 78 commits from develop-updateui into master 2026-04-02 05:59:23 +00:00
7 changed files with 222 additions and 34 deletions
Showing only changes of commit a80fa755d4 - Show all commits

View File

@@ -11,7 +11,12 @@
"status": "Status", "status": "Status",
"videos": "Videos", "videos": "Videos",
"selected": "{{count}} selected", "selected": "{{count}} selected",
"copy": "Copy" "copy": "Copy",
"rowsPerPage": "Rows per page",
"next": "Next",
"previous": "Previous",
"page": "Page {{current}} of {{total}}",
"records": "records"
}, },
"app": { "app": {
"name": "EcoStream" "name": "EcoStream"

View File

@@ -11,7 +11,12 @@
"status": "Trạng thái", "status": "Trạng thái",
"videos": "Video", "videos": "Video",
"selected": "{{count}} mục đã chọn", "selected": "{{count}} mục đã chọn",
"copy": "Sao chép" "copy": "Sao chép",
"rowsPerPage": "Số hàng mỗi trang",
"next": "Tiếp",
"previous": "Trước",
"page": "Trang {{current}} trên {{total}}",
"records": "bản ghi"
}, },
"app": { "app": {
"name": "EcoStream" "name": "EcoStream"

View File

@@ -9,6 +9,10 @@ import { useAuthStore } from "@/stores/auth";
import { useQuery } from "@pinia/colada"; import { useQuery } from "@pinia/colada";
import { useTranslation } from "i18next-vue"; import { useTranslation } from "i18next-vue";
import type { ColumnDef } from "@tanstack/vue-table"; import type { ColumnDef } from "@tanstack/vue-table";
import { computed, defineComponent, ref } from "vue";
import AppButton from "@/components/ui/AppButton.vue";
const pageSizeOptions = [5, 10, 20, 50] as const;
const normalizeHistoryStatus = (status?: string) => { const normalizeHistoryStatus = (status?: string) => {
switch ((status || '').toLowerCase()) { switch ((status || '').toLowerCase()) {
@@ -35,6 +39,8 @@ const PaymentHistory = defineComponent({
const { t } = useTranslation(); const { t } = useTranslation();
const toast = useAppToast(); const toast = useAppToast();
const downloadingInvoiceId = ref<string | null>(null); const downloadingInvoiceId = ref<string | null>(null);
const page = ref(1);
const limit = ref(10);
const formatTermLabel = (months: number) => t('settings.billing.termOption', { months }); const formatTermLabel = (months: number) => t('settings.billing.termOption', { months });
const formatPaymentMethodLabel = (value?: string) => { const formatPaymentMethodLabel = (value?: string) => {
@@ -74,9 +80,13 @@ const PaymentHistory = defineComponent({
}; };
}; };
const { data, isLoading } = useQuery({ const { data, isLoading } = useQuery({
key: ['paymentHistory'], key: () => ['paymentHistory', page.value, limit.value],
query: () => client.listPaymentHistory().then(res => (res.payments || []).map(mapHistoryItem)), query: () => client.listPaymentHistory(page.value, limit.value).then(res => ({
...res,
items: (res.payments || []).map(mapHistoryItem)
})),
}); });
const totalPages = computed(() => Math.max(1, Math.ceil((data.value?.total || 0) / (data.value?.limit || limit.value || 1))));
const handleDownloadInvoice = async (item: PaymentHistoryItem) => { const handleDownloadInvoice = async (item: PaymentHistoryItem) => {
if (!item.id) return; if (!item.id) return;
@@ -193,11 +203,26 @@ const PaymentHistory = defineComponent({
), ),
meta: { meta: {
headerClass: 'col-span-2 flex justify-center', headerClass: 'col-span-2 flex justify-center',
cellClass: 'col-span-2 justify-center', cellClass: 'col-span-2 flex justify-center',
}, },
}, },
]; ];
const previousPage = () => {
if (!data.value?.hasPrev || isLoading.value) return;
page.value -= 1;
};
const nextPage = () => {
if (!data.value?.hasNext || isLoading.value) return;
page.value += 1;
};
const changePageSize = (event: Event) => {
const nextLimit = Number((event.target as HTMLSelectElement).value) || 10;
if (nextLimit === limit.value) return;
limit.value = nextLimit;
page.value = 1;
};
return () => ( return () => (
<div class="px-6 py-4"> <div class="px-6 py-4">
<div class="flex items-center gap-4 mb-4"> <div class="flex items-center gap-4 mb-4">
@@ -211,7 +236,7 @@ const PaymentHistory = defineComponent({
</div> </div>
<BaseTable <BaseTable
data={data.value || []} data={data.value?.items || []}
columns={columns} columns={columns}
loading={isLoading.value} loading={isLoading.value}
emptyText={t('settings.billing.noPaymentHistory')} emptyText={t('settings.billing.noPaymentHistory')}
@@ -219,7 +244,7 @@ const PaymentHistory = defineComponent({
{{ {{
loading: () => ( loading: () => (
<div class="px-4 py-6 space-y-3"> <div class="px-4 py-6 space-y-3">
{Array.from({ length: 3 }).map((_, index) => ( {Array.from({ length: 10 }).map((_, index) => (
<div key={index} class="grid grid-cols-12 gap-4 items-center animate-pulse"> <div key={index} class="grid grid-cols-12 gap-4 items-center animate-pulse">
<div class="col-span-3 h-4 rounded bg-muted/50" /> <div class="col-span-3 h-4 rounded bg-muted/50" />
<div class="col-span-2 h-4 rounded bg-muted/50" /> <div class="col-span-2 h-4 rounded bg-muted/50" />
@@ -240,6 +265,28 @@ const PaymentHistory = defineComponent({
) )
}} }}
</BaseTable> </BaseTable>
<div class="mt-4 flex flex-col gap-3 text-xs text-foreground/55 sm:flex-row sm:items-center sm:justify-between">
<div>{t('common.page', { current: data.value?.page || page.value, total: totalPages.value })} · {data.value?.total || 0} {t('common.records')}</div>
<div class="flex flex-wrap items-center gap-2">
<label class="flex items-center gap-2">
<span>{t('common.rowsPerPage')}</span>
<select
class="rounded-md border border-border bg-background px-2 py-1 text-xs text-foreground"
value={String(limit.value)}
onChange={changePageSize}
>
{pageSizeOptions.map((option) => (
<option key={option} value={String(option)}>{option}</option>
))}
</select>
</label>
<div class="flex items-center gap-2 xl:justify-end">
<AppButton size="sm" variant="secondary" disabled={!data.value?.hasPrev || isLoading.value} onClick={previousPage}>{t('common.previous')}</AppButton>
<AppButton size="sm" variant="secondary" disabled={!data.value?.hasNext || isLoading.value} onClick={nextPage}>{t('common.next')}</AppButton>
</div>
</div>
</div>
</div> </div>
); );
} }

View File

@@ -440,7 +440,7 @@ onMounted(loadTemplates);
</div> </div>
<div class="space-y-2 md:col-span-2"> <div class="space-y-2 md:col-span-2">
<label class="text-sm font-medium text-foreground/70">Description</label> <label class="text-sm font-medium text-foreground/70">Description</label>
<AdminTextarea v-model="createForm.description" rows="3" placeholder="Optional" /> <AdminTextarea v-model="createForm.description" :rows="3" placeholder="Optional" />
</div> </div>
<div class="space-y-2 md:col-span-2"> <div class="space-y-2 md:col-span-2">
<label class="text-sm font-medium text-foreground/70">VAST URL</label> <label class="text-sm font-medium text-foreground/70">VAST URL</label>
@@ -488,7 +488,7 @@ onMounted(loadTemplates);
</div> </div>
<div class="space-y-2 md:col-span-2"> <div class="space-y-2 md:col-span-2">
<label class="text-sm font-medium text-foreground/70">Description</label> <label class="text-sm font-medium text-foreground/70">Description</label>
<AdminTextarea v-model="editForm.description" rows="3" /> <AdminTextarea v-model="editForm.description" :rows="3" />
</div> </div>
<div class="space-y-2 md:col-span-2"> <div class="space-y-2 md:col-span-2">
<label class="text-sm font-medium text-foreground/70">VAST URL</label> <label class="text-sm font-medium text-foreground/70">VAST URL</label>

View File

@@ -6,7 +6,7 @@ import AdminInput from "./components/AdminInput.vue";
import AdminSelect from "./components/AdminSelect.vue"; import AdminSelect from "./components/AdminSelect.vue";
import AdminTable from "./components/AdminTable.vue"; import AdminTable from "./components/AdminTable.vue";
import AdminSectionCard from "./components/AdminSectionCard.vue"; import AdminSectionCard from "./components/AdminSectionCard.vue";
import BillingPlansSection from "@/routes/settings/Billing/components/BillingPlansSection.vue"; import PlanSelection from "@/routes/settings/Billing/components/PlanSelection.tsx";
import type { Plan as ModelPlan } from "@/server/gen/proto/app/v1/common"; import type { Plan as ModelPlan } from "@/server/gen/proto/app/v1/common";
import { type ColumnDef } from "@tanstack/vue-table"; import { type ColumnDef } from "@tanstack/vue-table";
import { computed, h, onMounted, reactive, ref, watch } from "vue"; import { computed, h, onMounted, reactive, ref, watch } from "vue";
@@ -470,21 +470,10 @@ onMounted(() => {
</div> </div>
<div class="overflow-hidden rounded-lg border border-border"> <div class="overflow-hidden rounded-lg border border-border">
<BillingPlansSection <PlanSelection
title="Available plans"
description="Reuse the same plan cards from the billing screen when creating an admin payment."
:is-loading="plansLoading"
:plans="plans"
:current-plan-id="selectedPlanId" :current-plan-id="selectedPlanId"
:selecting-plan-id="selectedPlanId" :selected-plan-id="selectedPlanId"
:format-money="(amount) => formatMoney(amount, 'USD')" @upgrade="selectPlan"
:get-plan-storage-text="getPlanStorageText"
:get-plan-duration-text="getPlanDurationText"
:get-plan-uploads-text="getPlanUploadsText"
current-plan-label="Selected"
selecting-label="Selected"
choose-label="Select plan"
@select="selectPlan"
/> />
</div> </div>
</div> </div>

View File

@@ -38,10 +38,17 @@ export interface CreatePaymentResponse {
} }
export interface ListPaymentHistoryRequest { export interface ListPaymentHistoryRequest {
page?: number | undefined;
limit?: number | undefined;
} }
export interface ListPaymentHistoryResponse { export interface ListPaymentHistoryResponse {
payments?: PaymentHistoryItem[] | undefined; payments?: PaymentHistoryItem[] | undefined;
total?: number | undefined;
page?: number | undefined;
limit?: number | undefined;
hasPrev?: boolean | undefined;
hasNext?: boolean | undefined;
} }
export interface TopupWalletRequest { export interface TopupWalletRequest {
@@ -325,11 +332,17 @@ export const CreatePaymentResponse: MessageFns<CreatePaymentResponse> = {
}; };
function createBaseListPaymentHistoryRequest(): ListPaymentHistoryRequest { function createBaseListPaymentHistoryRequest(): ListPaymentHistoryRequest {
return {}; return { page: 0, limit: 0 };
} }
export const ListPaymentHistoryRequest: MessageFns<ListPaymentHistoryRequest> = { export const ListPaymentHistoryRequest: MessageFns<ListPaymentHistoryRequest> = {
encode(_: ListPaymentHistoryRequest, writer: BinaryWriter = new BinaryWriter()): BinaryWriter { encode(message: ListPaymentHistoryRequest, writer: BinaryWriter = new BinaryWriter()): BinaryWriter {
if (message.page !== undefined && message.page !== 0) {
writer.uint32(8).int32(message.page);
}
if (message.limit !== undefined && message.limit !== 0) {
writer.uint32(16).int32(message.limit);
}
return writer; return writer;
}, },
@@ -340,6 +353,22 @@ export const ListPaymentHistoryRequest: MessageFns<ListPaymentHistoryRequest> =
while (reader.pos < end) { while (reader.pos < end) {
const tag = reader.uint32(); const tag = reader.uint32();
switch (tag >>> 3) { switch (tag >>> 3) {
case 1: {
if (tag !== 8) {
break;
}
message.page = reader.int32();
continue;
}
case 2: {
if (tag !== 16) {
break;
}
message.limit = reader.int32();
continue;
}
} }
if ((tag & 7) === 4 || tag === 0) { if ((tag & 7) === 4 || tag === 0) {
break; break;
@@ -349,26 +378,37 @@ export const ListPaymentHistoryRequest: MessageFns<ListPaymentHistoryRequest> =
return message; return message;
}, },
fromJSON(_: any): ListPaymentHistoryRequest { fromJSON(object: any): ListPaymentHistoryRequest {
return {}; return {
page: isSet(object.page) ? globalThis.Number(object.page) : 0,
limit: isSet(object.limit) ? globalThis.Number(object.limit) : 0,
};
}, },
toJSON(_: ListPaymentHistoryRequest): unknown { toJSON(message: ListPaymentHistoryRequest): unknown {
const obj: any = {}; const obj: any = {};
if (message.page !== undefined && message.page !== 0) {
obj.page = Math.round(message.page);
}
if (message.limit !== undefined && message.limit !== 0) {
obj.limit = Math.round(message.limit);
}
return obj; return obj;
}, },
create<I extends Exact<DeepPartial<ListPaymentHistoryRequest>, I>>(base?: I): ListPaymentHistoryRequest { create<I extends Exact<DeepPartial<ListPaymentHistoryRequest>, I>>(base?: I): ListPaymentHistoryRequest {
return ListPaymentHistoryRequest.fromPartial(base ?? ({} as any)); return ListPaymentHistoryRequest.fromPartial(base ?? ({} as any));
}, },
fromPartial<I extends Exact<DeepPartial<ListPaymentHistoryRequest>, I>>(_: I): ListPaymentHistoryRequest { fromPartial<I extends Exact<DeepPartial<ListPaymentHistoryRequest>, I>>(object: I): ListPaymentHistoryRequest {
const message = createBaseListPaymentHistoryRequest(); const message = createBaseListPaymentHistoryRequest();
message.page = object.page ?? 0;
message.limit = object.limit ?? 0;
return message; return message;
}, },
}; };
function createBaseListPaymentHistoryResponse(): ListPaymentHistoryResponse { function createBaseListPaymentHistoryResponse(): ListPaymentHistoryResponse {
return { payments: [] }; return { payments: [], total: 0, page: 0, limit: 0, hasPrev: false, hasNext: false };
} }
export const ListPaymentHistoryResponse: MessageFns<ListPaymentHistoryResponse> = { export const ListPaymentHistoryResponse: MessageFns<ListPaymentHistoryResponse> = {
@@ -378,6 +418,21 @@ export const ListPaymentHistoryResponse: MessageFns<ListPaymentHistoryResponse>
PaymentHistoryItem.encode(v!, writer.uint32(10).fork()).join(); PaymentHistoryItem.encode(v!, writer.uint32(10).fork()).join();
} }
} }
if (message.total !== undefined && message.total !== 0) {
writer.uint32(16).int64(message.total);
}
if (message.page !== undefined && message.page !== 0) {
writer.uint32(24).int32(message.page);
}
if (message.limit !== undefined && message.limit !== 0) {
writer.uint32(32).int32(message.limit);
}
if (message.hasPrev !== undefined && message.hasPrev !== false) {
writer.uint32(40).bool(message.hasPrev);
}
if (message.hasNext !== undefined && message.hasNext !== false) {
writer.uint32(48).bool(message.hasNext);
}
return writer; return writer;
}, },
@@ -399,6 +454,46 @@ export const ListPaymentHistoryResponse: MessageFns<ListPaymentHistoryResponse>
} }
continue; continue;
} }
case 2: {
if (tag !== 16) {
break;
}
message.total = longToNumber(reader.int64());
continue;
}
case 3: {
if (tag !== 24) {
break;
}
message.page = reader.int32();
continue;
}
case 4: {
if (tag !== 32) {
break;
}
message.limit = reader.int32();
continue;
}
case 5: {
if (tag !== 40) {
break;
}
message.hasPrev = reader.bool();
continue;
}
case 6: {
if (tag !== 48) {
break;
}
message.hasNext = reader.bool();
continue;
}
} }
if ((tag & 7) === 4 || tag === 0) { if ((tag & 7) === 4 || tag === 0) {
break; break;
@@ -413,6 +508,19 @@ export const ListPaymentHistoryResponse: MessageFns<ListPaymentHistoryResponse>
payments: globalThis.Array.isArray(object?.payments) payments: globalThis.Array.isArray(object?.payments)
? object.payments.map((e: any) => PaymentHistoryItem.fromJSON(e)) ? object.payments.map((e: any) => PaymentHistoryItem.fromJSON(e))
: [], : [],
total: isSet(object.total) ? globalThis.Number(object.total) : 0,
page: isSet(object.page) ? globalThis.Number(object.page) : 0,
limit: isSet(object.limit) ? globalThis.Number(object.limit) : 0,
hasPrev: isSet(object.hasPrev)
? globalThis.Boolean(object.hasPrev)
: isSet(object.has_prev)
? globalThis.Boolean(object.has_prev)
: false,
hasNext: isSet(object.hasNext)
? globalThis.Boolean(object.hasNext)
: isSet(object.has_next)
? globalThis.Boolean(object.has_next)
: false,
}; };
}, },
@@ -421,6 +529,21 @@ export const ListPaymentHistoryResponse: MessageFns<ListPaymentHistoryResponse>
if (message.payments?.length) { if (message.payments?.length) {
obj.payments = message.payments.map((e) => PaymentHistoryItem.toJSON(e)); obj.payments = message.payments.map((e) => PaymentHistoryItem.toJSON(e));
} }
if (message.total !== undefined && message.total !== 0) {
obj.total = Math.round(message.total);
}
if (message.page !== undefined && message.page !== 0) {
obj.page = Math.round(message.page);
}
if (message.limit !== undefined && message.limit !== 0) {
obj.limit = Math.round(message.limit);
}
if (message.hasPrev !== undefined && message.hasPrev !== false) {
obj.hasPrev = message.hasPrev;
}
if (message.hasNext !== undefined && message.hasNext !== false) {
obj.hasNext = message.hasNext;
}
return obj; return obj;
}, },
@@ -430,6 +553,11 @@ export const ListPaymentHistoryResponse: MessageFns<ListPaymentHistoryResponse>
fromPartial<I extends Exact<DeepPartial<ListPaymentHistoryResponse>, I>>(object: I): ListPaymentHistoryResponse { fromPartial<I extends Exact<DeepPartial<ListPaymentHistoryResponse>, I>>(object: I): ListPaymentHistoryResponse {
const message = createBaseListPaymentHistoryResponse(); const message = createBaseListPaymentHistoryResponse();
message.payments = object.payments?.map((e) => PaymentHistoryItem.fromPartial(e)) || []; message.payments = object.payments?.map((e) => PaymentHistoryItem.fromPartial(e)) || [];
message.total = object.total ?? 0;
message.page = object.page ?? 0;
message.limit = object.limit ?? 0;
message.hasPrev = object.hasPrev ?? false;
message.hasNext = object.hasNext ?? false;
return message; return message;
}, },
}; };
@@ -888,6 +1016,17 @@ type KeysOfUnion<T> = T extends T ? keyof T : never;
export type Exact<P, I extends P> = P extends Builtin ? P export type Exact<P, I extends P> = P extends Builtin ? P
: P & { [K in keyof P]: Exact<P[K], I[K]> } & { [K in Exclude<keyof I, KeysOfUnion<P>>]: never }; : P & { [K in keyof P]: Exact<P[K], I[K]> } & { [K in Exclude<keyof I, KeysOfUnion<P>>]: never };
function longToNumber(int64: { toString(): string }): number {
const num = globalThis.Number(int64.toString());
if (num > globalThis.Number.MAX_SAFE_INTEGER) {
throw new globalThis.Error("Value is larger than Number.MAX_SAFE_INTEGER");
}
if (num < globalThis.Number.MIN_SAFE_INTEGER) {
throw new globalThis.Error("Value is smaller than Number.MIN_SAFE_INTEGER");
}
return num;
}
function isSet(value: any): boolean { function isSet(value: any): boolean {
return value !== null && value !== undefined; return value !== null && value !== undefined;
} }

View File

@@ -322,12 +322,15 @@ export const meMethods = {
const metadata = context.get("grpcMetadata"); const metadata = context.get("grpcMetadata");
return await plansClient.listPlans({}, metadata); return await plansClient.listPlans({}, metadata);
}, },
listPaymentHistory: async () => { listPaymentHistory: validateFn(
z.number().int().min(1).optional(),
z.number().int().min(1).max(100).optional(),
)(async (page, limit) => {
const context = getContext(); const context = getContext();
const paymentsClient = context.get("paymentsServiceClient"); const paymentsClient = context.get("paymentsServiceClient");
const metadata = context.get("grpcMetadata"); const metadata = context.get("grpcMetadata");
return await paymentsClient.listPaymentHistory({}, metadata); return await paymentsClient.listPaymentHistory({ page, limit }, metadata);
}, }),
createPayment: validateFn( createPayment: validateFn(
z.object({ z.object({
planId: z.string().trim().min(1), planId: z.string().trim().min(1),