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",
"videos": "Videos",
"selected": "{{count}} selected",
"copy": "Copy"
"copy": "Copy",
"rowsPerPage": "Rows per page",
"next": "Next",
"previous": "Previous",
"page": "Page {{current}} of {{total}}",
"records": "records"
},
"app": {
"name": "EcoStream"

View File

@@ -11,7 +11,12 @@
"status": "Trạng thái",
"videos": "Video",
"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": {
"name": "EcoStream"

View File

@@ -9,6 +9,10 @@ import { useAuthStore } from "@/stores/auth";
import { useQuery } from "@pinia/colada";
import { useTranslation } from "i18next-vue";
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) => {
switch ((status || '').toLowerCase()) {
@@ -35,6 +39,8 @@ const PaymentHistory = defineComponent({
const { t } = useTranslation();
const toast = useAppToast();
const downloadingInvoiceId = ref<string | null>(null);
const page = ref(1);
const limit = ref(10);
const formatTermLabel = (months: number) => t('settings.billing.termOption', { months });
const formatPaymentMethodLabel = (value?: string) => {
@@ -74,9 +80,13 @@ const PaymentHistory = defineComponent({
};
};
const { data, isLoading } = useQuery({
key: ['paymentHistory'],
query: () => client.listPaymentHistory().then(res => (res.payments || []).map(mapHistoryItem)),
key: () => ['paymentHistory', page.value, limit.value],
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) => {
if (!item.id) return;
@@ -193,11 +203,26 @@ const PaymentHistory = defineComponent({
),
meta: {
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 () => (
<div class="px-6 py-4">
<div class="flex items-center gap-4 mb-4">
@@ -211,7 +236,7 @@ const PaymentHistory = defineComponent({
</div>
<BaseTable
data={data.value || []}
data={data.value?.items || []}
columns={columns}
loading={isLoading.value}
emptyText={t('settings.billing.noPaymentHistory')}
@@ -219,7 +244,7 @@ const PaymentHistory = defineComponent({
{{
loading: () => (
<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 class="col-span-3 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>
<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>
);
}

View File

@@ -440,7 +440,7 @@ onMounted(loadTemplates);
</div>
<div class="space-y-2 md:col-span-2">
<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 class="space-y-2 md:col-span-2">
<label class="text-sm font-medium text-foreground/70">VAST URL</label>
@@ -488,7 +488,7 @@ onMounted(loadTemplates);
</div>
<div class="space-y-2 md:col-span-2">
<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 class="space-y-2 md:col-span-2">
<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 AdminTable from "./components/AdminTable.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 ColumnDef } from "@tanstack/vue-table";
import { computed, h, onMounted, reactive, ref, watch } from "vue";
@@ -470,21 +470,10 @@ onMounted(() => {
</div>
<div class="overflow-hidden rounded-lg border border-border">
<BillingPlansSection
title="Available plans"
description="Reuse the same plan cards from the billing screen when creating an admin payment."
:is-loading="plansLoading"
:plans="plans"
<PlanSelection
:current-plan-id="selectedPlanId"
:selecting-plan-id="selectedPlanId"
:format-money="(amount) => formatMoney(amount, 'USD')"
: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"
:selected-plan-id="selectedPlanId"
@upgrade="selectPlan"
/>
</div>
</div>

View File

@@ -38,10 +38,17 @@ export interface CreatePaymentResponse {
}
export interface ListPaymentHistoryRequest {
page?: number | undefined;
limit?: number | undefined;
}
export interface ListPaymentHistoryResponse {
payments?: PaymentHistoryItem[] | undefined;
total?: number | undefined;
page?: number | undefined;
limit?: number | undefined;
hasPrev?: boolean | undefined;
hasNext?: boolean | undefined;
}
export interface TopupWalletRequest {
@@ -325,11 +332,17 @@ export const CreatePaymentResponse: MessageFns<CreatePaymentResponse> = {
};
function createBaseListPaymentHistoryRequest(): ListPaymentHistoryRequest {
return {};
return { page: 0, limit: 0 };
}
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;
},
@@ -340,6 +353,22 @@ export const ListPaymentHistoryRequest: MessageFns<ListPaymentHistoryRequest> =
while (reader.pos < end) {
const tag = reader.uint32();
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) {
break;
@@ -349,26 +378,37 @@ export const ListPaymentHistoryRequest: MessageFns<ListPaymentHistoryRequest> =
return message;
},
fromJSON(_: any): ListPaymentHistoryRequest {
return {};
fromJSON(object: any): ListPaymentHistoryRequest {
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 = {};
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;
},
create<I extends Exact<DeepPartial<ListPaymentHistoryRequest>, I>>(base?: I): ListPaymentHistoryRequest {
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();
message.page = object.page ?? 0;
message.limit = object.limit ?? 0;
return message;
},
};
function createBaseListPaymentHistoryResponse(): ListPaymentHistoryResponse {
return { payments: [] };
return { payments: [], total: 0, page: 0, limit: 0, hasPrev: false, hasNext: false };
}
export const ListPaymentHistoryResponse: MessageFns<ListPaymentHistoryResponse> = {
@@ -378,6 +418,21 @@ export const ListPaymentHistoryResponse: MessageFns<ListPaymentHistoryResponse>
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;
},
@@ -399,6 +454,46 @@ export const ListPaymentHistoryResponse: MessageFns<ListPaymentHistoryResponse>
}
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) {
break;
@@ -413,6 +508,19 @@ export const ListPaymentHistoryResponse: MessageFns<ListPaymentHistoryResponse>
payments: globalThis.Array.isArray(object?.payments)
? 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) {
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;
},
@@ -430,6 +553,11 @@ export const ListPaymentHistoryResponse: MessageFns<ListPaymentHistoryResponse>
fromPartial<I extends Exact<DeepPartial<ListPaymentHistoryResponse>, I>>(object: I): ListPaymentHistoryResponse {
const message = createBaseListPaymentHistoryResponse();
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;
},
};
@@ -888,6 +1016,17 @@ type KeysOfUnion<T> = T extends T ? keyof T : never;
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 };
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 {
return value !== null && value !== undefined;
}

View File

@@ -322,12 +322,15 @@ export const meMethods = {
const metadata = context.get("grpcMetadata");
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 paymentsClient = context.get("paymentsServiceClient");
const metadata = context.get("grpcMetadata");
return await paymentsClient.listPaymentHistory({}, metadata);
},
return await paymentsClient.listPaymentHistory({ page, limit }, metadata);
}),
createPayment: validateFn(
z.object({
planId: z.string().trim().min(1),