Skip to content

Commit

Permalink
update user operation and fix api connection
Browse files Browse the repository at this point in the history
  • Loading branch information
zmh-program committed Nov 8, 2023
1 parent b40c60e commit 38dad63
Show file tree
Hide file tree
Showing 15 changed files with 333 additions and 16 deletions.
62 changes: 62 additions & 0 deletions admin/controller.go
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,16 @@ type GenerateInvitationForm struct {
Number int `json:"number"`
}

type QuotaOperationForm struct {
Id int64 `json:"id"`
Quota float32 `json:"quota"`
}

type SubscriptionOperationForm struct {
Id int64 `json:"id"`
Month int64 `json:"month"`
}

func InfoAPI(c *gin.Context) {
db := utils.GetDBFromContext(c)
cache := utils.GetCacheFromContext(c)
Expand Down Expand Up @@ -74,3 +84,55 @@ func UserPaginationAPI(c *gin.Context) {
search := strings.TrimSpace(c.Query("search"))
c.JSON(http.StatusOK, GetUserPagination(db, int64(page), search))
}

func UserQuotaAPI(c *gin.Context) {
db := utils.GetDBFromContext(c)

var form QuotaOperationForm
if err := c.ShouldBindJSON(&form); err != nil {
c.JSON(http.StatusOK, gin.H{
"status": false,
"message": err.Error(),
})
return
}

err := QuotaOperation(db, form.Id, form.Quota)
if err != nil {
c.JSON(http.StatusOK, gin.H{
"status": false,
"message": err.Error(),
})
return
}

c.JSON(http.StatusOK, gin.H{
"status": true,
})
}

func UserSubscriptionAPI(c *gin.Context) {
db := utils.GetDBFromContext(c)

var form SubscriptionOperationForm
if err := c.ShouldBindJSON(&form); err != nil {
c.JSON(http.StatusOK, gin.H{
"status": false,
"message": err.Error(),
})
return
}

err := SubscriptionOperation(db, form.Id, form.Month)
if err != nil {
c.JSON(http.StatusOK, gin.H{
"status": false,
"message": err.Error(),
})
return
}

c.JSON(http.StatusOK, gin.H{
"status": true,
})
}
2 changes: 2 additions & 0 deletions admin/router.go
Original file line number Diff line number Diff line change
Expand Up @@ -13,4 +13,6 @@ func Register(app *gin.Engine) {
app.POST("/admin/invitation/generate", GenerateInvitationAPI)

app.GET("/admin/user/list", UserPaginationAPI)
app.POST("/admin/user/quota", UserQuotaAPI)
app.POST("/admin/user/subscription", UserSubscriptionAPI)
}
1 change: 0 additions & 1 deletion admin/statistic.go
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,6 @@ func IncrRequest(cache *redis.Client) {
}

func IncrModelRequest(cache *redis.Client, model string, tokens int64) {
IncrRequest(cache)
utils.IncrWithExpire(cache, getModelFormat(getDay(), model), tokens, time.Hour*24*7*2)
}

Expand Down
28 changes: 27 additions & 1 deletion admin/user.go
Original file line number Diff line number Diff line change
Expand Up @@ -32,7 +32,7 @@ func GetUserPagination(db *sql.DB, page int64, search string) PaginationForm {
LEFT JOIN quota ON quota.user_id = auth.id
LEFT JOIN subscription ON subscription.user_id = auth.id
WHERE auth.username LIKE ?
ORDER BY auth.id DESC LIMIT ? OFFSET ?
ORDER BY auth.id LIMIT ? OFFSET ?
`, "%"+search+"%", pagination, page*pagination)
if err != nil {
return PaginationForm{
Expand Down Expand Up @@ -79,3 +79,29 @@ func GetUserPagination(db *sql.DB, page int64, search string) PaginationForm {
Data: users,
}
}

func QuotaOperation(db *sql.DB, id int64, quota float32) error {
// if quota is negative, then decrease quota
// if quota is positive, then increase quota

_, err := db.Exec(`
INSERT INTO quota (user_id, quota, used) VALUES (?, ?, ?)
ON DUPLICATE KEY UPDATE quota = quota + ?
`, id, quota, 0., quota)

return err
}

func SubscriptionOperation(db *sql.DB, id int64, month int64) error {
// if month is negative, then decrease month
// if month is positive, then increase month

expireAt := time.Now().AddDate(0, int(month), 0)

_, err := db.Exec(`
INSERT INTO subscription (user_id, total_month, expired_at) VALUES (?, ?, ?)
ON DUPLICATE KEY UPDATE total_month = total_month + ?, expired_at = DATE_ADD(expired_at, INTERVAL ? MONTH)
`, id, month, expireAt, month, month)

return err
}
25 changes: 25 additions & 0 deletions app/src/admin/api.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import {
BillingChartResponse,
CommonResponse,
ErrorChartResponse,
InfoResponse,
InvitationGenerateResponse,
Expand Down Expand Up @@ -111,3 +112,27 @@ export async function getUserList(

return response.data as UserResponse;
}

export async function quotaOperation(
id: number,
quota: number,
): Promise<CommonResponse> {
const response = await axios.post("/admin/user/quota", { id, quota });
if (response.status !== 200) {
return { status: false, message: "" };
}

return response.data as CommonResponse;
}

export async function subscriptionOperation(
id: number,
month: number,
): Promise<CommonResponse> {
const response = await axios.post("/admin/user/subscription", { id, month });
if (response.status !== 200) {
return { status: false, message: "" };
}

return response.data as CommonResponse;
}
7 changes: 6 additions & 1 deletion app/src/admin/types.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,8 @@
export type CommonResponse = {
status: boolean;
message: string;
};

export type InfoResponse = {
billing_today: number;
billing_month: number;
Expand Down Expand Up @@ -54,7 +59,7 @@ export type InvitationGenerateResponse = {
};

export type UserData = {
id: string;
id: number;
username: string;
is_admin: boolean;
quota: number;
Expand Down
79 changes: 79 additions & 0 deletions app/src/components/PopupDialog.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,79 @@
import {
Dialog,
DialogContent,
DialogDescription,
DialogFooter,
DialogHeader,
DialogTitle,
} from "@/components/ui/dialog.tsx";
import { useTranslation } from "react-i18next";
import { Button } from "@/components/ui/button.tsx";
import { Input } from "@/components/ui/input.tsx";
import { useState } from "react";

export type PopupDialogProps = {
title: string;
description?: string;
name: string;
defaultValue?: string;
onValueChange?: (value: string) => string;
onSubmit?: (value: string) => Promise<boolean>;

open: boolean;
setOpen: (open: boolean) => void;
};

function PopupDialog({
title,
description,
name,
defaultValue,
onValueChange,
onSubmit,
open,
setOpen,
}: PopupDialogProps) {
const { t } = useTranslation();
const [value, setValue] = useState<string>(defaultValue || "");

return (
<Dialog open={open} onOpenChange={setOpen}>
<DialogContent>
<DialogHeader>
<DialogTitle>{title}</DialogTitle>
<DialogDescription className={`pt-1.5`}>{description}</DialogDescription>
</DialogHeader>
<div className={`pt-1 flex flex-row items-center justify-center`}>
<span className={`mr-4 whitespace-nowrap`}>{name}</span>
<Input
onChange={(e) => {
setValue(
onValueChange ? onValueChange(e.target.value) : e.target.value,
);
}}
value={value}
/>
</div>
<DialogFooter>
<Button variant={`outline`} onClick={() => setOpen(false)}>
{t("cancel")}
</Button>
<Button
onClick={() => {
onSubmit && onSubmit(value).then((success) => {
if (success) {
setOpen(false);
setValue(defaultValue || "");
}
});
}}
>
{t("confirm")}
</Button>
</DialogFooter>
</DialogContent>
</Dialog>
);
}

export default PopupDialog;
4 changes: 2 additions & 2 deletions app/src/components/admin/InvitationTable.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -155,8 +155,8 @@ function InvitationTable() {
</TableRow>
</TableHeader>
<TableBody>
{data.data.map((invitation, idx) => (
<TableRow key={idx}>
{(data.data || []).map((invitation, idx) => (
<TableRow key={idx} className={`whitespace-nowrap`}>
<TableCell>{invitation.code}</TableCell>
<TableCell>{invitation.quota}</TableCell>
<TableCell>{invitation.type}</TableCell>
Expand Down
88 changes: 82 additions & 6 deletions app/src/components/admin/UserTable.tsx
Original file line number Diff line number Diff line change
@@ -1,8 +1,8 @@
import { useTranslation } from "react-i18next";
import { useToast } from "@/components/ui/use-toast.ts";
import { useState } from "react";
import { UserForm, UserResponse } from "@/admin/types.ts";
import { getUserList } from "@/admin/api.ts";
import {CommonResponse, UserForm, UserResponse} from "@/admin/types.ts";
import {getUserList, quotaOperation, subscriptionOperation} from "@/admin/api.ts";
import { useEffectAsync } from "@/utils/hook.ts";
import {
Table,
Expand All @@ -12,15 +12,93 @@ import {
TableHeader,
TableRow,
} from "@/components/ui/table.tsx";
import {
DropdownMenu,
DropdownMenuContent,
DropdownMenuItem,
DropdownMenuTrigger,
} from "@/components/ui/dropdown-menu.tsx";
import { Button } from "@/components/ui/button.tsx";
import {
CalendarClock,
ChevronLeft,
ChevronRight,
CloudCog,
MoreHorizontal,
RotateCw,
Search,
} from "lucide-react";
import { Input } from "@/components/ui/input.tsx";
import PopupDialog from "@/components/PopupDialog.tsx";
import {getNumber, parseNumber} from "@/utils/base.ts";

type OperationMenuProps = {
id: number;
};

function doToast(t: any, toast: any, resp: CommonResponse) {
if (!resp.status) toast({
title: t("admin.operate-failed"),
description: t("admin.operate-failed-prompt", { reason: resp.message }),
});
else toast({
title: t("admin.operate-success"),
description: t("admin.operate-success-prompt"),
});
}

function OperationMenu({ id }: OperationMenuProps) {
const { t } = useTranslation();
const { toast } = useToast();
const [quotaOpen, setQuotaOpen] = useState<boolean>(false);
const [subscriptionOpen, setSubscriptionOpen] = useState<boolean>(false);

return (
<>
<PopupDialog
title={t("admin.quota-action")} name={t("admin.quota")}
description={t("admin.quota-action-desc")}
defaultValue={"0"} onValueChange={getNumber}
open={quotaOpen} setOpen={setQuotaOpen}
onSubmit={async (value) => {
const quota = parseNumber(value);
const resp = await quotaOperation(id, quota);
doToast(t, toast, resp);
return resp.status;
}}
/>
<PopupDialog
title={t("admin.subscription-action")} name={t("admin.month")}
description={t("admin.subscription-action-desc")}
defaultValue={"0"} onValueChange={getNumber}
open={subscriptionOpen} setOpen={setSubscriptionOpen}
onSubmit={async (value) => {
const month = parseNumber(value);
const resp = await subscriptionOperation(id, month);
doToast(t, toast, resp);
return resp.status;
}}
/>
<DropdownMenu>
<DropdownMenuTrigger>
<Button variant={`outline`} size={`icon`}>
<MoreHorizontal className={`h-4 w-4`} />
</Button>
</DropdownMenuTrigger>
<DropdownMenuContent>
<DropdownMenuItem onClick={() => setQuotaOpen(true)}>
<CloudCog className={`h-4 w-4 mr-2`} />
{t("admin.quota-action")}
</DropdownMenuItem>
<DropdownMenuItem onClick={() => setSubscriptionOpen(true)}>
<CalendarClock className={`h-4 w-4 mr-2`} />
{t("admin.subscription-action")}
</DropdownMenuItem>
</DropdownMenuContent>
</DropdownMenu>
</>
);
}

function UserTable() {
const { t } = useTranslation();
Expand Down Expand Up @@ -73,7 +151,7 @@ function UserTable() {
</TableRow>
</TableHeader>
<TableBody>
{data.data.map((user, idx) => (
{(data.data || []).map((user, idx) => (
<TableRow key={idx}>
<TableCell>{user.id}</TableCell>
<TableCell>{user.username}</TableCell>
Expand All @@ -84,9 +162,7 @@ function UserTable() {
<TableCell>{t(user.enterprise.toString())}</TableCell>
<TableCell>{t(user.is_admin.toString())}</TableCell>
<TableCell>
<Button variant={`outline`} size={`icon`}>
<MoreHorizontal className={`h-4 w-4`} />
</Button>
<OperationMenu id={user.id} />
</TableCell>
</TableRow>
))}
Expand Down
Loading

0 comments on commit 38dad63

Please sign in to comment.