Commit d3384dc6 authored by Phạm Quang Bảo's avatar Phạm Quang Bảo

[tag]0.1-vcci

parents 8d2a2451 655ba51d
Pipeline #53131 passed with stages
in 4 minutes and 49 seconds
......@@ -3,11 +3,7 @@ import { AxiosError, isAxiosError } from 'axios'
import { QueryClient } from '@tanstack/react-query'
// App
// import router from '@/router'
import { handleAdminUnauthorized } from '@/lib/auth/admin-auth'
// import useProfileStore from '@stores/profile'
import { QueryData } from '@/lib/types/base-api'
// import { BASE_PATHS } from '@/constants/path'
// Constants
const RETRY_COUNT = 3
......@@ -25,35 +21,22 @@ const handleCheckBaseRetryLogical = (failureCount: number, error: Error) => {
return false
}
// Expired token error
// Expired token error — let the interceptor handle refresh, don't retry here
if (error.response?.status === EXPIRED_TOKEN_ERROR) {
if (typeof window !== "undefined" && window.location.pathname.startsWith("/admin")) {
handleUnAuthorizationError();
}
return false;
return false
}
// Denied permission error
if (error.response?.status === DENIED_PERMISSION_ERROR) {
// router.navigate('/')
window.location.href = process ? '/' : '/admin'
if (typeof window !== "undefined" && window.location.pathname.startsWith("/admin")) {
window.location.href = '/admin'
}
return false
}
return true
}
// Handle un authorization error
const handleUnAuthorizationError = () => {
void handleAdminUnauthorized()
// useProfileStore.getState().resetStore()
// const languageAwarePath = addLanguageToPath({
// path: BASE_PATHS.authSignIn
// })
// router.navigate('')
}
// Handle delay value
const handleDelayRetry = (failureCount: number) => failureCount * 1000 + Math.random() * 1000
......
import Axios, { AxiosError, AxiosRequestConfig } from "axios";
import links from "@/links";
const AXIOS_INSTANCE = Axios.create({
const instance = Axios.create({
baseURL: links.externalApiOrigin,
withCredentials: false,
});
type CustomClientPromise<T> = Promise<T> & {
cancel?: () => void;
};
const useCustomClient = <T>(
export async function useCustomClient<T>(
config: AxiosRequestConfig,
options?: AxiosRequestConfig,
): CustomClientPromise<T> => {
const source = Axios.CancelToken.source();
const promise = AXIOS_INSTANCE({ ...config, ...options, cancelToken: source.token })
.then(({ data }) => data as T) as CustomClientPromise<T>;
promise.cancel = () => {
source.cancel("Query was cancelled");
};
return promise;
};
export { useCustomClient };
): Promise<T> {
const response = await instance({ ...config, ...options });
return response.data as T;
}
export type ErrorType<Error> = AxiosError<Error>;
export type BodyType<BodyData> = BodyData;
......@@ -34,6 +34,7 @@ import type {
DeleteApiV10CategoryId200,
GetApiV10CategoryId200,
GetApiV10CategoryParams,
GetApiV10CategoryTree200,
PostApiV10Category200,
PutApiV10CategoryId200,
ResponseGetAllData
......@@ -629,4 +630,197 @@ export const usePostApiV10Category = <TError = ErrorType<unknown>,
return useMutation(mutationOptions, queryClient);
}
/**
* Retrieve hierarchical category tree with level field computed by backend. Each node includes children sorted by sort_order then name.
* @summary Get category tree
*/
export const getApiV10CategoryTree = (
options?: SecondParameter<typeof useCustomClient>,signal?: AbortSignal
) => {
return useCustomClient<GetApiV10CategoryTree200>(
{url: `/api/v1.0/category/tree`, method: 'GET', signal
},
options);
}
export const getGetApiV10CategoryTreeInfiniteQueryKey = () => {
return [
'infinite', `/api/v1.0/category/tree`
] as const;
}
export const getGetApiV10CategoryTreeQueryKey = () => {
return [
`/api/v1.0/category/tree`
] as const;
}
export const getGetApiV10CategoryTreeInfiniteQueryOptions = <TData = InfiniteData<Awaited<ReturnType<typeof getApiV10CategoryTree>>>, TError = ErrorType<unknown>>( options?: { query?:Partial<UseInfiniteQueryOptions<Awaited<ReturnType<typeof getApiV10CategoryTree>>, TError, TData>>, request?: SecondParameter<typeof useCustomClient>}
) => {
const {query: queryOptions, request: requestOptions} = options ?? {};
const queryKey = queryOptions?.queryKey ?? getGetApiV10CategoryTreeInfiniteQueryKey();
const queryFn: QueryFunction<Awaited<ReturnType<typeof getApiV10CategoryTree>>> = ({ signal }) => getApiV10CategoryTree(requestOptions, signal);
return { queryKey, queryFn, retry: 3, retryDelay: 1000, ...queryOptions} as UseInfiniteQueryOptions<Awaited<ReturnType<typeof getApiV10CategoryTree>>, TError, TData> & { queryKey: DataTag<QueryKey, TData, TError> }
}
export type GetApiV10CategoryTreeInfiniteQueryResult = NonNullable<Awaited<ReturnType<typeof getApiV10CategoryTree>>>
export type GetApiV10CategoryTreeInfiniteQueryError = ErrorType<unknown>
export function useGetApiV10CategoryTreeInfinite<TData = InfiniteData<Awaited<ReturnType<typeof getApiV10CategoryTree>>>, TError = ErrorType<unknown>>(
options: { query:Partial<UseInfiniteQueryOptions<Awaited<ReturnType<typeof getApiV10CategoryTree>>, TError, TData>> & Pick<
DefinedInitialDataOptions<
Awaited<ReturnType<typeof getApiV10CategoryTree>>,
TError,
Awaited<ReturnType<typeof getApiV10CategoryTree>>
> , 'initialData'
>, request?: SecondParameter<typeof useCustomClient>}
, queryClient?: QueryClient
): DefinedUseInfiniteQueryResult<TData, TError> & { queryKey: DataTag<QueryKey, TData, TError> }
export function useGetApiV10CategoryTreeInfinite<TData = InfiniteData<Awaited<ReturnType<typeof getApiV10CategoryTree>>>, TError = ErrorType<unknown>>(
options?: { query?:Partial<UseInfiniteQueryOptions<Awaited<ReturnType<typeof getApiV10CategoryTree>>, TError, TData>> & Pick<
UndefinedInitialDataOptions<
Awaited<ReturnType<typeof getApiV10CategoryTree>>,
TError,
Awaited<ReturnType<typeof getApiV10CategoryTree>>
> , 'initialData'
>, request?: SecondParameter<typeof useCustomClient>}
, queryClient?: QueryClient
): UseInfiniteQueryResult<TData, TError> & { queryKey: DataTag<QueryKey, TData, TError> }
export function useGetApiV10CategoryTreeInfinite<TData = InfiniteData<Awaited<ReturnType<typeof getApiV10CategoryTree>>>, TError = ErrorType<unknown>>(
options?: { query?:Partial<UseInfiniteQueryOptions<Awaited<ReturnType<typeof getApiV10CategoryTree>>, TError, TData>>, request?: SecondParameter<typeof useCustomClient>}
, queryClient?: QueryClient
): UseInfiniteQueryResult<TData, TError> & { queryKey: DataTag<QueryKey, TData, TError> }
/**
* @summary Get category tree
*/
export function useGetApiV10CategoryTreeInfinite<TData = InfiniteData<Awaited<ReturnType<typeof getApiV10CategoryTree>>>, TError = ErrorType<unknown>>(
options?: { query?:Partial<UseInfiniteQueryOptions<Awaited<ReturnType<typeof getApiV10CategoryTree>>, TError, TData>>, request?: SecondParameter<typeof useCustomClient>}
, queryClient?: QueryClient
): UseInfiniteQueryResult<TData, TError> & { queryKey: DataTag<QueryKey, TData, TError> } {
const queryOptions = getGetApiV10CategoryTreeInfiniteQueryOptions(options)
const query = useInfiniteQuery(queryOptions, queryClient) as UseInfiniteQueryResult<TData, TError> & { queryKey: DataTag<QueryKey, TData, TError> };
query.queryKey = queryOptions.queryKey ;
return query;
}
/**
* @summary Get category tree
*/
export const prefetchGetApiV10CategoryTreeInfiniteQuery = async <TData = Awaited<ReturnType<typeof getApiV10CategoryTree>>, TError = ErrorType<unknown>>(
queryClient: QueryClient, options?: { query?:Partial<UseInfiniteQueryOptions<Awaited<ReturnType<typeof getApiV10CategoryTree>>, TError, TData>>, request?: SecondParameter<typeof useCustomClient>}
): Promise<QueryClient> => {
const queryOptions = getGetApiV10CategoryTreeInfiniteQueryOptions(options)
await queryClient.prefetchInfiniteQuery(queryOptions);
return queryClient;
}
export const getGetApiV10CategoryTreeQueryOptions = <TData = Awaited<ReturnType<typeof getApiV10CategoryTree>>, TError = ErrorType<unknown>>( options?: { query?:Partial<UseQueryOptions<Awaited<ReturnType<typeof getApiV10CategoryTree>>, TError, TData>>, request?: SecondParameter<typeof useCustomClient>}
) => {
const {query: queryOptions, request: requestOptions} = options ?? {};
const queryKey = queryOptions?.queryKey ?? getGetApiV10CategoryTreeQueryKey();
const queryFn: QueryFunction<Awaited<ReturnType<typeof getApiV10CategoryTree>>> = ({ signal }) => getApiV10CategoryTree(requestOptions, signal);
return { queryKey, queryFn, retry: 3, retryDelay: 1000, ...queryOptions} as UseQueryOptions<Awaited<ReturnType<typeof getApiV10CategoryTree>>, TError, TData> & { queryKey: DataTag<QueryKey, TData, TError> }
}
export type GetApiV10CategoryTreeQueryResult = NonNullable<Awaited<ReturnType<typeof getApiV10CategoryTree>>>
export type GetApiV10CategoryTreeQueryError = ErrorType<unknown>
export function useGetApiV10CategoryTree<TData = Awaited<ReturnType<typeof getApiV10CategoryTree>>, TError = ErrorType<unknown>>(
options: { query:Partial<UseQueryOptions<Awaited<ReturnType<typeof getApiV10CategoryTree>>, TError, TData>> & Pick<
DefinedInitialDataOptions<
Awaited<ReturnType<typeof getApiV10CategoryTree>>,
TError,
Awaited<ReturnType<typeof getApiV10CategoryTree>>
> , 'initialData'
>, request?: SecondParameter<typeof useCustomClient>}
, queryClient?: QueryClient
): DefinedUseQueryResult<TData, TError> & { queryKey: DataTag<QueryKey, TData, TError> }
export function useGetApiV10CategoryTree<TData = Awaited<ReturnType<typeof getApiV10CategoryTree>>, TError = ErrorType<unknown>>(
options?: { query?:Partial<UseQueryOptions<Awaited<ReturnType<typeof getApiV10CategoryTree>>, TError, TData>> & Pick<
UndefinedInitialDataOptions<
Awaited<ReturnType<typeof getApiV10CategoryTree>>,
TError,
Awaited<ReturnType<typeof getApiV10CategoryTree>>
> , 'initialData'
>, request?: SecondParameter<typeof useCustomClient>}
, queryClient?: QueryClient
): UseQueryResult<TData, TError> & { queryKey: DataTag<QueryKey, TData, TError> }
export function useGetApiV10CategoryTree<TData = Awaited<ReturnType<typeof getApiV10CategoryTree>>, TError = ErrorType<unknown>>(
options?: { query?:Partial<UseQueryOptions<Awaited<ReturnType<typeof getApiV10CategoryTree>>, TError, TData>>, request?: SecondParameter<typeof useCustomClient>}
, queryClient?: QueryClient
): UseQueryResult<TData, TError> & { queryKey: DataTag<QueryKey, TData, TError> }
/**
* @summary Get category tree
*/
export function useGetApiV10CategoryTree<TData = Awaited<ReturnType<typeof getApiV10CategoryTree>>, TError = ErrorType<unknown>>(
options?: { query?:Partial<UseQueryOptions<Awaited<ReturnType<typeof getApiV10CategoryTree>>, TError, TData>>, request?: SecondParameter<typeof useCustomClient>}
, queryClient?: QueryClient
): UseQueryResult<TData, TError> & { queryKey: DataTag<QueryKey, TData, TError> } {
const queryOptions = getGetApiV10CategoryTreeQueryOptions(options)
const query = useQuery(queryOptions, queryClient) as UseQueryResult<TData, TError> & { queryKey: DataTag<QueryKey, TData, TError> };
query.queryKey = queryOptions.queryKey ;
return query;
}
/**
* @summary Get category tree
*/
export const prefetchGetApiV10CategoryTreeQuery = async <TData = Awaited<ReturnType<typeof getApiV10CategoryTree>>, TError = ErrorType<unknown>>(
queryClient: QueryClient, options?: { query?:Partial<UseQueryOptions<Awaited<ReturnType<typeof getApiV10CategoryTree>>, TError, TData>>, request?: SecondParameter<typeof useCustomClient>}
): Promise<QueryClient> => {
const queryOptions = getGetApiV10CategoryTreeQueryOptions(options)
await queryClient.prefetchQuery(queryOptions);
return queryClient;
}
......@@ -34,6 +34,8 @@ import type {
GetApiV10PostId200,
GetApiV10PostIdHistory200,
GetApiV10PostParams,
GetApiV10PostStats200,
GetApiV10PostStatsParams,
PostApiV10Post200,
PostMutate,
PutApiV10PostId200,
......@@ -824,4 +826,198 @@ export const usePostApiV10Post = <TError = ErrorType<unknown>,
return useMutation(mutationOptions, queryClient);
}
/**
* Retrieve aggregate post counts (total, published, featured) matching the given filters in a single request.
* @summary Get post stats
*/
export const getApiV10PostStats = (
params?: GetApiV10PostStatsParams,
options?: SecondParameter<typeof useCustomClient>,signal?: AbortSignal
) => {
return useCustomClient<GetApiV10PostStats200>(
{url: `/api/v1.0/post/stats`, method: 'GET',
params, signal
},
options);
}
export const getGetApiV10PostStatsInfiniteQueryKey = (params?: GetApiV10PostStatsParams,) => {
return [
'infinite', `/api/v1.0/post/stats`, ...(params ? [params]: [])
] as const;
}
export const getGetApiV10PostStatsQueryKey = (params?: GetApiV10PostStatsParams,) => {
return [
`/api/v1.0/post/stats`, ...(params ? [params]: [])
] as const;
}
export const getGetApiV10PostStatsInfiniteQueryOptions = <TData = InfiniteData<Awaited<ReturnType<typeof getApiV10PostStats>>>, TError = ErrorType<unknown>>(params?: GetApiV10PostStatsParams, options?: { query?:Partial<UseInfiniteQueryOptions<Awaited<ReturnType<typeof getApiV10PostStats>>, TError, TData>>, request?: SecondParameter<typeof useCustomClient>}
) => {
const {query: queryOptions, request: requestOptions} = options ?? {};
const queryKey = queryOptions?.queryKey ?? getGetApiV10PostStatsInfiniteQueryKey(params);
const queryFn: QueryFunction<Awaited<ReturnType<typeof getApiV10PostStats>>> = ({ signal }) => getApiV10PostStats(params, requestOptions, signal);
return { queryKey, queryFn, retry: 3, retryDelay: 1000, ...queryOptions} as UseInfiniteQueryOptions<Awaited<ReturnType<typeof getApiV10PostStats>>, TError, TData> & { queryKey: DataTag<QueryKey, TData, TError> }
}
export type GetApiV10PostStatsInfiniteQueryResult = NonNullable<Awaited<ReturnType<typeof getApiV10PostStats>>>
export type GetApiV10PostStatsInfiniteQueryError = ErrorType<unknown>
export function useGetApiV10PostStatsInfinite<TData = InfiniteData<Awaited<ReturnType<typeof getApiV10PostStats>>>, TError = ErrorType<unknown>>(
params: undefined | GetApiV10PostStatsParams, options: { query:Partial<UseInfiniteQueryOptions<Awaited<ReturnType<typeof getApiV10PostStats>>, TError, TData>> & Pick<
DefinedInitialDataOptions<
Awaited<ReturnType<typeof getApiV10PostStats>>,
TError,
Awaited<ReturnType<typeof getApiV10PostStats>>
> , 'initialData'
>, request?: SecondParameter<typeof useCustomClient>}
, queryClient?: QueryClient
): DefinedUseInfiniteQueryResult<TData, TError> & { queryKey: DataTag<QueryKey, TData, TError> }
export function useGetApiV10PostStatsInfinite<TData = InfiniteData<Awaited<ReturnType<typeof getApiV10PostStats>>>, TError = ErrorType<unknown>>(
params?: GetApiV10PostStatsParams, options?: { query?:Partial<UseInfiniteQueryOptions<Awaited<ReturnType<typeof getApiV10PostStats>>, TError, TData>> & Pick<
UndefinedInitialDataOptions<
Awaited<ReturnType<typeof getApiV10PostStats>>,
TError,
Awaited<ReturnType<typeof getApiV10PostStats>>
> , 'initialData'
>, request?: SecondParameter<typeof useCustomClient>}
, queryClient?: QueryClient
): UseInfiniteQueryResult<TData, TError> & { queryKey: DataTag<QueryKey, TData, TError> }
export function useGetApiV10PostStatsInfinite<TData = InfiniteData<Awaited<ReturnType<typeof getApiV10PostStats>>>, TError = ErrorType<unknown>>(
params?: GetApiV10PostStatsParams, options?: { query?:Partial<UseInfiniteQueryOptions<Awaited<ReturnType<typeof getApiV10PostStats>>, TError, TData>>, request?: SecondParameter<typeof useCustomClient>}
, queryClient?: QueryClient
): UseInfiniteQueryResult<TData, TError> & { queryKey: DataTag<QueryKey, TData, TError> }
/**
* @summary Get post stats
*/
export function useGetApiV10PostStatsInfinite<TData = InfiniteData<Awaited<ReturnType<typeof getApiV10PostStats>>>, TError = ErrorType<unknown>>(
params?: GetApiV10PostStatsParams, options?: { query?:Partial<UseInfiniteQueryOptions<Awaited<ReturnType<typeof getApiV10PostStats>>, TError, TData>>, request?: SecondParameter<typeof useCustomClient>}
, queryClient?: QueryClient
): UseInfiniteQueryResult<TData, TError> & { queryKey: DataTag<QueryKey, TData, TError> } {
const queryOptions = getGetApiV10PostStatsInfiniteQueryOptions(params,options)
const query = useInfiniteQuery(queryOptions, queryClient) as UseInfiniteQueryResult<TData, TError> & { queryKey: DataTag<QueryKey, TData, TError> };
query.queryKey = queryOptions.queryKey ;
return query;
}
/**
* @summary Get post stats
*/
export const prefetchGetApiV10PostStatsInfiniteQuery = async <TData = Awaited<ReturnType<typeof getApiV10PostStats>>, TError = ErrorType<unknown>>(
queryClient: QueryClient, params?: GetApiV10PostStatsParams, options?: { query?:Partial<UseInfiniteQueryOptions<Awaited<ReturnType<typeof getApiV10PostStats>>, TError, TData>>, request?: SecondParameter<typeof useCustomClient>}
): Promise<QueryClient> => {
const queryOptions = getGetApiV10PostStatsInfiniteQueryOptions(params,options)
await queryClient.prefetchInfiniteQuery(queryOptions);
return queryClient;
}
export const getGetApiV10PostStatsQueryOptions = <TData = Awaited<ReturnType<typeof getApiV10PostStats>>, TError = ErrorType<unknown>>(params?: GetApiV10PostStatsParams, options?: { query?:Partial<UseQueryOptions<Awaited<ReturnType<typeof getApiV10PostStats>>, TError, TData>>, request?: SecondParameter<typeof useCustomClient>}
) => {
const {query: queryOptions, request: requestOptions} = options ?? {};
const queryKey = queryOptions?.queryKey ?? getGetApiV10PostStatsQueryKey(params);
const queryFn: QueryFunction<Awaited<ReturnType<typeof getApiV10PostStats>>> = ({ signal }) => getApiV10PostStats(params, requestOptions, signal);
return { queryKey, queryFn, retry: 3, retryDelay: 1000, ...queryOptions} as UseQueryOptions<Awaited<ReturnType<typeof getApiV10PostStats>>, TError, TData> & { queryKey: DataTag<QueryKey, TData, TError> }
}
export type GetApiV10PostStatsQueryResult = NonNullable<Awaited<ReturnType<typeof getApiV10PostStats>>>
export type GetApiV10PostStatsQueryError = ErrorType<unknown>
export function useGetApiV10PostStats<TData = Awaited<ReturnType<typeof getApiV10PostStats>>, TError = ErrorType<unknown>>(
params: undefined | GetApiV10PostStatsParams, options: { query:Partial<UseQueryOptions<Awaited<ReturnType<typeof getApiV10PostStats>>, TError, TData>> & Pick<
DefinedInitialDataOptions<
Awaited<ReturnType<typeof getApiV10PostStats>>,
TError,
Awaited<ReturnType<typeof getApiV10PostStats>>
> , 'initialData'
>, request?: SecondParameter<typeof useCustomClient>}
, queryClient?: QueryClient
): DefinedUseQueryResult<TData, TError> & { queryKey: DataTag<QueryKey, TData, TError> }
export function useGetApiV10PostStats<TData = Awaited<ReturnType<typeof getApiV10PostStats>>, TError = ErrorType<unknown>>(
params?: GetApiV10PostStatsParams, options?: { query?:Partial<UseQueryOptions<Awaited<ReturnType<typeof getApiV10PostStats>>, TError, TData>> & Pick<
UndefinedInitialDataOptions<
Awaited<ReturnType<typeof getApiV10PostStats>>,
TError,
Awaited<ReturnType<typeof getApiV10PostStats>>
> , 'initialData'
>, request?: SecondParameter<typeof useCustomClient>}
, queryClient?: QueryClient
): UseQueryResult<TData, TError> & { queryKey: DataTag<QueryKey, TData, TError> }
export function useGetApiV10PostStats<TData = Awaited<ReturnType<typeof getApiV10PostStats>>, TError = ErrorType<unknown>>(
params?: GetApiV10PostStatsParams, options?: { query?:Partial<UseQueryOptions<Awaited<ReturnType<typeof getApiV10PostStats>>, TError, TData>>, request?: SecondParameter<typeof useCustomClient>}
, queryClient?: QueryClient
): UseQueryResult<TData, TError> & { queryKey: DataTag<QueryKey, TData, TError> }
/**
* @summary Get post stats
*/
export function useGetApiV10PostStats<TData = Awaited<ReturnType<typeof getApiV10PostStats>>, TError = ErrorType<unknown>>(
params?: GetApiV10PostStatsParams, options?: { query?:Partial<UseQueryOptions<Awaited<ReturnType<typeof getApiV10PostStats>>, TError, TData>>, request?: SecondParameter<typeof useCustomClient>}
, queryClient?: QueryClient
): UseQueryResult<TData, TError> & { queryKey: DataTag<QueryKey, TData, TError> } {
const queryOptions = getGetApiV10PostStatsQueryOptions(params,options)
const query = useQuery(queryOptions, queryClient) as UseQueryResult<TData, TError> & { queryKey: DataTag<QueryKey, TData, TError> };
query.queryKey = queryOptions.queryKey ;
return query;
}
/**
* @summary Get post stats
*/
export const prefetchGetApiV10PostStatsQuery = async <TData = Awaited<ReturnType<typeof getApiV10PostStats>>, TError = ErrorType<unknown>>(
queryClient: QueryClient, params?: GetApiV10PostStatsParams, options?: { query?:Partial<UseQueryOptions<Awaited<ReturnType<typeof getApiV10PostStats>>, TError, TData>>, request?: SecondParameter<typeof useCustomClient>}
): Promise<QueryClient> => {
const queryOptions = getGetApiV10PostStatsQueryOptions(params,options)
await queryClient.prefetchQuery(queryOptions);
return queryClient;
}
/**
* Generated by orval v7.17.0 🍺
* Do not edit manually.
* Backend Template
* Coded by Meu TEAM
* OpenAPI spec version: 1.0.0
*/
import type { ApiResponse } from './apiResponse';
import type { DeleteAdvertisementId200AllOf } from './deleteAdvertisementId200AllOf';
export type DeleteAdvertisementId200 = ApiResponse & DeleteAdvertisementId200AllOf;
/**
* Generated by orval v7.17.0 🍺
* Do not edit manually.
* Backend Template
* Coded by Meu TEAM
* OpenAPI spec version: 1.0.0
*/
export type DeleteAdvertisementId200AllOf = {
responseData?: boolean;
};
/**
* Generated by orval v7.17.0 🍺
* Do not edit manually.
* Backend Template
* Coded by Meu TEAM
* OpenAPI spec version: 1.0.0
*/
import type { ApiResponse } from './apiResponse';
import type { DeleteBannerId200AllOf } from './deleteBannerId200AllOf';
export type DeleteBannerId200 = ApiResponse & DeleteBannerId200AllOf;
/**
* Generated by orval v7.17.0 🍺
* Do not edit manually.
* Backend Template
* Coded by Meu TEAM
* OpenAPI spec version: 1.0.0
*/
export type DeleteBannerId200AllOf = {
responseData?: boolean;
};
/**
* Generated by orval v7.17.0 🍺
* Do not edit manually.
* Backend Template
* Coded by Meu TEAM
* OpenAPI spec version: 1.0.0
*/
import type { ApiResponse } from './apiResponse';
import type { DeleteCategoryId200AllOf } from './deleteCategoryId200AllOf';
export type DeleteCategoryId200 = ApiResponse & DeleteCategoryId200AllOf;
/**
* Generated by orval v7.17.0 🍺
* Do not edit manually.
* Backend Template
* Coded by Meu TEAM
* OpenAPI spec version: 1.0.0
*/
export type DeleteCategoryId200AllOf = {
responseData?: boolean;
};
/**
* Generated by orval v7.17.0 🍺
* Do not edit manually.
* Backend Template
* Coded by Meu TEAM
* OpenAPI spec version: 1.0.0
*/
import type { ApiResponse } from './apiResponse';
import type { DeleteContactId200AllOf } from './deleteContactId200AllOf';
export type DeleteContactId200 = ApiResponse & DeleteContactId200AllOf;
/**
* Generated by orval v7.17.0 🍺
* Do not edit manually.
* Backend Template
* Coded by Meu TEAM
* OpenAPI spec version: 1.0.0
*/
export type DeleteContactId200AllOf = {
responseData?: boolean;
};
/**
* Generated by orval v7.17.0 🍺
* Do not edit manually.
* Backend Template
* Coded by Meu TEAM
* OpenAPI spec version: 1.0.0
*/
import type { ApiResponse } from './apiResponse';
import type { DeleteFileId200AllOf } from './deleteFileId200AllOf';
export type DeleteFileId200 = ApiResponse & DeleteFileId200AllOf;
/**
* Generated by orval v7.17.0 🍺
* Do not edit manually.
* Backend Template
* Coded by Meu TEAM
* OpenAPI spec version: 1.0.0
*/
export type DeleteFileId200AllOf = {
responseData?: boolean;
};
/**
* Generated by orval v7.17.0 🍺
* Do not edit manually.
* Backend Template
* Coded by Meu TEAM
* OpenAPI spec version: 1.0.0
*/
import type { ApiResponse } from './apiResponse';
import type { DeleteLogoId200AllOf } from './deleteLogoId200AllOf';
export type DeleteLogoId200 = ApiResponse & DeleteLogoId200AllOf;
/**
* Generated by orval v7.17.0 🍺
* Do not edit manually.
* Backend Template
* Coded by Meu TEAM
* OpenAPI spec version: 1.0.0
*/
export type DeleteLogoId200AllOf = {
responseData?: boolean;
};
/**
* Generated by orval v7.17.0 🍺
* Do not edit manually.
* Backend Template
* Coded by Meu TEAM
* OpenAPI spec version: 1.0.0
*/
import type { ApiResponse } from './apiResponse';
import type { DeletePermissionBulk200AllOf } from './deletePermissionBulk200AllOf';
export type DeletePermissionBulk200 = ApiResponse & DeletePermissionBulk200AllOf;
/**
* Generated by orval v7.17.0 🍺
* Do not edit manually.
* Backend Template
* Coded by Meu TEAM
* OpenAPI spec version: 1.0.0
*/
import type { DeletePermissionBulk200AllOfResponseData } from './deletePermissionBulk200AllOfResponseData';
export type DeletePermissionBulk200AllOf = {
responseData?: DeletePermissionBulk200AllOfResponseData;
};
/**
* Generated by orval v7.17.0 🍺
* Do not edit manually.
* Backend Template
* Coded by Meu TEAM
* OpenAPI spec version: 1.0.0
*/
export type DeletePermissionBulk200AllOfResponseData = {
deletedCount?: number;
};
/**
* Generated by orval v7.17.0 🍺
* Do not edit manually.
* Backend Template
* Coded by Meu TEAM
* OpenAPI spec version: 1.0.0
*/
import type { ApiResponse } from './apiResponse';
import type { DeletePositionId200AllOf } from './deletePositionId200AllOf';
export type DeletePositionId200 = ApiResponse & DeletePositionId200AllOf;
/**
* Generated by orval v7.17.0 🍺
* Do not edit manually.
* Backend Template
* Coded by Meu TEAM
* OpenAPI spec version: 1.0.0
*/
export type DeletePositionId200AllOf = {
responseData?: boolean;
};
/**
* Generated by orval v7.17.0 🍺
* Do not edit manually.
* Backend Template
* Coded by Meu TEAM
* OpenAPI spec version: 1.0.0
*/
export type DeletePostCategoryPostIdParams = {
category_id: string;
};
/**
* Generated by orval v7.17.0 🍺
* Do not edit manually.
* Backend Template
* Coded by Meu TEAM
* OpenAPI spec version: 1.0.0
*/
import type { ApiResponse } from './apiResponse';
import type { DeletePostId200AllOf } from './deletePostId200AllOf';
export type DeletePostId200 = ApiResponse & DeletePostId200AllOf;
/**
* Generated by orval v7.17.0 🍺
* Do not edit manually.
* Backend Template
* Coded by Meu TEAM
* OpenAPI spec version: 1.0.0
*/
export type DeletePostId200AllOf = {
responseData?: boolean;
};
/**
* Generated by orval v7.17.0 🍺
* Do not edit manually.
* Backend Template
* Coded by Meu TEAM
* OpenAPI spec version: 1.0.0
*/
export type DeletePostTagPostIdParams = {
tag_id: string;
};
/**
* Generated by orval v7.17.0 🍺
* Do not edit manually.
* Backend Template
* Coded by Meu TEAM
* OpenAPI spec version: 1.0.0
*/
import type { ApiResponse } from './apiResponse';
import type { DeleteRoleBulk200AllOf } from './deleteRoleBulk200AllOf';
export type DeleteRoleBulk200 = ApiResponse & DeleteRoleBulk200AllOf;
/**
* Generated by orval v7.17.0 🍺
* Do not edit manually.
* Backend Template
* Coded by Meu TEAM
* OpenAPI spec version: 1.0.0
*/
import type { DeleteRoleBulk200AllOfResponseData } from './deleteRoleBulk200AllOfResponseData';
export type DeleteRoleBulk200AllOf = {
responseData?: DeleteRoleBulk200AllOfResponseData;
};
/**
* Generated by orval v7.17.0 🍺
* Do not edit manually.
* Backend Template
* Coded by Meu TEAM
* OpenAPI spec version: 1.0.0
*/
export type DeleteRoleBulk200AllOfResponseData = {
deletedCount?: number;
};
/**
* Generated by orval v7.17.0 🍺
* Do not edit manually.
* Backend Template
* Coded by Meu TEAM
* OpenAPI spec version: 1.0.0
*/
import type { ApiResponse } from './apiResponse';
import type { DeleteRolePermissionBulk200AllOf } from './deleteRolePermissionBulk200AllOf';
export type DeleteRolePermissionBulk200 = ApiResponse & DeleteRolePermissionBulk200AllOf;
/**
* Generated by orval v7.17.0 🍺
* Do not edit manually.
* Backend Template
* Coded by Meu TEAM
* OpenAPI spec version: 1.0.0
*/
import type { DeleteRolePermissionBulk200AllOfResponseData } from './deleteRolePermissionBulk200AllOfResponseData';
export type DeleteRolePermissionBulk200AllOf = {
responseData?: DeleteRolePermissionBulk200AllOfResponseData;
};
/**
* Generated by orval v7.17.0 🍺
* Do not edit manually.
* Backend Template
* Coded by Meu TEAM
* OpenAPI spec version: 1.0.0
*/
export type DeleteRolePermissionBulk200AllOfResponseData = {
deletedCount?: number;
};
/**
* Generated by orval v7.17.0 🍺
* Do not edit manually.
* Backend Template
* Coded by Meu TEAM
* OpenAPI spec version: 1.0.0
*/
import type { ApiResponse } from './apiResponse';
import type { DeleteRolePermissionRoleRoleId200AllOf } from './deleteRolePermissionRoleRoleId200AllOf';
export type DeleteRolePermissionRoleRoleId200 = ApiResponse & DeleteRolePermissionRoleRoleId200AllOf;
/**
* Generated by orval v7.17.0 🍺
* Do not edit manually.
* Backend Template
* Coded by Meu TEAM
* OpenAPI spec version: 1.0.0
*/
import type { DeleteRolePermissionRoleRoleId200AllOfResponseData } from './deleteRolePermissionRoleRoleId200AllOfResponseData';
export type DeleteRolePermissionRoleRoleId200AllOf = {
responseData?: DeleteRolePermissionRoleRoleId200AllOfResponseData;
};
/**
* Generated by orval v7.17.0 🍺
* Do not edit manually.
* Backend Template
* Coded by Meu TEAM
* OpenAPI spec version: 1.0.0
*/
export type DeleteRolePermissionRoleRoleId200AllOfResponseData = {
deletedCount?: number;
};
/**
* Generated by orval v7.17.0 🍺
* Do not edit manually.
* Backend Template
* Coded by Meu TEAM
* OpenAPI spec version: 1.0.0
*/
import type { ApiResponse } from './apiResponse';
import type { DeleteTermId200AllOf } from './deleteTermId200AllOf';
export type DeleteTermId200 = ApiResponse & DeleteTermId200AllOf;
/**
* Generated by orval v7.17.0 🍺
* Do not edit manually.
* Backend Template
* Coded by Meu TEAM
* OpenAPI spec version: 1.0.0
*/
export type DeleteTermId200AllOf = {
responseData?: boolean;
};
/**
* Generated by orval v7.17.0 🍺
* Do not edit manually.
* Backend Template
* Coded by Meu TEAM
* OpenAPI spec version: 1.0.0
*/
export type DeleteUserIdRoleBody = {
/** Role ID to remove */
role_id?: string;
};
/**
* Generated by orval v7.17.0 🍺
* Do not edit manually.
* Backend Template
* Coded by Meu TEAM
* OpenAPI spec version: 1.0.0
*/
import type { ApiResponse } from './apiResponse';
import type { DeleteUserRoleId200AllOf } from './deleteUserRoleId200AllOf';
export type DeleteUserRoleId200 = ApiResponse & DeleteUserRoleId200AllOf;
/**
* Generated by orval v7.17.0 🍺
* Do not edit manually.
* Backend Template
* Coded by Meu TEAM
* OpenAPI spec version: 1.0.0
*/
import type { DeleteUserRoleId200AllOfResponseData } from './deleteUserRoleId200AllOfResponseData';
export type DeleteUserRoleId200AllOf = {
responseData?: DeleteUserRoleId200AllOfResponseData;
};
/**
* Generated by orval v7.17.0 🍺
* Do not edit manually.
* Backend Template
* Coded by Meu TEAM
* OpenAPI spec version: 1.0.0
*/
export type DeleteUserRoleId200AllOfResponseData = {
id?: string;
deleted?: boolean;
};
/**
* Generated by orval v7.17.0 🍺
* Do not edit manually.
* Backend Template
* Coded by Meu TEAM
* OpenAPI spec version: 1.0.0
*/
import type { ApiResponse } from './apiResponse';
import type { GetAdvertisementId200AllOf } from './getAdvertisementId200AllOf';
export type GetAdvertisementId200 = ApiResponse & GetAdvertisementId200AllOf;
/**
* Generated by orval v7.17.0 🍺
* Do not edit manually.
* Backend Template
* Coded by Meu TEAM
* OpenAPI spec version: 1.0.0
*/
import type { Advertisement } from './advertisement';
export type GetAdvertisementId200AllOf = {
responseData?: Advertisement;
};
/**
* Generated by orval v7.17.0 🍺
* Do not edit manually.
* Backend Template
* Coded by Meu TEAM
* OpenAPI spec version: 1.0.0
*/
import type { FiltersParameter } from './filtersParameter';
import type { SortFieldParameter } from './sortFieldParameter';
import type { SortOrderParameter } from './sortOrderParameter';
import type { PageParameter } from './pageParameter';
import type { PageSizeParameter } from './pageSizeParameter';
export type GetAdvertisementParams = {
/**
* filter, visit https://www.npmjs.com/package/sequelize-api-paginate for syntax
*/
filters?: FiltersParameter;
/**
* sortField, visit https://www.npmjs.com/package/sequelize-api-paginate for syntax
*/
sortField?: SortFieldParameter;
/**
* sort order, visit https://www.npmjs.com/package/sequelize-api-paginate for syntax
*/
sortOrder?: SortOrderParameter;
/**
* page, visit https://www.npmjs.com/package/sequelize-api-paginate for syntax
* @minimum 1
*/
page?: PageParameter;
/**
* pageSize, visit https://www.npmjs.com/package/sequelize-api-paginate for syntax
* @minimum 1
*/
pageSize?: PageSizeParameter;
};
/**
* Generated by orval v7.17.0 🍺
* Do not edit manually.
* Backend Template
* Coded by Meu TEAM
* OpenAPI spec version: 1.0.0
*/
import type { ApiResponse } from './apiResponse';
import type { GetAdvertisementPublic200AllOf } from './getAdvertisementPublic200AllOf';
export type GetAdvertisementPublic200 = ApiResponse & GetAdvertisementPublic200AllOf;
/**
* Generated by orval v7.17.0 🍺
* Do not edit manually.
* Backend Template
* Coded by Meu TEAM
* OpenAPI spec version: 1.0.0
*/
import type { Advertisement } from './advertisement';
export type GetAdvertisementPublic200AllOf = {
responseData?: Advertisement[];
};
/**
* Generated by orval v7.17.0 🍺
* Do not edit manually.
* Backend Template
* Coded by Meu TEAM
* OpenAPI spec version: 1.0.0
*/
import type { GetAdvertisementPublicType } from './getAdvertisementPublicType';
export type GetAdvertisementPublicParams = {
/**
* Advertisement type (square or horizontal). If omitted, returns all active.
*/
type?: GetAdvertisementPublicType;
/**
* Max number of records to return (default 20)
* @minimum 1
* @maximum 100
*/
limit?: number;
};
/**
* Generated by orval v7.17.0 🍺
* Do not edit manually.
* Backend Template
* Coded by Meu TEAM
* OpenAPI spec version: 1.0.0
*/
export type GetAdvertisementPublicType = typeof GetAdvertisementPublicType[keyof typeof GetAdvertisementPublicType];
// eslint-disable-next-line @typescript-eslint/no-redeclare
export const GetAdvertisementPublicType = {
square: 'square',
horizontal: 'horizontal',
} as const;
/**
* Generated by orval v7.17.0 🍺
* Do not edit manually.
* VietProDev CMS Backend API
* Generated API documentation
* OpenAPI spec version: 1.0.0
*/
import type { ApiResponse } from './apiResponse';
import type { GetApiV10CategoryTree200AllOf } from './getApiV10CategoryTree200AllOf';
export type GetApiV10CategoryTree200 = ApiResponse & GetApiV10CategoryTree200AllOf;
/**
* Generated by orval v7.17.0 🍺
* Do not edit manually.
* VietProDev CMS Backend API
* Generated API documentation
* OpenAPI spec version: 1.0.0
*/
import type { Category } from './category';
export type GetApiV10CategoryTree200AllOf = {
responseData?: Category[];
};
/**
* Generated by orval v7.17.0 🍺
* Do not edit manually.
* VietProDev CMS Backend API
* Generated API documentation
* OpenAPI spec version: 1.0.0
*/
import type { ApiResponse } from './apiResponse';
import type { GetApiV10PostStats200AllOf } from './getApiV10PostStats200AllOf';
export type GetApiV10PostStats200 = ApiResponse & GetApiV10PostStats200AllOf;
/**
* Generated by orval v7.17.0 🍺
* Do not edit manually.
* VietProDev CMS Backend API
* Generated API documentation
* OpenAPI spec version: 1.0.0
*/
import type { GetApiV10PostStats200AllOfResponseData } from './getApiV10PostStats200AllOfResponseData';
export type GetApiV10PostStats200AllOf = {
responseData?: GetApiV10PostStats200AllOfResponseData;
};
/**
* Generated by orval v7.17.0 🍺
* Do not edit manually.
* VietProDev CMS Backend API
* Generated API documentation
* OpenAPI spec version: 1.0.0
*/
export type GetApiV10PostStats200AllOfResponseData = {
/** Total posts matching filters */
total?: number;
/** Published posts matching filters (is_hidden == false) */
published?: number;
/** Featured posts matching filters (is_featured == true) */
featured?: number;
};
/**
* Generated by orval v7.17.0 🍺
* Do not edit manually.
* VietProDev CMS Backend API
* Generated API documentation
* OpenAPI spec version: 1.0.0
*/
import type { FiltersParameter } from './filtersParameter';
export type GetApiV10PostStatsParams = {
/**
* filter, visit https://www.npmjs.com/package/sequelize-api-paginate for syntax
*/
filters?: FiltersParameter;
};
/**
* Generated by orval v7.17.0 🍺
* Do not edit manually.
* Backend Template
* Coded by Meu TEAM
* OpenAPI spec version: 1.0.0
*/
import type { ApiResponse } from './apiResponse';
import type { GetAuthMe200AllOf } from './getAuthMe200AllOf';
export type GetAuthMe200 = ApiResponse & GetAuthMe200AllOf;
/**
* Generated by orval v7.17.0 🍺
* Do not edit manually.
* Backend Template
* Coded by Meu TEAM
* OpenAPI spec version: 1.0.0
*/
import type { UserMeResponse } from './userMeResponse';
export type GetAuthMe200AllOf = {
responseData?: UserMeResponse;
};
/**
* Generated by orval v7.17.0 🍺
* Do not edit manually.
* Backend Template
* Coded by Meu TEAM
* OpenAPI spec version: 1.0.0
*/
import type { GetAuthProfile200Data } from './getAuthProfile200Data';
export type GetAuthProfile200 = {
success?: boolean;
data?: GetAuthProfile200Data;
};
/**
* Generated by orval v7.17.0 🍺
* Do not edit manually.
* Backend Template
* Coded by Meu TEAM
* OpenAPI spec version: 1.0.0
*/
import type { GetAuthProfile200DataRole } from './getAuthProfile200DataRole';
export type GetAuthProfile200Data = {
id?: string;
email?: string;
username?: string;
role?: GetAuthProfile200DataRole;
permissions?: string[];
};
/**
* Generated by orval v7.17.0 🍺
* Do not edit manually.
* Backend Template
* Coded by Meu TEAM
* OpenAPI spec version: 1.0.0
*/
export type GetAuthProfile200DataRole = typeof GetAuthProfile200DataRole[keyof typeof GetAuthProfile200DataRole];
// eslint-disable-next-line @typescript-eslint/no-redeclare
export const GetAuthProfile200DataRole = {
user: 'user',
admin: 'admin',
system_admin: 'system_admin',
} as const;
/**
* Generated by orval v7.17.0 🍺
* Do not edit manually.
* Backend Template
* Coded by Meu TEAM
* OpenAPI spec version: 1.0.0
*/
import type { ApiResponse } from './apiResponse';
import type { GetBannerId200AllOf } from './getBannerId200AllOf';
export type GetBannerId200 = ApiResponse & GetBannerId200AllOf;
/**
* Generated by orval v7.17.0 🍺
* Do not edit manually.
* Backend Template
* Coded by Meu TEAM
* OpenAPI spec version: 1.0.0
*/
import type { Banner } from './banner';
export type GetBannerId200AllOf = {
responseData?: Banner;
};
/**
* Generated by orval v7.17.0 🍺
* Do not edit manually.
* Backend Template
* Coded by Meu TEAM
* OpenAPI spec version: 1.0.0
*/
import type { FiltersParameter } from './filtersParameter';
import type { SortFieldParameter } from './sortFieldParameter';
import type { SortOrderParameter } from './sortOrderParameter';
import type { PageParameter } from './pageParameter';
import type { PageSizeParameter } from './pageSizeParameter';
export type GetBannerParams = {
/**
* filter, visit https://www.npmjs.com/package/sequelize-api-paginate for syntax
*/
filters?: FiltersParameter;
/**
* sortField, visit https://www.npmjs.com/package/sequelize-api-paginate for syntax
*/
sortField?: SortFieldParameter;
/**
* sort order, visit https://www.npmjs.com/package/sequelize-api-paginate for syntax
*/
sortOrder?: SortOrderParameter;
/**
* page, visit https://www.npmjs.com/package/sequelize-api-paginate for syntax
* @minimum 1
*/
page?: PageParameter;
/**
* pageSize, visit https://www.npmjs.com/package/sequelize-api-paginate for syntax
* @minimum 1
*/
pageSize?: PageSizeParameter;
};
/**
* Generated by orval v7.17.0 🍺
* Do not edit manually.
* Backend Template
* Coded by Meu TEAM
* OpenAPI spec version: 1.0.0
*/
import type { FiltersParameter } from './filtersParameter';
import type { SortFieldParameter } from './sortFieldParameter';
import type { SortOrderParameter } from './sortOrderParameter';
import type { PageParameter } from './pageParameter';
import type { PageSizeParameter } from './pageSizeParameter';
export type GetBusinessParams = {
/**
* filter, visit https://www.npmjs.com/package/sequelize-api-paginate for syntax
*/
filters?: FiltersParameter;
/**
* sortField, visit https://www.npmjs.com/package/sequelize-api-paginate for syntax
*/
sortField?: SortFieldParameter;
/**
* sort order, visit https://www.npmjs.com/package/sequelize-api-paginate for syntax
*/
sortOrder?: SortOrderParameter;
/**
* page, visit https://www.npmjs.com/package/sequelize-api-paginate for syntax
* @minimum 1
*/
page?: PageParameter;
/**
* pageSize, visit https://www.npmjs.com/package/sequelize-api-paginate for syntax
* @minimum 1
*/
pageSize?: PageSizeParameter;
};
/**
* Generated by orval v7.17.0 🍺
* Do not edit manually.
* Backend Template
* Coded by Meu TEAM
* OpenAPI spec version: 1.0.0
*/
import type { ApiResponse } from './apiResponse';
import type { GetCategoryId200AllOf } from './getCategoryId200AllOf';
export type GetCategoryId200 = ApiResponse & GetCategoryId200AllOf;
/**
* Generated by orval v7.17.0 🍺
* Do not edit manually.
* Backend Template
* Coded by Meu TEAM
* OpenAPI spec version: 1.0.0
*/
import type { Category } from './category';
export type GetCategoryId200AllOf = {
responseData?: Category;
};
/**
* Generated by orval v7.17.0 🍺
* Do not edit manually.
* Backend Template
* Coded by Meu TEAM
* OpenAPI spec version: 1.0.0
*/
import type { FiltersParameter } from './filtersParameter';
import type { SortFieldParameter } from './sortFieldParameter';
import type { SortOrderParameter } from './sortOrderParameter';
import type { PageParameter } from './pageParameter';
import type { PageSizeParameter } from './pageSizeParameter';
export type GetCategoryParams = {
/**
* filter, visit https://www.npmjs.com/package/sequelize-api-paginate for syntax
*/
filters?: FiltersParameter;
/**
* sortField, visit https://www.npmjs.com/package/sequelize-api-paginate for syntax
*/
sortField?: SortFieldParameter;
/**
* sort order, visit https://www.npmjs.com/package/sequelize-api-paginate for syntax
*/
sortOrder?: SortOrderParameter;
/**
* page, visit https://www.npmjs.com/package/sequelize-api-paginate for syntax
* @minimum 1
*/
page?: PageParameter;
/**
* pageSize, visit https://www.npmjs.com/package/sequelize-api-paginate for syntax
* @minimum 1
*/
pageSize?: PageSizeParameter;
};
/**
* Generated by orval v7.17.0 🍺
* Do not edit manually.
* Backend Template
* Coded by Meu TEAM
* OpenAPI spec version: 1.0.0
*/
import type { FiltersParameter } from './filtersParameter';
import type { SortFieldParameter } from './sortFieldParameter';
import type { SortOrderParameter } from './sortOrderParameter';
import type { PageParameter } from './pageParameter';
import type { PageSizeParameter } from './pageSizeParameter';
export type GetCategoryTagParams = {
/**
* filter, visit https://www.npmjs.com/package/sequelize-api-paginate for syntax
*/
filters?: FiltersParameter;
/**
* sortField, visit https://www.npmjs.com/package/sequelize-api-paginate for syntax
*/
sortField?: SortFieldParameter;
/**
* sort order, visit https://www.npmjs.com/package/sequelize-api-paginate for syntax
*/
sortOrder?: SortOrderParameter;
/**
* page, visit https://www.npmjs.com/package/sequelize-api-paginate for syntax
* @minimum 1
*/
page?: PageParameter;
/**
* pageSize, visit https://www.npmjs.com/package/sequelize-api-paginate for syntax
* @minimum 1
*/
pageSize?: PageSizeParameter;
};
/**
* Generated by orval v7.17.0 🍺
* Do not edit manually.
* Backend Template
* Coded by Meu TEAM
* OpenAPI spec version: 1.0.0
*/
import type { ApiResponse } from './apiResponse';
import type { GetContactId200AllOf } from './getContactId200AllOf';
export type GetContactId200 = ApiResponse & GetContactId200AllOf;
/**
* Generated by orval v7.17.0 🍺
* Do not edit manually.
* Backend Template
* Coded by Meu TEAM
* OpenAPI spec version: 1.0.0
*/
import type { Contact } from './contact';
export type GetContactId200AllOf = {
responseData?: Contact;
};
/**
* Generated by orval v7.17.0 🍺
* Do not edit manually.
* Backend Template
* Coded by Meu TEAM
* OpenAPI spec version: 1.0.0
*/
import type { FiltersParameter } from './filtersParameter';
import type { SortFieldParameter } from './sortFieldParameter';
import type { SortOrderParameter } from './sortOrderParameter';
import type { PageParameter } from './pageParameter';
import type { PageSizeParameter } from './pageSizeParameter';
export type GetContactParams = {
/**
* filter, visit https://www.npmjs.com/package/sequelize-api-paginate for syntax
*/
filters?: FiltersParameter;
/**
* sortField, visit https://www.npmjs.com/package/sequelize-api-paginate for syntax
*/
sortField?: SortFieldParameter;
/**
* sort order, visit https://www.npmjs.com/package/sequelize-api-paginate for syntax
*/
sortOrder?: SortOrderParameter;
/**
* page, visit https://www.npmjs.com/package/sequelize-api-paginate for syntax
* @minimum 1
*/
page?: PageParameter;
/**
* pageSize, visit https://www.npmjs.com/package/sequelize-api-paginate for syntax
* @minimum 1
*/
pageSize?: PageSizeParameter;
};
/**
* Generated by orval v7.17.0 🍺
* Do not edit manually.
* Backend Template
* Coded by Meu TEAM
* OpenAPI spec version: 1.0.0
*/
import type { ApiResponse } from './apiResponse';
import type { GetFileId200AllOf } from './getFileId200AllOf';
export type GetFileId200 = ApiResponse & GetFileId200AllOf;
/**
* Generated by orval v7.17.0 🍺
* Do not edit manually.
* Backend Template
* Coded by Meu TEAM
* OpenAPI spec version: 1.0.0
*/
import type { File } from './file';
export type GetFileId200AllOf = {
responseData?: File;
};
/**
* Generated by orval v7.17.0 🍺
* Do not edit manually.
* Backend Template
* Coded by Meu TEAM
* OpenAPI spec version: 1.0.0
*/
import type { FiltersParameter } from './filtersParameter';
import type { SortFieldParameter } from './sortFieldParameter';
import type { SortOrderParameter } from './sortOrderParameter';
import type { PageParameter } from './pageParameter';
import type { PageSizeParameter } from './pageSizeParameter';
export type GetFileParams = {
/**
* filter, visit https://www.npmjs.com/package/sequelize-api-paginate for syntax
*/
filters?: FiltersParameter;
/**
* sortField, visit https://www.npmjs.com/package/sequelize-api-paginate for syntax
*/
sortField?: SortFieldParameter;
/**
* sort order, visit https://www.npmjs.com/package/sequelize-api-paginate for syntax
*/
sortOrder?: SortOrderParameter;
/**
* page, visit https://www.npmjs.com/package/sequelize-api-paginate for syntax
* @minimum 1
*/
page?: PageParameter;
/**
* pageSize, visit https://www.npmjs.com/package/sequelize-api-paginate for syntax
* @minimum 1
*/
pageSize?: PageSizeParameter;
};
/**
* Generated by orval v7.17.0 🍺
* Do not edit manually.
* Backend Template
* Coded by Meu TEAM
* OpenAPI spec version: 1.0.0
*/
import type { ApiResponse } from './apiResponse';
import type { GetLogoId200AllOf } from './getLogoId200AllOf';
export type GetLogoId200 = ApiResponse & GetLogoId200AllOf;
/**
* Generated by orval v7.17.0 🍺
* Do not edit manually.
* Backend Template
* Coded by Meu TEAM
* OpenAPI spec version: 1.0.0
*/
import type { Logo } from './logo';
export type GetLogoId200AllOf = {
responseData?: Logo;
};
/**
* Generated by orval v7.17.0 🍺
* Do not edit manually.
* Backend Template
* Coded by Meu TEAM
* OpenAPI spec version: 1.0.0
*/
import type { FiltersParameter } from './filtersParameter';
import type { SortFieldParameter } from './sortFieldParameter';
import type { SortOrderParameter } from './sortOrderParameter';
import type { PageParameter } from './pageParameter';
import type { PageSizeParameter } from './pageSizeParameter';
export type GetLogoParams = {
/**
* filter, visit https://www.npmjs.com/package/sequelize-api-paginate for syntax
*/
filters?: FiltersParameter;
/**
* sortField, visit https://www.npmjs.com/package/sequelize-api-paginate for syntax
*/
sortField?: SortFieldParameter;
/**
* sort order, visit https://www.npmjs.com/package/sequelize-api-paginate for syntax
*/
sortOrder?: SortOrderParameter;
/**
* page, visit https://www.npmjs.com/package/sequelize-api-paginate for syntax
* @minimum 1
*/
page?: PageParameter;
/**
* pageSize, visit https://www.npmjs.com/package/sequelize-api-paginate for syntax
* @minimum 1
*/
pageSize?: PageSizeParameter;
};
/**
* Generated by orval v7.17.0 🍺
* Do not edit manually.
* Backend Template
* Coded by Meu TEAM
* OpenAPI spec version: 1.0.0
*/
import type { FiltersParameter } from './filtersParameter';
import type { SortFieldParameter } from './sortFieldParameter';
import type { SortOrderParameter } from './sortOrderParameter';
import type { PageParameter } from './pageParameter';
import type { PageSizeParameter } from './pageSizeParameter';
export type GetMemberParams = {
/**
* filter, visit https://www.npmjs.com/package/sequelize-api-paginate for syntax
*/
filters?: FiltersParameter;
/**
* sortField, visit https://www.npmjs.com/package/sequelize-api-paginate for syntax
*/
sortField?: SortFieldParameter;
/**
* sort order, visit https://www.npmjs.com/package/sequelize-api-paginate for syntax
*/
sortOrder?: SortOrderParameter;
/**
* page, visit https://www.npmjs.com/package/sequelize-api-paginate for syntax
* @minimum 1
*/
page?: PageParameter;
/**
* pageSize, visit https://www.npmjs.com/package/sequelize-api-paginate for syntax
* @minimum 1
*/
pageSize?: PageSizeParameter;
};
/**
* Generated by orval v7.17.0 🍺
* Do not edit manually.
* Backend Template
* Coded by Meu TEAM
* OpenAPI spec version: 1.0.0
*/
import type { FiltersParameter } from './filtersParameter';
import type { SortFieldParameter } from './sortFieldParameter';
import type { SortOrderParameter } from './sortOrderParameter';
import type { PageParameter } from './pageParameter';
import type { PageSizeParameter } from './pageSizeParameter';
export type GetNewsletterSubscriptionParams = {
/**
* filter, visit https://www.npmjs.com/package/sequelize-api-paginate for syntax
*/
filters?: FiltersParameter;
/**
* sortField, visit https://www.npmjs.com/package/sequelize-api-paginate for syntax
*/
sortField?: SortFieldParameter;
/**
* sort order, visit https://www.npmjs.com/package/sequelize-api-paginate for syntax
*/
sortOrder?: SortOrderParameter;
/**
* page, visit https://www.npmjs.com/package/sequelize-api-paginate for syntax
* @minimum 1
*/
page?: PageParameter;
/**
* pageSize, visit https://www.npmjs.com/package/sequelize-api-paginate for syntax
* @minimum 1
*/
pageSize?: PageSizeParameter;
};
/**
* Generated by orval v7.17.0 🍺
* Do not edit manually.
* Backend Template
* Coded by Meu TEAM
* OpenAPI spec version: 1.0.0
*/
export type GetPageConfigParams = {
/**
* ID of the page config to retrieve
*/
id?: string;
/**
* Static link of the page config to retrieve
*/
static_link?: string;
/**
* Code of the page config to retrieve
*/
code?: string;
};
/**
* Generated by orval v7.17.0 🍺
* Do not edit manually.
* Backend Template
* Coded by Meu TEAM
* OpenAPI spec version: 1.0.0
*/
import type { GetPasswordResetRequestStatus } from './getPasswordResetRequestStatus';
import type { GetPasswordResetRequestSortOrder } from './getPasswordResetRequestSortOrder';
export type GetPasswordResetRequestParams = {
page?: number;
pageSize?: number;
status?: GetPasswordResetRequestStatus;
sortField?: string;
sortOrder?: GetPasswordResetRequestSortOrder;
};
/**
* Generated by orval v7.17.0 🍺
* Do not edit manually.
* Backend Template
* Coded by Meu TEAM
* OpenAPI spec version: 1.0.0
*/
export type GetPasswordResetRequestSortOrder = typeof GetPasswordResetRequestSortOrder[keyof typeof GetPasswordResetRequestSortOrder];
// eslint-disable-next-line @typescript-eslint/no-redeclare
export const GetPasswordResetRequestSortOrder = {
asc: 'asc',
desc: 'desc',
} as const;
/**
* Generated by orval v7.17.0 🍺
* Do not edit manually.
* Backend Template
* Coded by Meu TEAM
* OpenAPI spec version: 1.0.0
*/
export type GetPasswordResetRequestStatus = typeof GetPasswordResetRequestStatus[keyof typeof GetPasswordResetRequestStatus];
// eslint-disable-next-line @typescript-eslint/no-redeclare
export const GetPasswordResetRequestStatus = {
PENDING: 'PENDING',
RESOLVED: 'RESOLVED',
REJECTED: 'REJECTED',
all: 'all',
} as const;
/**
* Generated by orval v7.17.0 🍺
* Do not edit manually.
* Backend Template
* Coded by Meu TEAM
* OpenAPI spec version: 1.0.0
*/
import type { ApiResponse } from './apiResponse';
import type { GetPermissionId200AllOf } from './getPermissionId200AllOf';
export type GetPermissionId200 = ApiResponse & GetPermissionId200AllOf;
/**
* Generated by orval v7.17.0 🍺
* Do not edit manually.
* Backend Template
* Coded by Meu TEAM
* OpenAPI spec version: 1.0.0
*/
import type { Permission } from './permission';
export type GetPermissionId200AllOf = {
responseData?: Permission;
};
/**
* Generated by orval v7.17.0 🍺
* Do not edit manually.
* Backend Template
* Coded by Meu TEAM
* OpenAPI spec version: 1.0.0
*/
import type { FiltersParameter } from './filtersParameter';
import type { SortFieldParameter } from './sortFieldParameter';
import type { SortOrderParameter } from './sortOrderParameter';
import type { PageParameter } from './pageParameter';
import type { PageSizeParameter } from './pageSizeParameter';
export type GetPermissionParams = {
/**
* filter, visit https://www.npmjs.com/package/sequelize-api-paginate for syntax
*/
filters?: FiltersParameter;
/**
* sortField, visit https://www.npmjs.com/package/sequelize-api-paginate for syntax
*/
sortField?: SortFieldParameter;
/**
* sort order, visit https://www.npmjs.com/package/sequelize-api-paginate for syntax
*/
sortOrder?: SortOrderParameter;
/**
* page, visit https://www.npmjs.com/package/sequelize-api-paginate for syntax
* @minimum 1
*/
page?: PageParameter;
/**
* pageSize, visit https://www.npmjs.com/package/sequelize-api-paginate for syntax
* @minimum 1
*/
pageSize?: PageSizeParameter;
};
/**
* Generated by orval v7.17.0 🍺
* Do not edit manually.
* Backend Template
* Coded by Meu TEAM
* OpenAPI spec version: 1.0.0
*/
import type { ApiResponse } from './apiResponse';
import type { GetPositionId200AllOf } from './getPositionId200AllOf';
export type GetPositionId200 = ApiResponse & GetPositionId200AllOf;
/**
* Generated by orval v7.17.0 🍺
* Do not edit manually.
* Backend Template
* Coded by Meu TEAM
* OpenAPI spec version: 1.0.0
*/
import type { Position } from './position';
export type GetPositionId200AllOf = {
responseData?: Position;
};
/**
* Generated by orval v7.17.0 🍺
* Do not edit manually.
* Backend Template
* Coded by Meu TEAM
* OpenAPI spec version: 1.0.0
*/
import type { FiltersParameter } from './filtersParameter';
import type { SortFieldParameter } from './sortFieldParameter';
import type { SortOrderParameter } from './sortOrderParameter';
import type { PageParameter } from './pageParameter';
import type { PageSizeParameter } from './pageSizeParameter';
export type GetPositionParams = {
/**
* filter, visit https://www.npmjs.com/package/sequelize-api-paginate for syntax
*/
filters?: FiltersParameter;
/**
* sortField, visit https://www.npmjs.com/package/sequelize-api-paginate for syntax
*/
sortField?: SortFieldParameter;
/**
* sort order, visit https://www.npmjs.com/package/sequelize-api-paginate for syntax
*/
sortOrder?: SortOrderParameter;
/**
* page, visit https://www.npmjs.com/package/sequelize-api-paginate for syntax
* @minimum 1
*/
page?: PageParameter;
/**
* pageSize, visit https://www.npmjs.com/package/sequelize-api-paginate for syntax
* @minimum 1
*/
pageSize?: PageSizeParameter;
};
/**
* Generated by orval v7.17.0 🍺
* Do not edit manually.
* Backend Template
* Coded by Meu TEAM
* OpenAPI spec version: 1.0.0
*/
import type { FiltersParameter } from './filtersParameter';
import type { SortFieldParameter } from './sortFieldParameter';
import type { SortOrderParameter } from './sortOrderParameter';
import type { PageParameter } from './pageParameter';
import type { PageSizeParameter } from './pageSizeParameter';
export type GetPostCategoryParams = {
/**
* filter, visit https://www.npmjs.com/package/sequelize-api-paginate for syntax
*/
filters?: FiltersParameter;
/**
* sortField, visit https://www.npmjs.com/package/sequelize-api-paginate for syntax
*/
sortField?: SortFieldParameter;
/**
* sort order, visit https://www.npmjs.com/package/sequelize-api-paginate for syntax
*/
sortOrder?: SortOrderParameter;
/**
* page, visit https://www.npmjs.com/package/sequelize-api-paginate for syntax
* @minimum 1
*/
page?: PageParameter;
/**
* pageSize, visit https://www.npmjs.com/package/sequelize-api-paginate for syntax
* @minimum 1
*/
pageSize?: PageSizeParameter;
};
/**
* Generated by orval v7.17.0 🍺
* Do not edit manually.
* Backend Template
* Coded by Meu TEAM
* OpenAPI spec version: 1.0.0
*/
import type { FiltersParameter } from './filtersParameter';
import type { SortFieldParameter } from './sortFieldParameter';
import type { SortOrderParameter } from './sortOrderParameter';
import type { PageParameter } from './pageParameter';
import type { PageSizeParameter } from './pageSizeParameter';
export type GetPostCategoryPostIdParams = {
/**
* filter, visit https://www.npmjs.com/package/sequelize-api-paginate for syntax
*/
filters?: FiltersParameter;
/**
* sortField, visit https://www.npmjs.com/package/sequelize-api-paginate for syntax
*/
sortField?: SortFieldParameter;
/**
* sort order, visit https://www.npmjs.com/package/sequelize-api-paginate for syntax
*/
sortOrder?: SortOrderParameter;
/**
* page, visit https://www.npmjs.com/package/sequelize-api-paginate for syntax
* @minimum 1
*/
page?: PageParameter;
/**
* pageSize, visit https://www.npmjs.com/package/sequelize-api-paginate for syntax
* @minimum 1
*/
pageSize?: PageSizeParameter;
};
/**
* Generated by orval v7.17.0 🍺
* Do not edit manually.
* Backend Template
* Coded by Meu TEAM
* OpenAPI spec version: 1.0.0
*/
import type { ApiResponse } from './apiResponse';
import type { GetPostId200AllOf } from './getPostId200AllOf';
export type GetPostId200 = ApiResponse & GetPostId200AllOf;
/**
* Generated by orval v7.17.0 🍺
* Do not edit manually.
* Backend Template
* Coded by Meu TEAM
* OpenAPI spec version: 1.0.0
*/
import type { Post } from './post';
export type GetPostId200AllOf = {
responseData?: Post;
};
/**
* Generated by orval v7.17.0 🍺
* Do not edit manually.
* Backend Template
* Coded by Meu TEAM
* OpenAPI spec version: 1.0.0
*/
import type { ApiResponse } from './apiResponse';
import type { GetPostIdHistory200AllOf } from './getPostIdHistory200AllOf';
export type GetPostIdHistory200 = ApiResponse & GetPostIdHistory200AllOf;
/**
* Generated by orval v7.17.0 🍺
* Do not edit manually.
* Backend Template
* Coded by Meu TEAM
* OpenAPI spec version: 1.0.0
*/
import type { GetPostIdHistory200AllOfResponseDataItem } from './getPostIdHistory200AllOfResponseDataItem';
export type GetPostIdHistory200AllOf = {
responseData?: GetPostIdHistory200AllOfResponseDataItem[];
};
/**
* Generated by orval v7.17.0 🍺
* Do not edit manually.
* Backend Template
* Coded by Meu TEAM
* OpenAPI spec version: 1.0.0
*/
import type { GetPostIdHistory200AllOfResponseDataItemAction } from './getPostIdHistory200AllOfResponseDataItemAction';
import type { GetPostIdHistory200AllOfResponseDataItemActor } from './getPostIdHistory200AllOfResponseDataItemActor';
import type { GetPostIdHistory200AllOfResponseDataItemChanges } from './getPostIdHistory200AllOfResponseDataItemChanges';
import type { GetPostIdHistory200AllOfResponseDataItemSnapshot } from './getPostIdHistory200AllOfResponseDataItemSnapshot';
export type GetPostIdHistory200AllOfResponseDataItem = {
id?: string;
post_id?: string;
action?: GetPostIdHistory200AllOfResponseDataItemAction;
actor?: GetPostIdHistory200AllOfResponseDataItemActor;
/**
* { field: { old: x, new: y } }
* @nullable
*/
changes?: GetPostIdHistory200AllOfResponseDataItemChanges;
/** @nullable */
snapshot?: GetPostIdHistory200AllOfResponseDataItemSnapshot;
created_at?: string;
};
/**
* Generated by orval v7.17.0 🍺
* Do not edit manually.
* Backend Template
* Coded by Meu TEAM
* OpenAPI spec version: 1.0.0
*/
export type GetPostIdHistory200AllOfResponseDataItemAction = typeof GetPostIdHistory200AllOfResponseDataItemAction[keyof typeof GetPostIdHistory200AllOfResponseDataItemAction];
// eslint-disable-next-line @typescript-eslint/no-redeclare
export const GetPostIdHistory200AllOfResponseDataItemAction = {
CREATE: 'CREATE',
UPDATE: 'UPDATE',
DELETE: 'DELETE',
} as const;
/**
* Generated by orval v7.17.0 🍺
* Do not edit manually.
* Backend Template
* Coded by Meu TEAM
* OpenAPI spec version: 1.0.0
*/
export type GetPostIdHistory200AllOfResponseDataItemActor = {
id?: string;
email?: string;
/** @nullable */
username?: string | null;
full_name?: string;
/** @nullable */
avatar_url?: string | null;
};
/**
* Generated by orval v7.17.0 🍺
* Do not edit manually.
* Backend Template
* Coded by Meu TEAM
* OpenAPI spec version: 1.0.0
*/
/**
* { field: { old: x, new: y } }
* @nullable
*/
export type GetPostIdHistory200AllOfResponseDataItemChanges = { [key: string]: unknown } | null;
/**
* Generated by orval v7.17.0 🍺
* Do not edit manually.
* Backend Template
* Coded by Meu TEAM
* OpenAPI spec version: 1.0.0
*/
/**
* @nullable
*/
export type GetPostIdHistory200AllOfResponseDataItemSnapshot = { [key: string]: unknown } | null;
/**
* Generated by orval v7.17.0 🍺
* Do not edit manually.
* Backend Template
* Coded by Meu TEAM
* OpenAPI spec version: 1.0.0
*/
import type { FiltersParameter } from './filtersParameter';
import type { SortFieldParameter } from './sortFieldParameter';
import type { SortOrderParameter } from './sortOrderParameter';
import type { PageParameter } from './pageParameter';
import type { PageSizeParameter } from './pageSizeParameter';
export type GetPostParams = {
/**
* filter, visit https://www.npmjs.com/package/sequelize-api-paginate for syntax
*/
filters?: FiltersParameter;
/**
* sortField, visit https://www.npmjs.com/package/sequelize-api-paginate for syntax
*/
sortField?: SortFieldParameter;
/**
* sort order, visit https://www.npmjs.com/package/sequelize-api-paginate for syntax
*/
sortOrder?: SortOrderParameter;
/**
* page, visit https://www.npmjs.com/package/sequelize-api-paginate for syntax
* @minimum 1
*/
page?: PageParameter;
/**
* pageSize, visit https://www.npmjs.com/package/sequelize-api-paginate for syntax
* @minimum 1
*/
pageSize?: PageSizeParameter;
};
/**
* Generated by orval v7.17.0 🍺
* Do not edit manually.
* Backend Template
* Coded by Meu TEAM
* OpenAPI spec version: 1.0.0
*/
import type { ApiResponse } from './apiResponse';
import type { GetPostStats200AllOf } from './getPostStats200AllOf';
export type GetPostStats200 = ApiResponse & GetPostStats200AllOf;
/**
* Generated by orval v7.17.0 🍺
* Do not edit manually.
* Backend Template
* Coded by Meu TEAM
* OpenAPI spec version: 1.0.0
*/
import type { GetPostStats200AllOfResponseData } from './getPostStats200AllOfResponseData';
export type GetPostStats200AllOf = {
responseData?: GetPostStats200AllOfResponseData;
};
/**
* Generated by orval v7.17.0 🍺
* Do not edit manually.
* Backend Template
* Coded by Meu TEAM
* OpenAPI spec version: 1.0.0
*/
export type GetPostStats200AllOfResponseData = {
/** Total posts matching filters */
total?: number;
/** Published posts matching filters (is_hidden == false) */
published?: number;
/** Featured posts matching filters (is_featured == true) */
featured?: number;
};
/**
* Generated by orval v7.17.0 🍺
* Do not edit manually.
* Backend Template
* Coded by Meu TEAM
* OpenAPI spec version: 1.0.0
*/
import type { FiltersParameter } from './filtersParameter';
export type GetPostStatsParams = {
/**
* filter, visit https://www.npmjs.com/package/sequelize-api-paginate for syntax
*/
filters?: FiltersParameter;
};
/**
* Generated by orval v7.17.0 🍺
* Do not edit manually.
* Backend Template
* Coded by Meu TEAM
* OpenAPI spec version: 1.0.0
*/
import type { FiltersParameter } from './filtersParameter';
import type { SortFieldParameter } from './sortFieldParameter';
import type { SortOrderParameter } from './sortOrderParameter';
import type { PageParameter } from './pageParameter';
import type { PageSizeParameter } from './pageSizeParameter';
export type GetPostTagParams = {
/**
* filter, visit https://www.npmjs.com/package/sequelize-api-paginate for syntax
*/
filters?: FiltersParameter;
/**
* sortField, visit https://www.npmjs.com/package/sequelize-api-paginate for syntax
*/
sortField?: SortFieldParameter;
/**
* sort order, visit https://www.npmjs.com/package/sequelize-api-paginate for syntax
*/
sortOrder?: SortOrderParameter;
/**
* page, visit https://www.npmjs.com/package/sequelize-api-paginate for syntax
* @minimum 1
*/
page?: PageParameter;
/**
* pageSize, visit https://www.npmjs.com/package/sequelize-api-paginate for syntax
* @minimum 1
*/
pageSize?: PageSizeParameter;
};
/**
* Generated by orval v7.17.0 🍺
* Do not edit manually.
* Backend Template
* Coded by Meu TEAM
* OpenAPI spec version: 1.0.0
*/
import type { FiltersParameter } from './filtersParameter';
import type { SortFieldParameter } from './sortFieldParameter';
import type { SortOrderParameter } from './sortOrderParameter';
import type { PageParameter } from './pageParameter';
import type { PageSizeParameter } from './pageSizeParameter';
export type GetPostTagPostIdParams = {
/**
* filter, visit https://www.npmjs.com/package/sequelize-api-paginate for syntax
*/
filters?: FiltersParameter;
/**
* sortField, visit https://www.npmjs.com/package/sequelize-api-paginate for syntax
*/
sortField?: SortFieldParameter;
/**
* sort order, visit https://www.npmjs.com/package/sequelize-api-paginate for syntax
*/
sortOrder?: SortOrderParameter;
/**
* page, visit https://www.npmjs.com/package/sequelize-api-paginate for syntax
* @minimum 1
*/
page?: PageParameter;
/**
* pageSize, visit https://www.npmjs.com/package/sequelize-api-paginate for syntax
* @minimum 1
*/
pageSize?: PageSizeParameter;
};
/**
* Generated by orval v7.17.0 🍺
* Do not edit manually.
* Backend Template
* Coded by Meu TEAM
* OpenAPI spec version: 1.0.0
*/
import type { FiltersParameter } from './filtersParameter';
import type { SortFieldParameter } from './sortFieldParameter';
import type { SortOrderParameter } from './sortOrderParameter';
import type { PageParameter } from './pageParameter';
import type { PageSizeParameter } from './pageSizeParameter';
export type GetRoleParams = {
/**
* filter, visit https://www.npmjs.com/package/sequelize-api-paginate for syntax
*/
filters?: FiltersParameter;
/**
* sortField, visit https://www.npmjs.com/package/sequelize-api-paginate for syntax
*/
sortField?: SortFieldParameter;
/**
* sort order, visit https://www.npmjs.com/package/sequelize-api-paginate for syntax
*/
sortOrder?: SortOrderParameter;
/**
* page, visit https://www.npmjs.com/package/sequelize-api-paginate for syntax
* @minimum 1
*/
page?: PageParameter;
/**
* pageSize, visit https://www.npmjs.com/package/sequelize-api-paginate for syntax
* @minimum 1
*/
pageSize?: PageSizeParameter;
};
/**
* Generated by orval v7.17.0 🍺
* Do not edit manually.
* Backend Template
* Coded by Meu TEAM
* OpenAPI spec version: 1.0.0
*/
import type { FiltersParameter } from './filtersParameter';
import type { SortFieldParameter } from './sortFieldParameter';
import type { SortOrderParameter } from './sortOrderParameter';
import type { PageParameter } from './pageParameter';
import type { PageSizeParameter } from './pageSizeParameter';
export type GetRolePermissionParams = {
/**
* filter, visit https://www.npmjs.com/package/sequelize-api-paginate for syntax
*/
filters?: FiltersParameter;
/**
* sortField, visit https://www.npmjs.com/package/sequelize-api-paginate for syntax
*/
sortField?: SortFieldParameter;
/**
* sort order, visit https://www.npmjs.com/package/sequelize-api-paginate for syntax
*/
sortOrder?: SortOrderParameter;
/**
* page, visit https://www.npmjs.com/package/sequelize-api-paginate for syntax
* @minimum 1
*/
page?: PageParameter;
/**
* pageSize, visit https://www.npmjs.com/package/sequelize-api-paginate for syntax
* @minimum 1
*/
pageSize?: PageSizeParameter;
};
/**
* Generated by orval v7.17.0 🍺
* Do not edit manually.
* Backend Template
* Coded by Meu TEAM
* OpenAPI spec version: 1.0.0
*/
import type { ApiResponse } from './apiResponse';
import type { GetSiteInformation200AllOf } from './getSiteInformation200AllOf';
export type GetSiteInformation200 = ApiResponse & GetSiteInformation200AllOf;
/**
* Generated by orval v7.17.0 🍺
* Do not edit manually.
* Backend Template
* Coded by Meu TEAM
* OpenAPI spec version: 1.0.0
*/
import type { SiteInformationData } from './siteInformationData';
export type GetSiteInformation200AllOf = {
responseData?: SiteInformationData;
};
/**
* Generated by orval v7.17.0 🍺
* Do not edit manually.
* Backend Template
* Coded by Meu TEAM
* OpenAPI spec version: 1.0.0
*/
import type { FiltersParameter } from './filtersParameter';
import type { SortFieldParameter } from './sortFieldParameter';
import type { SortOrderParameter } from './sortOrderParameter';
import type { PageParameter } from './pageParameter';
import type { PageSizeParameter } from './pageSizeParameter';
export type GetSiteInformationParams = {
/**
* filter, visit https://www.npmjs.com/package/sequelize-api-paginate for syntax
*/
filters?: FiltersParameter;
/**
* sortField, visit https://www.npmjs.com/package/sequelize-api-paginate for syntax
*/
sortField?: SortFieldParameter;
/**
* sort order, visit https://www.npmjs.com/package/sequelize-api-paginate for syntax
*/
sortOrder?: SortOrderParameter;
/**
* page, visit https://www.npmjs.com/package/sequelize-api-paginate for syntax
* @minimum 1
*/
page?: PageParameter;
/**
* pageSize, visit https://www.npmjs.com/package/sequelize-api-paginate for syntax
* @minimum 1
*/
pageSize?: PageSizeParameter;
};
/**
* Generated by orval v7.17.0 🍺
* Do not edit manually.
* Backend Template
* Coded by Meu TEAM
* OpenAPI spec version: 1.0.0
*/
import type { ApiResponse } from './apiResponse';
import type { GetTagId200AllOf } from './getTagId200AllOf';
export type GetTagId200 = ApiResponse & GetTagId200AllOf;
/**
* Generated by orval v7.17.0 🍺
* Do not edit manually.
* Backend Template
* Coded by Meu TEAM
* OpenAPI spec version: 1.0.0
*/
import type { Tag } from './tag';
export type GetTagId200AllOf = {
responseData?: Tag;
};
/**
* Generated by orval v7.17.0 🍺
* Do not edit manually.
* Backend Template
* Coded by Meu TEAM
* OpenAPI spec version: 1.0.0
*/
import type { FiltersParameter } from './filtersParameter';
import type { SortFieldParameter } from './sortFieldParameter';
import type { SortOrderParameter } from './sortOrderParameter';
import type { PageParameter } from './pageParameter';
import type { PageSizeParameter } from './pageSizeParameter';
export type GetTagParams = {
/**
* filter, visit https://www.npmjs.com/package/sequelize-api-paginate for syntax
*/
filters?: FiltersParameter;
/**
* sortField, visit https://www.npmjs.com/package/sequelize-api-paginate for syntax
*/
sortField?: SortFieldParameter;
/**
* sort order, visit https://www.npmjs.com/package/sequelize-api-paginate for syntax
*/
sortOrder?: SortOrderParameter;
/**
* page, visit https://www.npmjs.com/package/sequelize-api-paginate for syntax
* @minimum 1
*/
page?: PageParameter;
/**
* pageSize, visit https://www.npmjs.com/package/sequelize-api-paginate for syntax
* @minimum 1
*/
pageSize?: PageSizeParameter;
};
/**
* Generated by orval v7.17.0 🍺
* Do not edit manually.
* Backend Template
* Coded by Meu TEAM
* OpenAPI spec version: 1.0.0
*/
import type { ApiResponse } from './apiResponse';
import type { GetTerm200AllOf } from './getTerm200AllOf';
export type GetTerm200 = ApiResponse & GetTerm200AllOf;
/**
* Generated by orval v7.17.0 🍺
* Do not edit manually.
* Backend Template
* Coded by Meu TEAM
* OpenAPI spec version: 1.0.0
*/
import type { GetTerm200AllOfResponseData } from './getTerm200AllOfResponseData';
export type GetTerm200AllOf = {
responseData?: GetTerm200AllOfResponseData;
};
/**
* Generated by orval v7.17.0 🍺
* Do not edit manually.
* Backend Template
* Coded by Meu TEAM
* OpenAPI spec version: 1.0.0
*/
import type { TermResponse } from './termResponse';
export type GetTerm200AllOfResponseData = {
rows?: TermResponse[];
count?: number;
page?: number;
pageSize?: number;
};
/**
* Generated by orval v7.17.0 🍺
* Do not edit manually.
* Backend Template
* Coded by Meu TEAM
* OpenAPI spec version: 1.0.0
*/
import type { ApiResponse } from './apiResponse';
import type { GetTermId200AllOf } from './getTermId200AllOf';
export type GetTermId200 = ApiResponse & GetTermId200AllOf;
/**
* Generated by orval v7.17.0 🍺
* Do not edit manually.
* Backend Template
* Coded by Meu TEAM
* OpenAPI spec version: 1.0.0
*/
import type { TermResponse } from './termResponse';
export type GetTermId200AllOf = {
responseData?: TermResponse;
};
/**
* Generated by orval v7.17.0 🍺
* Do not edit manually.
* Backend Template
* Coded by Meu TEAM
* OpenAPI spec version: 1.0.0
*/
import type { FiltersParameter } from './filtersParameter';
import type { SortFieldParameter } from './sortFieldParameter';
import type { SortOrderParameter } from './sortOrderParameter';
import type { PageParameter } from './pageParameter';
import type { PageSizeParameter } from './pageSizeParameter';
export type GetTermParams = {
/**
* filter, visit https://www.npmjs.com/package/sequelize-api-paginate for syntax
*/
filters?: FiltersParameter;
/**
* sortField, visit https://www.npmjs.com/package/sequelize-api-paginate for syntax
*/
sortField?: SortFieldParameter;
/**
* sort order, visit https://www.npmjs.com/package/sequelize-api-paginate for syntax
*/
sortOrder?: SortOrderParameter;
/**
* page, visit https://www.npmjs.com/package/sequelize-api-paginate for syntax
* @minimum 1
*/
page?: PageParameter;
/**
* pageSize, visit https://www.npmjs.com/package/sequelize-api-paginate for syntax
* @minimum 1
*/
pageSize?: PageSizeParameter;
};
/**
* Generated by orval v7.17.0 🍺
* Do not edit manually.
* Backend Template
* Coded by Meu TEAM
* OpenAPI spec version: 1.0.0
*/
import type { FiltersParameter } from './filtersParameter';
import type { SortFieldParameter } from './sortFieldParameter';
import type { SortOrderParameter } from './sortOrderParameter';
import type { PageParameter } from './pageParameter';
import type { PageSizeParameter } from './pageSizeParameter';
export type GetUserParams = {
/**
* filter, visit https://www.npmjs.com/package/sequelize-api-paginate for syntax
*/
filters?: FiltersParameter;
/**
* sortField, visit https://www.npmjs.com/package/sequelize-api-paginate for syntax
*/
sortField?: SortFieldParameter;
/**
* sort order, visit https://www.npmjs.com/package/sequelize-api-paginate for syntax
*/
sortOrder?: SortOrderParameter;
/**
* page, visit https://www.npmjs.com/package/sequelize-api-paginate for syntax
* @minimum 1
*/
page?: PageParameter;
/**
* pageSize, visit https://www.npmjs.com/package/sequelize-api-paginate for syntax
* @minimum 1
*/
pageSize?: PageSizeParameter;
};
/**
* Generated by orval v7.17.0 🍺
* Do not edit manually.
* Backend Template
* Coded by Meu TEAM
* OpenAPI spec version: 1.0.0
*/
import type { ApiResponse } from './apiResponse';
import type { GetUserRole200AllOf } from './getUserRole200AllOf';
export type GetUserRole200 = ApiResponse & GetUserRole200AllOf;
/**
* Generated by orval v7.17.0 🍺
* Do not edit manually.
* Backend Template
* Coded by Meu TEAM
* OpenAPI spec version: 1.0.0
*/
import type { UserRoleListData } from './userRoleListData';
export type GetUserRole200AllOf = {
responseData?: UserRoleListData;
};
/**
* Generated by orval v7.17.0 🍺
* Do not edit manually.
* Backend Template
* Coded by Meu TEAM
* OpenAPI spec version: 1.0.0
*/
import type { ApiResponse } from './apiResponse';
import type { GetUserRoleId200AllOf } from './getUserRoleId200AllOf';
export type GetUserRoleId200 = ApiResponse & GetUserRoleId200AllOf;
/**
* Generated by orval v7.17.0 🍺
* Do not edit manually.
* Backend Template
* Coded by Meu TEAM
* OpenAPI spec version: 1.0.0
*/
import type { UserRole } from './userRole';
export type GetUserRoleId200AllOf = {
responseData?: UserRole;
};
/**
* Generated by orval v7.17.0 🍺
* Do not edit manually.
* Backend Template
* Coded by Meu TEAM
* OpenAPI spec version: 1.0.0
*/
import type { FiltersParameter } from './filtersParameter';
import type { SortFieldParameter } from './sortFieldParameter';
import type { SortOrderParameter } from './sortOrderParameter';
import type { PageParameter } from './pageParameter';
import type { PageSizeParameter } from './pageSizeParameter';
export type GetUserRoleParams = {
/**
* filter, visit https://www.npmjs.com/package/sequelize-api-paginate for syntax
*/
filters?: FiltersParameter;
/**
* sortField, visit https://www.npmjs.com/package/sequelize-api-paginate for syntax
*/
sortField?: SortFieldParameter;
/**
* sort order, visit https://www.npmjs.com/package/sequelize-api-paginate for syntax
*/
sortOrder?: SortOrderParameter;
/**
* page, visit https://www.npmjs.com/package/sequelize-api-paginate for syntax
* @minimum 1
*/
page?: PageParameter;
/**
* pageSize, visit https://www.npmjs.com/package/sequelize-api-paginate for syntax
* @minimum 1
*/
pageSize?: PageSizeParameter;
};
/**
* Generated by orval v7.17.0 🍺
* Do not edit manually.
* Backend Template
* Coded by Meu TEAM
* OpenAPI spec version: 1.0.0
*/
import type { ApiResponse } from './apiResponse';
import type { GetVideoId200AllOf } from './getVideoId200AllOf';
export type GetVideoId200 = ApiResponse & GetVideoId200AllOf;
/**
* Generated by orval v7.17.0 🍺
* Do not edit manually.
* Backend Template
* Coded by Meu TEAM
* OpenAPI spec version: 1.0.0
*/
import type { Video } from './video';
export type GetVideoId200AllOf = {
responseData?: Video;
};
/**
* Generated by orval v7.17.0 🍺
* Do not edit manually.
* Backend Template
* Coded by Meu TEAM
* OpenAPI spec version: 1.0.0
*/
import type { FiltersParameter } from './filtersParameter';
import type { SortFieldParameter } from './sortFieldParameter';
import type { SortOrderParameter } from './sortOrderParameter';
import type { PageParameter } from './pageParameter';
import type { PageSizeParameter } from './pageSizeParameter';
export type GetVideoParams = {
/**
* filter, visit https://www.npmjs.com/package/sequelize-api-paginate for syntax
*/
filters?: FiltersParameter;
/**
* sortField, visit https://www.npmjs.com/package/sequelize-api-paginate for syntax
*/
sortField?: SortFieldParameter;
/**
* sort order, visit https://www.npmjs.com/package/sequelize-api-paginate for syntax
*/
sortOrder?: SortOrderParameter;
/**
* page, visit https://www.npmjs.com/package/sequelize-api-paginate for syntax
* @minimum 1
*/
page?: PageParameter;
/**
* pageSize, visit https://www.npmjs.com/package/sequelize-api-paginate for syntax
* @minimum 1
*/
pageSize?: PageSizeParameter;
};
......@@ -120,6 +120,8 @@ export * from './getApiV10CategoryId200';
export * from './getApiV10CategoryId200AllOf';
export * from './getApiV10CategoryParams';
export * from './getApiV10CategoryTagParams';
export * from './getApiV10CategoryTree200';
export * from './getApiV10CategoryTree200AllOf';
export * from './getApiV10ContactId200';
export * from './getApiV10ContactId200AllOf';
export * from './getApiV10ContactParams';
......@@ -153,6 +155,10 @@ export * from './getApiV10PostIdHistory200AllOfResponseDataItemActor';
export * from './getApiV10PostIdHistory200AllOfResponseDataItemChanges';
export * from './getApiV10PostIdHistory200AllOfResponseDataItemSnapshot';
export * from './getApiV10PostParams';
export * from './getApiV10PostStats200';
export * from './getApiV10PostStats200AllOf';
export * from './getApiV10PostStats200AllOfResponseData';
export * from './getApiV10PostStatsParams';
export * from './getApiV10PostTagParams';
export * from './getApiV10PostTagPostIdParams';
export * from './getApiV10RoleParams';
......
/**
* Generated by orval v7.17.0 🍺
* Do not edit manually.
* Backend Template
* Coded by Meu TEAM
* OpenAPI spec version: 1.0.0
*/
import type { ApiResponse } from './apiResponse';
import type { PatchNewsletterSubscriptionId200AllOf } from './patchNewsletterSubscriptionId200AllOf';
export type PatchNewsletterSubscriptionId200 = ApiResponse & PatchNewsletterSubscriptionId200AllOf;
/**
* Generated by orval v7.17.0 🍺
* Do not edit manually.
* Backend Template
* Coded by Meu TEAM
* OpenAPI spec version: 1.0.0
*/
import type { NewsletterSubscription } from './newsletterSubscription';
export type PatchNewsletterSubscriptionId200AllOf = {
responseData?: NewsletterSubscription;
};
/**
* Generated by orval v7.17.0 🍺
* Do not edit manually.
* Backend Template
* Coded by Meu TEAM
* OpenAPI spec version: 1.0.0
*/
import type { ApiResponse } from './apiResponse';
import type { PatchTagId200AllOf } from './patchTagId200AllOf';
export type PatchTagId200 = ApiResponse & PatchTagId200AllOf;
/**
* Generated by orval v7.17.0 🍺
* Do not edit manually.
* Backend Template
* Coded by Meu TEAM
* OpenAPI spec version: 1.0.0
*/
import type { Tag } from './tag';
export type PatchTagId200AllOf = {
responseData?: Tag;
};
/**
* Generated by orval v7.17.0 🍺
* Do not edit manually.
* Backend Template
* Coded by Meu TEAM
* OpenAPI spec version: 1.0.0
*/
import type { PatchUserIdStatusBodyStatus } from './patchUserIdStatusBodyStatus';
export type PatchUserIdStatusBody = {
/** Trạng thái mới của user */
status?: PatchUserIdStatusBodyStatus;
};
/**
* Generated by orval v7.17.0 🍺
* Do not edit manually.
* Backend Template
* Coded by Meu TEAM
* OpenAPI spec version: 1.0.0
*/
/**
* Trạng thái mới của user
*/
export type PatchUserIdStatusBodyStatus = typeof PatchUserIdStatusBodyStatus[keyof typeof PatchUserIdStatusBodyStatus];
// eslint-disable-next-line @typescript-eslint/no-redeclare
export const PatchUserIdStatusBodyStatus = {
active: 'active',
inactive: 'inactive',
} as const;
/**
* Generated by orval v7.17.0 🍺
* Do not edit manually.
* Backend Template
* Coded by Meu TEAM
* OpenAPI spec version: 1.0.0
*/
import type { ApiResponse } from './apiResponse';
import type { PatchVideoId200AllOf } from './patchVideoId200AllOf';
export type PatchVideoId200 = ApiResponse & PatchVideoId200AllOf;
/**
* Generated by orval v7.17.0 🍺
* Do not edit manually.
* Backend Template
* Coded by Meu TEAM
* OpenAPI spec version: 1.0.0
*/
import type { Video } from './video';
export type PatchVideoId200AllOf = {
responseData?: Video;
};
/**
* Generated by orval v7.17.0 🍺
* Do not edit manually.
* Backend Template
* Coded by Meu TEAM
* OpenAPI spec version: 1.0.0
*/
import type { ApiResponse } from './apiResponse';
import type { PostAdvertisement200AllOf } from './postAdvertisement200AllOf';
export type PostAdvertisement200 = ApiResponse & PostAdvertisement200AllOf;
/**
* Generated by orval v7.17.0 🍺
* Do not edit manually.
* Backend Template
* Coded by Meu TEAM
* OpenAPI spec version: 1.0.0
*/
import type { Advertisement } from './advertisement';
export type PostAdvertisement200AllOf = {
responseData?: Advertisement;
};
/**
* Generated by orval v7.17.0 🍺
* Do not edit manually.
* Backend Template
* Coded by Meu TEAM
* OpenAPI spec version: 1.0.0
*/
export type PostAuthForgotPasswordRequestBody = {
email: string;
/** Ghi chú thêm từ user (tên, lý do, etc.) */
note?: string;
};
/**
* Generated by orval v7.17.0 🍺
* Do not edit manually.
* Backend Template
* Coded by Meu TEAM
* OpenAPI spec version: 1.0.0
*/
import type { ForgotPasswordVerifyOtpResponse } from './forgotPasswordVerifyOtpResponse';
export type PostAuthForgotPasswordVerifyOtp200 = {
responseData?: ForgotPasswordVerifyOtpResponse;
};
/**
* Generated by orval v7.17.0 🍺
* Do not edit manually.
* Backend Template
* Coded by Meu TEAM
* OpenAPI spec version: 1.0.0
*/
import type { LoginResponse } from './loginResponse';
import type { PostAuthLogin200Message } from './postAuthLogin200Message';
export type PostAuthLogin200 = {
success?: boolean;
data?: LoginResponse;
message?: PostAuthLogin200Message;
};
/**
* Generated by orval v7.17.0 🍺
* Do not edit manually.
* Backend Template
* Coded by Meu TEAM
* OpenAPI spec version: 1.0.0
*/
export type PostAuthLogin200Message = {
vi?: string;
en?: string;
};
/**
* Generated by orval v7.17.0 🍺
* Do not edit manually.
* Backend Template
* Coded by Meu TEAM
* OpenAPI spec version: 1.0.0
*/
import type { PostAuthLogin423Error } from './postAuthLogin423Error';
export type PostAuthLogin423 = {
success?: boolean;
error?: PostAuthLogin423Error;
};
/**
* Generated by orval v7.17.0 🍺
* Do not edit manually.
* Backend Template
* Coded by Meu TEAM
* OpenAPI spec version: 1.0.0
*/
import type { PostAuthLogin423ErrorMessage } from './postAuthLogin423ErrorMessage';
export type PostAuthLogin423Error = {
code?: string;
message?: PostAuthLogin423ErrorMessage;
};
/**
* Generated by orval v7.17.0 🍺
* Do not edit manually.
* Backend Template
* Coded by Meu TEAM
* OpenAPI spec version: 1.0.0
*/
export type PostAuthLogin423ErrorMessage = {
vi?: string;
en?: string;
};
/**
* Generated by orval v7.17.0 🍺
* Do not edit manually.
* Backend Template
* Coded by Meu TEAM
* OpenAPI spec version: 1.0.0
*/
import type { PostAuthLogout200Message } from './postAuthLogout200Message';
export type PostAuthLogout200 = {
success?: boolean;
message?: PostAuthLogout200Message;
};
/**
* Generated by orval v7.17.0 🍺
* Do not edit manually.
* Backend Template
* Coded by Meu TEAM
* OpenAPI spec version: 1.0.0
*/
export type PostAuthLogout200Message = {
vi?: string;
en?: string;
};
/**
* Generated by orval v7.17.0 🍺
* Do not edit manually.
* Backend Template
* Coded by Meu TEAM
* OpenAPI spec version: 1.0.0
*/
import type { LoginResponse } from './loginResponse';
import type { PostAuthRefresh200Message } from './postAuthRefresh200Message';
export type PostAuthRefresh200 = {
success?: boolean;
data?: LoginResponse;
message?: PostAuthRefresh200Message;
};
/**
* Generated by orval v7.17.0 🍺
* Do not edit manually.
* Backend Template
* Coded by Meu TEAM
* OpenAPI spec version: 1.0.0
*/
export type PostAuthRefresh200Message = {
vi?: string;
en?: string;
};
/**
* Generated by orval v7.17.0 🍺
* Do not edit manually.
* Backend Template
* Coded by Meu TEAM
* OpenAPI spec version: 1.0.0
*/
import type { PostAuthRefreshBodyDeviceInfo } from './postAuthRefreshBodyDeviceInfo';
export type PostAuthRefreshBody = {
/** Refresh token (optional if sent via cookie) */
refresh_token?: string;
/** Device information */
device_info?: PostAuthRefreshBodyDeviceInfo;
};
/**
* Generated by orval v7.17.0 🍺
* Do not edit manually.
* Backend Template
* Coded by Meu TEAM
* OpenAPI spec version: 1.0.0
*/
/**
* Device information
*/
export type PostAuthRefreshBodyDeviceInfo = { [key: string]: unknown };
/**
* Generated by orval v7.17.0 🍺
* Do not edit manually.
* Backend Template
* Coded by Meu TEAM
* OpenAPI spec version: 1.0.0
*/
import type { ApiResponse } from './apiResponse';
import type { PostBanner200AllOf } from './postBanner200AllOf';
export type PostBanner200 = ApiResponse & PostBanner200AllOf;
/**
* Generated by orval v7.17.0 🍺
* Do not edit manually.
* Backend Template
* Coded by Meu TEAM
* OpenAPI spec version: 1.0.0
*/
import type { Banner } from './banner';
export type PostBanner200AllOf = {
responseData?: Banner;
};
/**
* Generated by orval v7.17.0 🍺
* Do not edit manually.
* Backend Template
* Coded by Meu TEAM
* OpenAPI spec version: 1.0.0
*/
import type { ApiResponse } from './apiResponse';
import type { PostCategory200AllOf } from './postCategory200AllOf';
export type PostCategory200 = ApiResponse & PostCategory200AllOf;
/**
* Generated by orval v7.17.0 🍺
* Do not edit manually.
* Backend Template
* Coded by Meu TEAM
* OpenAPI spec version: 1.0.0
*/
import type { Category } from './category';
export type PostCategory200AllOf = {
responseData?: Category;
};
/**
* Generated by orval v7.17.0 🍺
* Do not edit manually.
* Backend Template
* Coded by Meu TEAM
* OpenAPI spec version: 1.0.0
*/
import type { ApiResponse } from './apiResponse';
import type { PostCategoryTag200AllOf } from './postCategoryTag200AllOf';
export type PostCategoryTag200 = ApiResponse & PostCategoryTag200AllOf;
/**
* Generated by orval v7.17.0 🍺
* Do not edit manually.
* Backend Template
* Coded by Meu TEAM
* OpenAPI spec version: 1.0.0
*/
import type { CategoryTag } from './categoryTag';
export type PostCategoryTag200AllOf = {
responseData?: CategoryTag;
};
/**
* Generated by orval v7.17.0 🍺
* Do not edit manually.
* Backend Template
* Coded by Meu TEAM
* OpenAPI spec version: 1.0.0
*/
import type { ApiResponse } from './apiResponse';
import type { PostCategoryTagCategoryIdBulk200AllOf } from './postCategoryTagCategoryIdBulk200AllOf';
export type PostCategoryTagCategoryIdBulk200 = ApiResponse & PostCategoryTagCategoryIdBulk200AllOf;
/**
* Generated by orval v7.17.0 🍺
* Do not edit manually.
* Backend Template
* Coded by Meu TEAM
* OpenAPI spec version: 1.0.0
*/
import type { CategoryTag } from './categoryTag';
export type PostCategoryTagCategoryIdBulk200AllOf = {
responseData?: CategoryTag[];
};
/**
* Generated by orval v7.17.0 🍺
* Do not edit manually.
* Backend Template
* Coded by Meu TEAM
* OpenAPI spec version: 1.0.0
*/
export type PostCategoryTagCategoryIdBulkBody = {
tag_ids?: string[];
};
/**
* Generated by orval v7.17.0 🍺
* Do not edit manually.
* Backend Template
* Coded by Meu TEAM
* OpenAPI spec version: 1.0.0
*/
import type { ApiResponse } from './apiResponse';
import type { PostContact200AllOf } from './postContact200AllOf';
export type PostContact200 = ApiResponse & PostContact200AllOf;
/**
* Generated by orval v7.17.0 🍺
* Do not edit manually.
* Backend Template
* Coded by Meu TEAM
* OpenAPI spec version: 1.0.0
*/
import type { Contact } from './contact';
export type PostContact200AllOf = {
responseData?: Contact;
};
/**
* Generated by orval v7.17.0 🍺
* Do not edit manually.
* Backend Template
* Coded by Meu TEAM
* OpenAPI spec version: 1.0.0
*/
import type { ApiResponse } from './apiResponse';
import type { PostFileUpload200AllOf } from './postFileUpload200AllOf';
export type PostFileUpload200 = ApiResponse & PostFileUpload200AllOf;
/**
* Generated by orval v7.17.0 🍺
* Do not edit manually.
* Backend Template
* Coded by Meu TEAM
* OpenAPI spec version: 1.0.0
*/
import type { File } from './file';
export type PostFileUpload200AllOf = {
responseData?: File;
};
/**
* Generated by orval v7.17.0 🍺
* Do not edit manually.
* Backend Template
* Coded by Meu TEAM
* OpenAPI spec version: 1.0.0
*/
export type PostFileUploadBody = {
/** File to upload */
file?: Blob;
/** Custom path for file storage (optional) */
path?: string;
/** Original filename (optional) */
original?: string;
/** Whether to compress the file */
compress?: boolean;
/** Custom extension to append to filename (optional) */
extendName?: string;
};
/**
* Generated by orval v7.17.0 🍺
* Do not edit manually.
* Backend Template
* Coded by Meu TEAM
* OpenAPI spec version: 1.0.0
*/
import type { ApiResponse } from './apiResponse';
import type { PostLogo200AllOf } from './postLogo200AllOf';
export type PostLogo200 = ApiResponse & PostLogo200AllOf;
/**
* Generated by orval v7.17.0 🍺
* Do not edit manually.
* Backend Template
* Coded by Meu TEAM
* OpenAPI spec version: 1.0.0
*/
import type { Logo } from './logo';
export type PostLogo200AllOf = {
responseData?: Logo;
};
/**
* Generated by orval v7.17.0 🍺
* Do not edit manually.
* Backend Template
* Coded by Meu TEAM
* OpenAPI spec version: 1.0.0
*/
import type { MemberImportResponse } from './memberImportResponse';
import type { PostMemberImport200Violations } from './postMemberImport200Violations';
export type PostMemberImport200 = {
message?: string;
message_en?: string;
responseData?: MemberImportResponse;
status?: string;
timeStamp?: string;
/** @nullable */
violations?: PostMemberImport200Violations;
};
/**
* Generated by orval v7.17.0 🍺
* Do not edit manually.
* Backend Template
* Coded by Meu TEAM
* OpenAPI spec version: 1.0.0
*/
/**
* @nullable
*/
export type PostMemberImport200Violations = unknown | null;
/**
* Generated by orval v7.17.0 🍺
* Do not edit manually.
* Backend Template
* Coded by Meu TEAM
* OpenAPI spec version: 1.0.0
*/
export type PostMemberImportBody = {
/** File .xlsx or .csv */
file: Blob;
/** @nullable */
sheet_name?: string | null;
};
/**
* Generated by orval v7.17.0 🍺
* Do not edit manually.
* Backend Template
* Coded by Meu TEAM
* OpenAPI spec version: 1.0.0
*/
import type { ApiResponse } from './apiResponse';
import type { PostNewsletterSubscription200AllOf } from './postNewsletterSubscription200AllOf';
export type PostNewsletterSubscription200 = ApiResponse & PostNewsletterSubscription200AllOf;
/**
* Generated by orval v7.17.0 🍺
* Do not edit manually.
* Backend Template
* Coded by Meu TEAM
* OpenAPI spec version: 1.0.0
*/
import type { NewsletterSubscription } from './newsletterSubscription';
export type PostNewsletterSubscription200AllOf = {
responseData?: NewsletterSubscription;
};
/**
* Generated by orval v7.17.0 🍺
* Do not edit manually.
* Backend Template
* Coded by Meu TEAM
* OpenAPI spec version: 1.0.0
*/
export type PostPageConfigBody = {
/** @maxLength 50 */
code: string;
/** @maxLength 255 */
name: string;
/** @maxLength 255 */
static_link: string;
/** @nullable */
static_link_en?: string | null;
parent_id: string;
/**
* @minimum 0
* @nullable
*/
sort_order?: number | null;
/** @nullable */
is_article?: boolean | null;
category_ids?: string[];
};
/**
* Generated by orval v7.17.0 🍺
* Do not edit manually.
* Backend Template
* Coded by Meu TEAM
* OpenAPI spec version: 1.0.0
*/
export type PostPasswordResetRequestIdRejectBody = {
/** Lý do từ chối */
resolveNote?: string;
};
/**
* Generated by orval v7.17.0 🍺
* Do not edit manually.
* Backend Template
* Coded by Meu TEAM
* OpenAPI spec version: 1.0.0
*/
export type PostPasswordResetRequestIdResolveBody = {
/** Mật khẩu mới (nếu bỏ trống sẽ dùng vcci@2026) */
newPassword?: string;
/** Ghi chú của admin */
resolveNote?: string;
};
/**
* Generated by orval v7.17.0 🍺
* Do not edit manually.
* Backend Template
* Coded by Meu TEAM
* OpenAPI spec version: 1.0.0
*/
import type { ApiResponse } from './apiResponse';
import type { PostPermissionBulk200AllOf } from './postPermissionBulk200AllOf';
export type PostPermissionBulk200 = ApiResponse & PostPermissionBulk200AllOf;
/**
* Generated by orval v7.17.0 🍺
* Do not edit manually.
* Backend Template
* Coded by Meu TEAM
* OpenAPI spec version: 1.0.0
*/
import type { Permission } from './permission';
export type PostPermissionBulk200AllOf = {
responseData?: Permission[];
};
/**
* Generated by orval v7.17.0 🍺
* Do not edit manually.
* Backend Template
* Coded by Meu TEAM
* OpenAPI spec version: 1.0.0
*/
import type { ApiResponse } from './apiResponse';
import type { PostPosition200AllOf } from './postPosition200AllOf';
export type PostPosition200 = ApiResponse & PostPosition200AllOf;
/**
* Generated by orval v7.17.0 🍺
* Do not edit manually.
* Backend Template
* Coded by Meu TEAM
* OpenAPI spec version: 1.0.0
*/
import type { Position } from './position';
export type PostPosition200AllOf = {
responseData?: Position;
};
/**
* Generated by orval v7.17.0 🍺
* Do not edit manually.
* Backend Template
* Coded by Meu TEAM
* OpenAPI spec version: 1.0.0
*/
import type { ApiResponse } from './apiResponse';
import type { PostPost200AllOf } from './postPost200AllOf';
export type PostPost200 = ApiResponse & PostPost200AllOf;
/**
* Generated by orval v7.17.0 🍺
* Do not edit manually.
* Backend Template
* Coded by Meu TEAM
* OpenAPI spec version: 1.0.0
*/
import type { Post } from './post';
export type PostPost200AllOf = {
responseData?: Post;
};
/**
* Generated by orval v7.17.0 🍺
* Do not edit manually.
* Backend Template
* Coded by Meu TEAM
* OpenAPI spec version: 1.0.0
*/
import type { ApiResponse } from './apiResponse';
import type { PostPostCategory200AllOf } from './postPostCategory200AllOf';
export type PostPostCategory200 = ApiResponse & PostPostCategory200AllOf;
/**
* Generated by orval v7.17.0 🍺
* Do not edit manually.
* Backend Template
* Coded by Meu TEAM
* OpenAPI spec version: 1.0.0
*/
import type { PostPostCategory200AllOfResponseData } from './postPostCategory200AllOfResponseData';
export type PostPostCategory200AllOf = {
responseData?: PostPostCategory200AllOfResponseData;
};
/**
* Generated by orval v7.17.0 🍺
* Do not edit manually.
* Backend Template
* Coded by Meu TEAM
* OpenAPI spec version: 1.0.0
*/
export type PostPostCategory200AllOfResponseData = {
post_id?: string;
category_id?: string;
created_at?: string;
created_by?: string;
};
/**
* Generated by orval v7.17.0 🍺
* Do not edit manually.
* Backend Template
* Coded by Meu TEAM
* OpenAPI spec version: 1.0.0
*/
export type PostPostCategoryBody = {
post_id?: string;
category_id?: string;
created_by?: string;
};
/**
* Generated by orval v7.17.0 🍺
* Do not edit manually.
* Backend Template
* Coded by Meu TEAM
* OpenAPI spec version: 1.0.0
*/
import type { ApiResponse } from './apiResponse';
import type { PostPostCategoryPostIdBulk200AllOf } from './postPostCategoryPostIdBulk200AllOf';
export type PostPostCategoryPostIdBulk200 = ApiResponse & PostPostCategoryPostIdBulk200AllOf;
/**
* Generated by orval v7.17.0 🍺
* Do not edit manually.
* Backend Template
* Coded by Meu TEAM
* OpenAPI spec version: 1.0.0
*/
import type { PostPostCategoryPostIdBulk200AllOfResponseDataItem } from './postPostCategoryPostIdBulk200AllOfResponseDataItem';
export type PostPostCategoryPostIdBulk200AllOf = {
responseData?: PostPostCategoryPostIdBulk200AllOfResponseDataItem[];
};
/**
* Generated by orval v7.17.0 🍺
* Do not edit manually.
* Backend Template
* Coded by Meu TEAM
* OpenAPI spec version: 1.0.0
*/
export type PostPostCategoryPostIdBulk200AllOfResponseDataItem = {
post_id?: string;
category_id?: string;
created_at?: string;
created_by?: string;
};
/**
* Generated by orval v7.17.0 🍺
* Do not edit manually.
* Backend Template
* Coded by Meu TEAM
* OpenAPI spec version: 1.0.0
*/
export type PostPostCategoryPostIdBulkBody = {
category_ids?: string[];
created_by?: string;
};
/**
* Generated by orval v7.17.0 🍺
* Do not edit manually.
* Backend Template
* Coded by Meu TEAM
* OpenAPI spec version: 1.0.0
*/
import type { ApiResponse } from './apiResponse';
import type { PostPostTag200AllOf } from './postPostTag200AllOf';
export type PostPostTag200 = ApiResponse & PostPostTag200AllOf;
/**
* Generated by orval v7.17.0 🍺
* Do not edit manually.
* Backend Template
* Coded by Meu TEAM
* OpenAPI spec version: 1.0.0
*/
import type { PostPostTag200AllOfResponseData } from './postPostTag200AllOfResponseData';
export type PostPostTag200AllOf = {
responseData?: PostPostTag200AllOfResponseData;
};
/**
* Generated by orval v7.17.0 🍺
* Do not edit manually.
* Backend Template
* Coded by Meu TEAM
* OpenAPI spec version: 1.0.0
*/
export type PostPostTag200AllOfResponseData = {
post_id?: string;
tag_id?: string;
created_at?: string;
};
/**
* Generated by orval v7.17.0 🍺
* Do not edit manually.
* Backend Template
* Coded by Meu TEAM
* OpenAPI spec version: 1.0.0
*/
export type PostPostTagBody = {
post_id?: string;
tag_id?: string;
};
/**
* Generated by orval v7.17.0 🍺
* Do not edit manually.
* Backend Template
* Coded by Meu TEAM
* OpenAPI spec version: 1.0.0
*/
import type { ApiResponse } from './apiResponse';
import type { PostPostTagPostIdBulk200AllOf } from './postPostTagPostIdBulk200AllOf';
export type PostPostTagPostIdBulk200 = ApiResponse & PostPostTagPostIdBulk200AllOf;
/**
* Generated by orval v7.17.0 🍺
* Do not edit manually.
* Backend Template
* Coded by Meu TEAM
* OpenAPI spec version: 1.0.0
*/
import type { PostPostTagPostIdBulk200AllOfResponseDataItem } from './postPostTagPostIdBulk200AllOfResponseDataItem';
export type PostPostTagPostIdBulk200AllOf = {
responseData?: PostPostTagPostIdBulk200AllOfResponseDataItem[];
};
/**
* Generated by orval v7.17.0 🍺
* Do not edit manually.
* Backend Template
* Coded by Meu TEAM
* OpenAPI spec version: 1.0.0
*/
export type PostPostTagPostIdBulk200AllOfResponseDataItem = {
post_id?: string;
tag_id?: string;
created_at?: string;
};
/**
* Generated by orval v7.17.0 🍺
* Do not edit manually.
* Backend Template
* Coded by Meu TEAM
* OpenAPI spec version: 1.0.0
*/
export type PostPostTagPostIdBulkBody = {
tag_ids?: string[];
};
/**
* Generated by orval v7.17.0 🍺
* Do not edit manually.
* Backend Template
* Coded by Meu TEAM
* OpenAPI spec version: 1.0.0
*/
export type PostRoleBody = {
name?: string;
description?: string;
permissions?: string[];
};
/**
* Generated by orval v7.17.0 🍺
* Do not edit manually.
* Backend Template
* Coded by Meu TEAM
* OpenAPI spec version: 1.0.0
*/
import type { ApiResponse } from './apiResponse';
import type { PostRoleBulk200AllOf } from './postRoleBulk200AllOf';
export type PostRoleBulk200 = ApiResponse & PostRoleBulk200AllOf;
/**
* Generated by orval v7.17.0 🍺
* Do not edit manually.
* Backend Template
* Coded by Meu TEAM
* OpenAPI spec version: 1.0.0
*/
import type { Role } from './role';
export type PostRoleBulk200AllOf = {
responseData?: Role[];
};
/**
* Generated by orval v7.17.0 🍺
* Do not edit manually.
* Backend Template
* Coded by Meu TEAM
* OpenAPI spec version: 1.0.0
*/
import type { ApiResponse } from './apiResponse';
import type { PostRolePermissionRoleRoleIdBulk200AllOf } from './postRolePermissionRoleRoleIdBulk200AllOf';
export type PostRolePermissionRoleRoleIdBulk200 = ApiResponse & PostRolePermissionRoleRoleIdBulk200AllOf;
/**
* Generated by orval v7.17.0 🍺
* Do not edit manually.
* Backend Template
* Coded by Meu TEAM
* OpenAPI spec version: 1.0.0
*/
import type { RolePermission } from './rolePermission';
export type PostRolePermissionRoleRoleIdBulk200AllOf = {
responseData?: RolePermission[];
};
/**
* Generated by orval v7.17.0 🍺
* Do not edit manually.
* Backend Template
* Coded by Meu TEAM
* OpenAPI spec version: 1.0.0
*/
import type { ApiResponse } from './apiResponse';
import type { PostTag200AllOf } from './postTag200AllOf';
export type PostTag200 = ApiResponse & PostTag200AllOf;
/**
* Generated by orval v7.17.0 🍺
* Do not edit manually.
* Backend Template
* Coded by Meu TEAM
* OpenAPI spec version: 1.0.0
*/
import type { Tag } from './tag';
export type PostTag200AllOf = {
responseData?: Tag;
};
/**
* Generated by orval v7.17.0 🍺
* Do not edit manually.
* Backend Template
* Coded by Meu TEAM
* OpenAPI spec version: 1.0.0
*/
import type { ApiResponse } from './apiResponse';
import type { PostTagIds200AllOf } from './postTagIds200AllOf';
export type PostTagIds200 = ApiResponse & PostTagIds200AllOf;
/**
* Generated by orval v7.17.0 🍺
* Do not edit manually.
* Backend Template
* Coded by Meu TEAM
* OpenAPI spec version: 1.0.0
*/
import type { Tag } from './tag';
export type PostTagIds200AllOf = {
responseData?: Tag[];
};
/**
* Generated by orval v7.17.0 🍺
* Do not edit manually.
* Backend Template
* Coded by Meu TEAM
* OpenAPI spec version: 1.0.0
*/
export type PostTagIdsBody = {
tag_ids?: string[];
};
/**
* Generated by orval v7.17.0 🍺
* Do not edit manually.
* Backend Template
* Coded by Meu TEAM
* OpenAPI spec version: 1.0.0
*/
import type { ApiResponse } from './apiResponse';
import type { PostTerm201AllOf } from './postTerm201AllOf';
export type PostTerm201 = ApiResponse & PostTerm201AllOf;
/**
* Generated by orval v7.17.0 🍺
* Do not edit manually.
* Backend Template
* Coded by Meu TEAM
* OpenAPI spec version: 1.0.0
*/
import type { TermResponse } from './termResponse';
export type PostTerm201AllOf = {
responseData?: TermResponse;
};
/**
* Generated by orval v7.17.0 🍺
* Do not edit manually.
* Backend Template
* Coded by Meu TEAM
* OpenAPI spec version: 1.0.0
*/
export type PostUserIdRoleBody = {
/** Role ID to assign */
role_id?: string;
/** Set as primary role */
is_primary?: boolean;
};
/**
* Generated by orval v7.17.0 🍺
* Do not edit manually.
* Backend Template
* Coded by Meu TEAM
* OpenAPI spec version: 1.0.0
*/
import type { ApiResponse } from './apiResponse';
import type { PostUserRole200AllOf } from './postUserRole200AllOf';
export type PostUserRole200 = ApiResponse & PostUserRole200AllOf;
/**
* Generated by orval v7.17.0 🍺
* Do not edit manually.
* Backend Template
* Coded by Meu TEAM
* OpenAPI spec version: 1.0.0
*/
import type { UserRole } from './userRole';
export type PostUserRole200AllOf = {
responseData?: UserRole;
};
/**
* Generated by orval v7.17.0 🍺
* Do not edit manually.
* Backend Template
* Coded by Meu TEAM
* OpenAPI spec version: 1.0.0
*/
import type { ApiResponse } from './apiResponse';
import type { PostVideo200AllOf } from './postVideo200AllOf';
export type PostVideo200 = ApiResponse & PostVideo200AllOf;
/**
* Generated by orval v7.17.0 🍺
* Do not edit manually.
* Backend Template
* Coded by Meu TEAM
* OpenAPI spec version: 1.0.0
*/
import type { Video } from './video';
export type PostVideo200AllOf = {
responseData?: Video;
};
/**
* Generated by orval v7.17.0 🍺
* Do not edit manually.
* Backend Template
* Coded by Meu TEAM
* OpenAPI spec version: 1.0.0
*/
import type { ApiResponse } from './apiResponse';
import type { PutAdvertisementId200AllOf } from './putAdvertisementId200AllOf';
export type PutAdvertisementId200 = ApiResponse & PutAdvertisementId200AllOf;
/**
* Generated by orval v7.17.0 🍺
* Do not edit manually.
* Backend Template
* Coded by Meu TEAM
* OpenAPI spec version: 1.0.0
*/
import type { Advertisement } from './advertisement';
export type PutAdvertisementId200AllOf = {
responseData?: Advertisement;
};
/**
* Generated by orval v7.17.0 🍺
* Do not edit manually.
* Backend Template
* Coded by Meu TEAM
* OpenAPI spec version: 1.0.0
*/
import type { ApiResponse } from './apiResponse';
import type { PutBannerId200AllOf } from './putBannerId200AllOf';
export type PutBannerId200 = ApiResponse & PutBannerId200AllOf;
/**
* Generated by orval v7.17.0 🍺
* Do not edit manually.
* Backend Template
* Coded by Meu TEAM
* OpenAPI spec version: 1.0.0
*/
import type { Banner } from './banner';
export type PutBannerId200AllOf = {
responseData?: Banner;
};
/**
* Generated by orval v7.17.0 🍺
* Do not edit manually.
* Backend Template
* Coded by Meu TEAM
* OpenAPI spec version: 1.0.0
*/
import type { ApiResponse } from './apiResponse';
import type { PutCategoryId200AllOf } from './putCategoryId200AllOf';
export type PutCategoryId200 = ApiResponse & PutCategoryId200AllOf;
/**
* Generated by orval v7.17.0 🍺
* Do not edit manually.
* Backend Template
* Coded by Meu TEAM
* OpenAPI spec version: 1.0.0
*/
import type { Category } from './category';
export type PutCategoryId200AllOf = {
responseData?: Category;
};
/**
* Generated by orval v7.17.0 🍺
* Do not edit manually.
* Backend Template
* Coded by Meu TEAM
* OpenAPI spec version: 1.0.0
*/
import type { ApiResponse } from './apiResponse';
import type { PutContactId200AllOf } from './putContactId200AllOf';
export type PutContactId200 = ApiResponse & PutContactId200AllOf;
/**
* Generated by orval v7.17.0 🍺
* Do not edit manually.
* Backend Template
* Coded by Meu TEAM
* OpenAPI spec version: 1.0.0
*/
import type { Contact } from './contact';
export type PutContactId200AllOf = {
responseData?: Contact;
};
/**
* Generated by orval v7.17.0 🍺
* Do not edit manually.
* Backend Template
* Coded by Meu TEAM
* OpenAPI spec version: 1.0.0
*/
import type { ApiResponse } from './apiResponse';
import type { PutLogoId200AllOf } from './putLogoId200AllOf';
export type PutLogoId200 = ApiResponse & PutLogoId200AllOf;
/**
* Generated by orval v7.17.0 🍺
* Do not edit manually.
* Backend Template
* Coded by Meu TEAM
* OpenAPI spec version: 1.0.0
*/
import type { Logo } from './logo';
export type PutLogoId200AllOf = {
responseData?: Logo;
};
/**
* Generated by orval v7.17.0 🍺
* Do not edit manually.
* Backend Template
* Coded by Meu TEAM
* OpenAPI spec version: 1.0.0
*/
export type PutPageConfigIdBody = {
/** @maxLength 50 */
code?: string;
/** @maxLength 255 */
name?: string;
/** @maxLength 255 */
static_link?: string;
/** @nullable */
static_link_en?: string | null;
parent_id?: string;
/**
* @minimum 0
* @nullable
*/
sort_order?: number | null;
/** @nullable */
is_article?: boolean | null;
};
/**
* Generated by orval v7.17.0 🍺
* Do not edit manually.
* Backend Template
* Coded by Meu TEAM
* OpenAPI spec version: 1.0.0
*/
import type { ApiResponse } from './apiResponse';
import type { PutPermissionId200AllOf } from './putPermissionId200AllOf';
export type PutPermissionId200 = ApiResponse & PutPermissionId200AllOf;
/**
* Generated by orval v7.17.0 🍺
* Do not edit manually.
* Backend Template
* Coded by Meu TEAM
* OpenAPI spec version: 1.0.0
*/
import type { Permission } from './permission';
export type PutPermissionId200AllOf = {
responseData?: Permission;
};
/**
* Generated by orval v7.17.0 🍺
* Do not edit manually.
* Backend Template
* Coded by Meu TEAM
* OpenAPI spec version: 1.0.0
*/
import type { ApiResponse } from './apiResponse';
import type { PutPositionId200AllOf } from './putPositionId200AllOf';
export type PutPositionId200 = ApiResponse & PutPositionId200AllOf;
/**
* Generated by orval v7.17.0 🍺
* Do not edit manually.
* Backend Template
* Coded by Meu TEAM
* OpenAPI spec version: 1.0.0
*/
import type { Position } from './position';
export type PutPositionId200AllOf = {
responseData?: Position;
};
/**
* Generated by orval v7.17.0 🍺
* Do not edit manually.
* Backend Template
* Coded by Meu TEAM
* OpenAPI spec version: 1.0.0
*/
import type { ApiResponse } from './apiResponse';
import type { PutPostId200AllOf } from './putPostId200AllOf';
export type PutPostId200 = ApiResponse & PutPostId200AllOf;
/**
* Generated by orval v7.17.0 🍺
* Do not edit manually.
* Backend Template
* Coded by Meu TEAM
* OpenAPI spec version: 1.0.0
*/
import type { Post } from './post';
export type PutPostId200AllOf = {
responseData?: Post;
};
/**
* Generated by orval v7.17.0 🍺
* Do not edit manually.
* Backend Template
* Coded by Meu TEAM
* OpenAPI spec version: 1.0.0
*/
import type { ApiResponse } from './apiResponse';
import type { PutSiteInformation200AllOf } from './putSiteInformation200AllOf';
export type PutSiteInformation200 = ApiResponse & PutSiteInformation200AllOf;
/**
* Generated by orval v7.17.0 🍺
* Do not edit manually.
* Backend Template
* Coded by Meu TEAM
* OpenAPI spec version: 1.0.0
*/
import type { SiteInformationData } from './siteInformationData';
export type PutSiteInformation200AllOf = {
responseData?: SiteInformationData;
};
/**
* Generated by orval v7.17.0 🍺
* Do not edit manually.
* Backend Template
* Coded by Meu TEAM
* OpenAPI spec version: 1.0.0
*/
import type { ApiResponse } from './apiResponse';
import type { PutTermId200AllOf } from './putTermId200AllOf';
export type PutTermId200 = ApiResponse & PutTermId200AllOf;
/**
* Generated by orval v7.17.0 🍺
* Do not edit manually.
* Backend Template
* Coded by Meu TEAM
* OpenAPI spec version: 1.0.0
*/
import type { TermResponse } from './termResponse';
export type PutTermId200AllOf = {
responseData?: TermResponse;
};
/**
* Generated by orval v7.17.0 🍺
* Do not edit manually.
* Backend Template
* Coded by Meu TEAM
* OpenAPI spec version: 1.0.0
*/
import type { ApiResponse } from './apiResponse';
import type { PutUserRoleId200AllOf } from './putUserRoleId200AllOf';
export type PutUserRoleId200 = ApiResponse & PutUserRoleId200AllOf;
/**
* Generated by orval v7.17.0 🍺
* Do not edit manually.
* Backend Template
* Coded by Meu TEAM
* OpenAPI spec version: 1.0.0
*/
import type { UserRole } from './userRole';
export type PutUserRoleId200AllOf = {
responseData?: UserRole;
};
import Axios, { AxiosError, AxiosHeaders, AxiosRequestConfig, InternalAxiosRequestConfig } from "axios";
import links from "@/links";
import {
ensureValidAdminAccessToken,
refreshAdminAccessToken,
} from "@/lib/auth/admin-auth";
import useAuthStore from "@/store/useAuthStore";
import useUserStore from "@/store/useUserStore";
interface RetriableAxiosRequestConfig extends InternalAxiosRequestConfig {
_retry?: boolean;
}
const createAxiosInstance = () => {
const instance = Axios.create({
baseURL: links.apiEndpoint,
withCredentials: false,
});
let refreshPromise: Promise<string | null> | null = null;
instance.interceptors.request.use(async (config) => {
if (shouldSkipAuthHandling(config.url) || !shouldHandleAdminAuth()) {
config.withCredentials = false;
return config;
}
const shouldSkipAuthHandling = (url?: string | null) => {
if (!url) return false;
return /\/auth\/(login|refresh|logout)(\?|$)/.test(url);
};
const shouldHandleAdminAuth = () => {
if (typeof window === "undefined") return false;
return window.location.pathname.startsWith("/admin");
};
config.withCredentials = true;
const token = await ensureValidAdminAccessToken().catch(() => null);
const instance = Axios.create({
baseURL: links.apiEndpoint,
withCredentials: false,
});
if (token) {
const headers = AxiosHeaders.from(config.headers);
headers.set("Authorization", `Bearer ${token}`);
config.headers = headers;
instance.interceptors.request.use((config) => {
const authStore = useAuthStore.getState();
if (authStore.appAccessToken) {
config.headers.set("Authorization", `Bearer ${authStore.appAccessToken}`);
}
return config;
});
});
instance.interceptors.response.use(
instance.interceptors.response.use(
async (response) => response,
async (error: AxiosError) => {
const originalRequest = error.config as RetriableAxiosRequestConfig | undefined;
......@@ -51,199 +51,76 @@ const createAxiosInstance = () => {
originalRequest._retry = true;
try {
const nextAccessToken = await refreshAdminAccessToken();
if (!nextAccessToken) {
return Promise.reject(error);
}
const headers = AxiosHeaders.from(originalRequest.headers);
headers.set("Authorization", `Bearer ${nextAccessToken}`);
originalRequest.headers = headers;
if (!refreshPromise) {
refreshPromise = (async () => {
const { appRefreshToken } = useAuthStore.getState();
return instance(originalRequest);
} catch (refreshError) {
return Promise.reject(refreshError);
if (!appRefreshToken) {
return null;
}
},
);
return instance;
};
const AXIOS_INSTANCE = createAxiosInstance();
const GET_CACHE_TTL = 2 * 60 * 1000;
const MAX_GET_CACHE_SIZE = 100;
type CustomClientPromise<T> = Promise<T> & {
cancel?: () => void;
};
type CachedGetResponse = {
expiresAt: number;
value: unknown;
};
const inFlightGetRequests = new Map<string, CustomClientPromise<unknown>>();
const cachedGetResponses = new Map<string, CachedGetResponse>();
const shouldSkipAuthHandling = (url?: string | null) => {
if (!url) return false;
return /\/auth\/(login|refresh|logout)(\?|$)/.test(url);
};
const shouldHandleAdminAuth = () => {
if (typeof window === "undefined") return false;
return window.location.pathname.startsWith("/admin");
};
const getHeadersRecord = (config: AxiosRequestConfig): Record<string, string> => {
const headers = config.headers;
if (!headers) return {};
if (headers instanceof AxiosHeaders) {
const result: Record<string, string> = {};
headers.forEach((value: string, key: string) => {
result[key] = String(value);
});
return result;
}
if (Array.isArray(headers)) {
const result: Record<string, string> = {};
headers.forEach(([key, value]) => {
result[key] = String(value);
try {
const { data } = await instance.post("/api/v1.0/auth/refresh", {
refresh_token: appRefreshToken,
});
return result;
}
return headers as Record<string, string>;
};
const hasAuthorizationHeader = (config: AxiosRequestConfig) => {
return Object.keys(getHeadersRecord(config)).some(
(key) => key.toLowerCase() === "authorization",
);
};
const shouldUseGetCache = (config: AxiosRequestConfig) => {
const method = (config.method || "get").toUpperCase();
return (
method === "GET" &&
!config.data &&
!hasAuthorizationHeader(config) &&
!shouldSkipAuthHandling(config.url)
);
};
const makeGetCacheKey = (config: AxiosRequestConfig) => {
const headers = getHeadersRecord(config);
const headerKey = Object.keys(headers)
.sort()
.map((key) => `${key}:${headers[key]}`)
.join("|");
const paramsKey = config.params ? JSON.stringify(config.params) : "";
return `${config.url}::${headerKey}::${paramsKey}`;
};
const clearGetCache = () => {
inFlightGetRequests.clear();
cachedGetResponses.clear();
};
const trimGetCache = () => {
while (cachedGetResponses.size > MAX_GET_CACHE_SIZE) {
const firstKey = cachedGetResponses.keys().next().value;
if (!firstKey) return;
cachedGetResponses.delete(firstKey);
}
};
const withNoopCancel = <T>(promise: Promise<T>): CustomClientPromise<T> => {
const nextPromise = promise as CustomClientPromise<T>;
nextPromise.cancel = () => { };
return nextPromise;
};
const API_PREFIX = "/api/v1.0";
const normalizeApiUrl = (url: string): string => {
if (/^https?:\/\//i.test(url)) return url;
if (url.startsWith(`${API_PREFIX}/`) || url.startsWith(`${API_PREFIX}?`)) return url;
return `${API_PREFIX}${url.startsWith("/") ? "" : "/"}${url}`;
};
const refreshData = data?.responseData ?? data?.data?.responseData ?? data;
const useCustomClient = <T>(
config: AxiosRequestConfig,
options?: AxiosRequestConfig,
): Promise<T> => {
const mergedConfig: AxiosRequestConfig = {
...config,
...options,
url: normalizeApiUrl(config.url as string),
};
const shouldCacheGet = shouldUseGetCache(mergedConfig);
const cacheKey = shouldCacheGet ? makeGetCacheKey(mergedConfig) : "";
const method = (mergedConfig.method || "get").toUpperCase();
if (shouldCacheGet) {
const cachedResponse = cachedGetResponses.get(cacheKey);
if (cachedResponse && cachedResponse.expiresAt > Date.now()) {
return withNoopCancel(Promise.resolve(cachedResponse.value as T));
if (!refreshData?.access_token || !refreshData?.refresh_token) {
throw new Error("Thiếu access token mới từ API.");
}
cachedGetResponses.delete(cacheKey);
const inFlightRequest = inFlightGetRequests.get(cacheKey);
useAuthStore.getState().setAuthSession({
accessToken: refreshData.access_token,
refreshToken: refreshData.refresh_token,
persistSession: useAuthStore.getState().appPersistSession,
});
if (inFlightRequest) {
return withNoopCancel(inFlightRequest as Promise<T>);
return refreshData.access_token;
} catch (err) {
return null;
} finally {
refreshPromise = null;
}
} else if (method !== "GET") {
clearGetCache();
})();
}
const source = Axios.CancelToken.source();
const axiosConfig: AxiosRequestConfig = {
...mergedConfig,
signal: shouldCacheGet ? undefined : (mergedConfig.signal || undefined),
cancelToken: shouldCacheGet ? undefined : source.token,
};
const nextAccessToken = await refreshPromise;
const promise = AXIOS_INSTANCE(axiosConfig)
.then(({ data, status }) => {
const response = data instanceof Blob ? data : { ...data, statusCode: status };
if (shouldCacheGet) {
cachedGetResponses.set(cacheKey, {
expiresAt: Date.now() + GET_CACHE_TTL,
value: response,
});
trimGetCache();
} else if (method !== "GET") {
clearGetCache();
if (!nextAccessToken) {
useAuthStore.getState().resetStore();
useUserStore.getState().clearUser();
if (typeof window !== "undefined") {
const currentPath = `${window.location.pathname}${window.location.search}`;
const redirect =
currentPath.startsWith("/admin") &&
currentPath !== "/admin/login"
? `?redirect=${encodeURIComponent(currentPath)}`
: "";
window.location.replace(`/admin/login${redirect}`);
}
return response;
})
.finally(() => {
if (shouldCacheGet) {
inFlightGetRequests.delete(cacheKey);
return Promise.reject(error);
}
}) as CustomClientPromise<T>;
if (shouldCacheGet) {
inFlightGetRequests.set(cacheKey, promise as CustomClientPromise<unknown>);
const headers = AxiosHeaders.from(originalRequest.headers);
headers.set("Authorization", `Bearer ${nextAccessToken}`);
originalRequest.headers = headers;
return instance(originalRequest);
} catch (refreshError) {
return Promise.reject(refreshError);
}
},
);
promise.cancel = () => {
source.cancel("Query was cancelled");
};
return promise;
export async function useCustomClient<T>(
config: AxiosRequestConfig,
options?: AxiosRequestConfig,
): Promise<T> {
const response = await instance({ ...config, ...options });
return response.data as T;
};
export { useCustomClient };
export type ErrorType<Error> = AxiosError<Error>;
export type BodyType<BodyData> = BodyData;
/** Loại quảng cáo: vuông (sidebar) hoặc ngang (banner full-width) */
export type AdvertisementType = "square" | "horizontal";
/** Chế độ hiển thị */
export type AdvertisementStatus = "active" | "inactive";
export interface AdvertisementItem {
id: string;
/** Tên quảng cáo */
name: string;
/** Đường dẫn ảnh (có thể là .gif) */
image: string;
/** Alt text cho ảnh */
alt: string;
/** Đường link khi bấm vào */
link: string;
/** Loại quảng cáo: vuông | ngang */
type: AdvertisementType;
/** Chế độ hiển thị */
status: AdvertisementStatus;
/** Thứ tự hiển thị (số nhỏ hiện trước) */
sortOrder: number;
/** Ngày tạo (ISO string) */
createdAt: string;
/** Tạo bởi ai */
createdBy: string;
/** Ngày sửa (ISO string) */
updatedAt: string;
/** Sửa bởi ai */
updatedBy: string;
}
export interface AdvertisementFormValues {
id?: string;
name: string;
image: string;
alt: string;
link: string;
type: AdvertisementType;
status: AdvertisementStatus;
sortOrder: number;
}
export interface BaseConfigLogoItem {
id: string;
name: string;
imageId: string;
isActive: boolean;
}
export interface BaseConfigBannerItem {
id: string;
name: string;
imageId: string;
isActive: boolean;
displayTimeSeconds: number;
sortOrder: number;
}
export interface BaseConfigBranchItem {
id: string;
branchName: string;
address: string;
hotline: string;
email: string;
fax: string;
mapsEmbedUrl: string;
sortOrder: number;
isVisible: boolean;
}
export interface BaseConfigSocialItem {
id: string;
label: string;
url: string;
isVisible: boolean;
sortOrder: number;
}
export interface BaseConfigData {
logo: BaseConfigLogoItem | null;
banners: BaseConfigBannerItem[];
websiteName: string;
websiteLink: string;
socials: BaseConfigSocialItem[];
branches: BaseConfigBranchItem[];
}
export interface CategoryAdminItem {
id: string
name: string
description: string
created_at: string
created_by: string | null
updated_at: string
updated_by: string | null
}
export interface CategoryAdminResponseData {
count: number
rows: CategoryAdminItem[]
totalPages: number
currentPage: number
}
export interface GetCategoryAdminResponseType {
message: string
message_en: string
responseData: CategoryAdminResponseData
status: 'success' | 'error'
timeStamp: string
violations: any | null
}
export type ContactPurpose =
| "Hội viên VCCI"
| "Xuất xứ hàng hóa C/O"
| "Xúc tiến thương mại"
| "Quảng cáo"
| "Mục đích khác";
export interface NewsletterSubscriptionItem {
id: string;
email: string;
submittedAt: string;
}
export interface ContactRequestItem {
id: string;
purpose: ContactPurpose;
contactName: string;
contactPosition: string;
contactEmail: string;
contactPhone: string;
message: string;
organizationName: string;
businessField: string;
email: string;
website: string;
submittedAt: string;
}
export interface MembershipApplicationItem {
id: string;
organizationName: string;
membershipType: string;
contactName: string;
contactPosition: string;
contactEmail: string;
contactPhone: string;
address: string;
businessField: string;
website: string;
note: string;
submittedAt: string;
}
import { ResponseType } from '@lib/types/common'
export type EventStatus = {
id: string;
name: string;
name_en: string;
code: string;
};
export type EventOrganization = {
id: string;
organization_id: string | null;
event_id: string;
status: string | null;
created_at: string;
updated_at: string | null;
created_by: string;
updated_by: string | null;
role: string;
guest_name: string;
guest_image: string;
org_table_count: number | null;
org_counter_count: number | null;
add_info: unknown | null;
organization: unknown | null;
};
// useGetEventsId
export type GetEventsIdQueryResponseType = ResponseType<{
accept_entries: boolean | null
counter_cost: number
counter_count: number
created_at: string
description: string
end_time: string
event_organizations: Array<{
add_info: string | null
created_at: string
guest_image: string | null
guest_name: string | null
id: string
org_counter_count: number | null
org_table_count: number | null
organization: {
address: string
avatar: string | null
club_link: string | null
club_name: string | null
id: string
name: string
org_categories: string[]
org_link: string | null
org_status_id: string | null
organization_products: Array<{
id: string
images: string[]
}>
province: string[]
tax_code: string
users: Array<{
id: string
}>
website: string
} | null
role: 'PARTAKER' | 'MAIN' | 'SUPPORT' | 'SUPPORT_1' | 'SUPPORT_2' | 'SUPPORT_3' | 'GUEST'
status: string | null
}>
host_club: string | null
id: string
image: string
introduction: string | null
location: string
name: string
org_support_titles: string[] | null
province: string
seo_text: string
seo_text_en: string | null
start_time: string
status: string
status_status: {
code: string
id: string
name: string
name_en: string
}
table_cost: number
table_count: number
updated_at: string | null
}>
export type EventItem = {
id: string;
name: string;
start_time: string;
end_time: string;
created_at: string;
created_by: string;
updated_at: string | null;
updated_by: string | null;
status: string;
image: string;
description: string;
location: string;
province: string;
table_count: number;
counter_count: number;
seo_text: string;
seo_text_en: string | null;
table_cost: number;
counter_cost: number;
table_min_pick: number | null;
counter_min_pick: number | null;
table_max_pick: number | null;
counter_max_pick: number | null;
org_support_titles: string[];
accept_entries: boolean;
type: string;
introduction: string;
host_club: string | null;
config: unknown | null;
event_organizations: EventOrganization[];
status_status: EventStatus;
};
export type EventResponseData = {
count: number;
rows: EventItem[];
totalPages: number;
currentPage: number;
};
export type EventApiResponse = {
message: string;
message_en: string;
responseData: EventResponseData;
status: string;
timeStamp: string;
violations: null | unknown;
};
export interface HeaderCategoryPostItem {
id: string;
category_id: string;
title: string;
slug: string;
excerpt: string;
content: string;
thumbnail: string;
published_at: string;
is_active: boolean;
created_at: string;
updated_at: string;
}
export interface HeaderCategoryPostFormValues {
id?: string;
title: string;
slug: string;
excerpt: string;
content: string;
thumbnail: string;
published_at: string;
is_active: boolean;
}
export type HeaderCategoryType = "category" | "page" | "news";
export interface HeaderCategoryItem {
id: string;
name: string;
slug: string;
url?: string | null;
sort_order: number;
type: HeaderCategoryType;
is_article?: boolean;
parent_id: string | null;
level: number;
category_ids?: string[];
tagsearch_values?: string[];
description?: string;
created_at?: string;
updated_at?: string;
}
export interface HeaderCategoryTreeItem extends HeaderCategoryItem {
children: HeaderCategoryTreeItem[];
}
export interface HeaderArticleCategoryOption {
id: string;
name: string;
}
import type { AdminNewsContentSection } from "./post";
export interface MemberField {
id: string;
name: string;
}
export interface MemberRegion {
id: string;
name: string;
}
export interface MemberImageRef {
id: string;
name: string;
alt: string;
url: string;
}
export interface MemberItem {
id: string;
name: string;
is_featured: boolean;
image: MemberImageRef | null;
region_id: string;
field_id: string;
address: string;
phone: string;
fax: string;
email: string;
website: string;
introduction: AdminNewsContentSection[];
created_at: string;
updated_at: string;
}
export interface MemberFormValues {
id?: string;
name: string;
is_featured: boolean;
image: MemberImageRef | null;
region_id: string;
field_id: string;
address: string;
phone: string;
fax: string;
email: string;
website: string;
introduction: AdminNewsContentSection[];
}
export interface NewsPageConfigItem {
id: string,
name: string,
code: string,
static_link: string,
is_article: boolean,
level: number,
sort_order: number,
children: Array<NewsPageConfigItem>,
};
export interface GetNewsPageConfigResponseType {
message: string,
message_en: string,
responseData: NewsPageConfigItem,
status: string,
timeStamp: string,
violations: "null | Object",
};
\ No newline at end of file
export interface NewsItem {
id: string
title: string
thumbnail: string
description: string
release_at: string
is_active: boolean
created_at: string
created_by: string | null
updated_at: string
updated_by: string | null
mode: 'NOW' | string
category: string
page_config: {
id: string
name: string
static_link: string
static_link_en: string
code: string
}
}
export interface NewsResponseData {
count: number
rows: NewsItem[]
totalPages: number
currentPage: number
}
export interface GetNewsResponseType {
message: string
message_en: string
responseData: NewsResponseData
status: 'success' | 'error'
timeStamp: string
violations: string | null
}
export interface PostHistoryUser {
id: string
email: string
username: string | null
first_name: string | null
last_name: string | null
full_name: string
avatar_url: string | null
}
import type { UserSummary } from "./user";
export type PostHistoryUser = UserSummary;
export type PostHistoryAction = "CREATE" | "UPDATE" | "DELETE"
......
import type { UserSummary } from "./user";
export const ADMIN_NEWS_TYPE_OPTIONS = [
{ value: "tintuc", label: "Tin tức" },
{ value: "baiviettrang", label: "Bài viết trang" },
] as const;
export type AdminNewsType = (typeof ADMIN_NEWS_TYPE_OPTIONS)[number]["value"];
export const ADMIN_NEWS_TYPE_LABELS: Record<AdminNewsType, string> =
ADMIN_NEWS_TYPE_OPTIONS.reduce(
(result, option) => {
result[option.value] = option.label;
return result;
},
{} as Record<AdminNewsType, string>,
);
export interface AdminMediaItem {
id: string;
name: string;
alt: string;
url: string;
mime: string;
size: number;
created_at: string;
updated_at: string;
source: "seed" | "upload";
}
export interface AdminNewsImageRef {
id: string;
name: string;
alt: string;
url: string;
}
export interface AdminNewsContentImage {
position: number;
image: AdminNewsImageRef;
caption: string;
}
export interface AdminNewsContentSection {
id: string;
type: "text" | "image";
position: number;
content: string;
image_columns: number;
image_rows: number;
images: AdminNewsContentImage[];
}
export interface AdminNewsItem {
id: string;
title: string;
slug: string;
summary: string;
type: AdminNewsType;
header_category_id: string;
category_ids: string[];
tagsearch_values: string[];
is_featured: boolean;
thumbnail: AdminNewsImageRef | null;
is_hidden: boolean;
created_at: string;
updated_at: string;
published_at: string;
expired_at: string;
started_at: string;
ended_at: string;
registration_deadline: string;
location: string;
participation_fee: string;
event_dates?: string[];
post_content: AdminNewsContentSection[];
creator?: UserSummary | null;
editor?: UserSummary | null;
}
export interface AdminNewsFormValues {
title: string;
slug: string;
summary: string;
type: AdminNewsType | "";
header_category_id: string;
category_ids: string[];
tagsearch_values: string[];
is_featured: boolean;
thumbnail: AdminNewsImageRef | null;
is_hidden: boolean;
created_at: string;
updated_at: string;
published_at: string;
expired_at: string;
started_at: string;
ended_at: string;
registration_deadline: string;
location: string;
participation_fee: string;
event_dates?: string[];
post_content: AdminNewsContentSection[];
}
export interface TagItem {
id: string
name: string
slug: string
created_at?: string
updated_at?: string
}
export interface UserSummary {
id: string
email: string
username: string | null
first_name: string | null
last_name: string | null
full_name: string
avatar_url: string | null
}
export interface RawUser {
id?: string | null
email?: string | null
username?: string | null
first_name?: string | null
last_name?: string | null
full_name?: string | null
avatar_url?: string | null
}
export interface VideoItem {
id: string;
name: string;
url: string;
}
export interface VideoFormValues {
id?: string;
name: string;
url: string;
}
......@@ -6,8 +6,9 @@ import {
stripHtml,
} from "./templates/data";
import { getApiV10Post, getApiV10PostId } from "@/api/vcci-news/endpoints/post";
import { getApiV10Category } from "@/api/vcci-news/endpoints/category";
import { categoryFallbackRows } from "@/mockdata/categories";
import type { DynamicPostItem } from "./templates/types";
import { fetchCmsCategories } from "@/lib/api/cms-admin";
import DynamicPageClient from "./DynamicPageClient";
const STATIC_PAGE_SLUGS = new Set([
......@@ -186,8 +187,22 @@ export async function generateMetadata({
let categoryTitle = "";
if (!postId) {
try {
const categories = await fetchCmsCategories();
const matched = categories.find((item) => item.url === path || `/${item.slug}` === path);
const response = await getApiV10Category({
page: 1,
pageSize: 200,
sortField: "sort_order",
sortOrder: "asc",
}).catch(() => ({
responseData: {
count: categoryFallbackRows.length,
page: 1,
pageSize: 50,
rows: categoryFallbackRows as any[],
},
}));
const result = (response.responseData ?? {}) as { rows?: any[] };
const categories = (result.rows ?? []).filter((item: any) => item.type !== "category");
const matched = categories.find((item: any) => item.url === path || `/${item.slug}` === path);
if (matched) categoryTitle = matched.name;
} catch {
// ignore
......
"use client";
import React from "react";
import React, { useEffect, useState } from "react";
import Link from "next/link";
import ListCategory from "@/components/base/list-category";
import { buildDynamicCategoryMenu } from "../templates/data";
import type { DynamicCategoryRouteItem } from "../templates/types";
import { useGetApiV10PageConfig } from "@/api/vcci-news/endpoints/page-config";
import { GetNewsPageConfigResponseType } from "@/api/vcci-news/types/news-page-config";
import { getApiV10CategoryTree } from "@/api/vcci-news/endpoints/category";
import type { HeaderCategoryTreeItem } from "@/api/vcci-news/types/header-config";
type SiteMapProps = {
category: DynamicCategoryRouteItem | null;
......@@ -13,7 +13,22 @@ type SiteMapProps = {
};
export default function SiteMap({ category, allCategories }: SiteMapProps) {
const { data: categoriesData, isLoading, isError } = useGetApiV10PageConfig<GetNewsPageConfigResponseType>();
const [categoriesData, setCategoriesData] = useState<{ responseData?: HeaderCategoryTreeItem[] } | null>(null);
const [isLoading, setIsLoading] = useState(true);
const [isError, setIsError] = useState(false);
useEffect(() => {
void getApiV10CategoryTree()
.then((response) => {
setCategoriesData(response as { responseData?: HeaderCategoryTreeItem[] });
setIsLoading(false);
})
.catch(() => {
setIsError(true);
setIsLoading(false);
});
}, []);
const categoryMenu = category ? buildDynamicCategoryMenu(category, allCategories) : [];
if (isLoading) {
......@@ -38,7 +53,7 @@ export default function SiteMap({ category, allCategories }: SiteMapProps) {
);
}
const sections = categoriesData.responseData.children || [];
const sections = categoriesData?.responseData ?? [];
return (
<>
......@@ -78,7 +93,7 @@ export default function SiteMap({ category, allCategories }: SiteMapProps) {
{/* Section Box */}
<div className="relative z-20">
<Link
href={section.static_link || "#"}
href={section.url || "#"}
className="flex bg-[#063e8e] text-white px-4 py-3 rounded-md font-medium text-center hover:bg-[#0a4fb5] transition shadow-md w-full text-sm min-h-20 items-center justify-center"
>
<span className="leading-tight">{section.name.toUpperCase()}</span>
......@@ -108,7 +123,7 @@ export default function SiteMap({ category, allCategories }: SiteMapProps) {
<div className="absolute right-1/2 top-1/2 -translate-y-1/2 w-1/2 h-0.5 bg-gray-600"></div>
<Link
href={child.static_link || "#"}
href={child.url || "#"}
className="block bg-gray-400 text-white px-3 py-2.5 rounded text-xs font-medium text-center hover:bg-gray-500 transition shadow-sm leading-tight relative z-10"
>
{child.name.toUpperCase()}
......
......@@ -127,7 +127,7 @@ export default function NewsDetailPage({
const primaryCategory =
findDisplayCategoryForPost(post, category, allCategories)?.name ||
category?.name ||
"Tin tức";
"Tin tc";
const categoryMenu = category ? buildDynamicCategoryMenu(category, allCategories) : [];
return (
......
......@@ -2,7 +2,7 @@
import { Trash2 } from "lucide-react";
import { Button } from "@/components/ui/button";
import type { BaseConfigBranchItem } from "@/mockdata/base-config";
import type { BaseConfigBranchItem } from "@/api/vcci-news/types/base-config";
export function BranchCard({
branch,
......
"use client";
import type { BaseConfigBannerItem } from "@/mockdata/base-config";
import type { BaseConfigBannerItem } from "@/api/vcci-news/types/base-config";
import type { AdminMediaItem } from "@/mockdata/admin-news";
import { SafeImage } from "@/components/shared/safe-image";
......
......@@ -8,12 +8,13 @@ import { Button } from "@/components/ui/button";
import { Input } from "@/components/ui/input";
import { Label } from "@/components/ui/label";
import { usePutApiV10UserChangePassword } from "@/api/vcci-news/endpoints/user";
import { usePostApiV10AuthLogout } from "@/api/vcci-news/endpoints/authentication";
import useAuthStore from "@/store/useAuthStore";
import { logoutAdmin } from "@/lib/auth/admin-auth";
import useUserStore from "@/store/useUserStore";
export default function ChangePasswordPage() {
const router = useRouter();
const appUser = useAuthStore((state) => state.appUser);
const appUser = useUserStore((state) => state.appUser);
const [oldPassword, setOldPassword] = useState("");
const [newPassword, setNewPassword] = useState("");
const [confirmPassword, setConfirmPassword] = useState("");
......@@ -23,6 +24,13 @@ export default function ChangePasswordPage() {
const [done, setDone] = useState(false);
const changePasswordMutation = usePutApiV10UserChangePassword();
const { mutateAsync: logoutAsync } = usePostApiV10AuthLogout({
mutation: {
onError: (error) => {
console.error('Logout error:', error);
},
},
});
const validate = (): string | null => {
if (!oldPassword.trim()) return "Vui lòng nhập mật khẩu cũ";
......@@ -51,14 +59,22 @@ export default function ChangePasswordPage() {
toast.success("Đổi mật khẩu thành công!");
// Update store để bỏ must_change_password
useAuthStore.getState().setAppUser({
useUserStore.getState().setAppUser({
...appUser,
must_change_password: false,
} as typeof appUser);
// Sau 2s, logout để user đăng nhập lại bằng mật khẩu mới
setTimeout(async () => {
await logoutAdmin({ silent: true, redirectToLogin: true });
try {
await logoutAsync();
} catch {
// Ignore logout API failure and continue clearing local auth state.
} finally {
useAuthStore.getState().resetStore();
useUserStore.getState().clearUser();
router.push('/admin/login');
}
}, 2000);
} catch (err: unknown) {
const e = err as { message?: string };
......
import dayjs from "dayjs";
import { buildHeaderCategoryTree } from "@/mockdata/header-config";
import type { HeaderCategoryTreeItem } from "@/api/vcci-news/types/header-config";
export const PAGE_SIZE = 10;
export type HeaderCategoryTreeNode = ReturnType<typeof buildHeaderCategoryTree>;
export function formatDateTime(value: string) {
return value ? dayjs(value).format("DD/MM/YYYY HH:mm") : "—";
}
export function flattenTree(items: HeaderCategoryTreeNode) {
const rows: HeaderCategoryTreeNode = [];
export function flattenTree(items: HeaderCategoryTreeItem[]) {
const rows: HeaderCategoryTreeItem[] = [];
const walk = (nodes: HeaderCategoryTreeNode) => {
const walk = (nodes: HeaderCategoryTreeItem[]) => {
nodes.forEach((item) => {
rows.push(item);
if (item.children.length > 0) {
......
......@@ -26,15 +26,15 @@ import {
TableHeader,
TableRow,
} from "@/components/ui/table";
import {
deleteCmsNewsItem,
fetchCmsNewsItems,
fetchHeaderConfigItems,
type CmsHeaderCategoryItem,
type CmsNewsItem,
} from "@/lib/api/cms-admin";
import { ADMIN_NEWS_TYPE_LABELS } from "@/mockdata/admin-news";
import { buildHeaderCategoryTree } from "@/mockdata/header-config";
import { getApiV10Post } from "@/api/vcci-news/endpoints/post";
import { deleteApiV10PostId } from "@/api/vcci-news/endpoints/post";
import { getApiV10CategoryTree } from "@/api/vcci-news/endpoints/category";
import { ADMIN_NEWS_TYPE_LABELS, type AdminNewsItem } from "@/mockdata/admin-news";
import { normalizeUser } from "@/lib/utils/cms-user";
import { normalizeDateTimeInput } from "@/lib/utils/datetime";
import { parsePostContent, parseLegacyPostContent } from "@/lib/utils/post-content";
import links from "@/links";
import type { HeaderCategoryTreeItem } from "@/api/vcci-news/types/header-config";
import { HeaderCategoryPostsLoading } from "./_components/HeaderCategoryPostsLoading";
import {
PAGE_SIZE,
......@@ -48,11 +48,11 @@ export default function HeaderCategoryPostsPage() {
const pathname = usePathname();
const searchParams = useSearchParams();
const categoryId = String(params.categoryId ?? "");
const [items, setItems] = useState<CmsNewsItem[]>([]);
const [headerItems, setHeaderItems] = useState<CmsHeaderCategoryItem[]>([]);
const [items, setItems] = useState<AdminNewsItem[]>([]);
const [headerTree, setHeaderTree] = useState<HeaderCategoryTreeItem[]>([]);
const [search, setSearch] = useState(() => searchParams.get("q") ?? "");
const [ready, setReady] = useState(false);
const [deleteTarget, setDeleteTarget] = useState<CmsNewsItem | null>(null);
const [deleteTarget, setDeleteTarget] = useState<AdminNewsItem | null>(null);
const didMountRef = useRef(false);
const [total, setTotal] = useState(0);
const [page, setPage] = useState(() => {
......@@ -90,22 +90,61 @@ export default function HeaderCategoryPostsPage() {
keyword ? `title@=${keyword}|slug@=${keyword}` : "",
].filter(Boolean).join(",");
const [newsData, headerConfig] = await Promise.all([
fetchCmsNewsItems({
const [newsResponse, treeResponse] = await Promise.all([
getApiV10Post({
page,
pageSize: PAGE_SIZE,
sortField: "created_at",
sortOrder: "desc",
filters,
filters: filters.trim() || undefined,
}),
fetchHeaderConfigItems(),
getApiV10CategoryTree(),
]);
if (cancelled) return;
setItems(newsData.items);
setTotal(newsData.total);
setHeaderItems(headerConfig.items);
const newsResult = (newsResponse.responseData ?? {}) as { rows?: any[]; count?: number };
const tree = (treeResponse.responseData ?? []) as unknown as HeaderCategoryTreeItem[];
setItems((newsResult.rows ?? []).map((item: any): AdminNewsItem => {
const structuredContent = parsePostContent(item.content_structure);
const postContent = structuredContent.length > 0 ? structuredContent : parseLegacyPostContent(item.content);
const categories = Array.isArray(item.categories) ? item.categories : [];
const primaryCategory = categories[0] ?? null;
const primaryCategoryType = primaryCategory?.type ?? null;
return {
id: item.id ?? "",
title: item.title ?? "",
slug: item.slug ?? "",
summary: item.summary ?? "",
type: item.type === "page" || primaryCategoryType === "post" || primaryCategoryType === "page" ? "baiviettrang" : "tintuc",
header_category_id: primaryCategory?.id ?? "",
category_ids: categories.map((c: any) => c.id),
tagsearch_values: [],
is_featured: Boolean(item.is_featured),
thumbnail: item.thumbnail?.id ? {
id: item.thumbnail.id,
name: item.thumbnail.original ?? item.thumbnail.path ?? "thumbnail",
alt: item.thumbnail.original ?? item.thumbnail.path ?? "thumbnail",
url: links.resolveImageUrl(item.thumbnail.path),
} : null,
is_hidden: Boolean(item.is_hidden),
created_at: item.created_at ?? "",
updated_at: item.updated_at ?? "",
published_at: normalizeDateTimeInput(item.published_at ?? item.release_at),
expired_at: normalizeDateTimeInput(item.expired_at),
started_at: normalizeDateTimeInput(item.started_at),
ended_at: normalizeDateTimeInput(item.ended_at),
registration_deadline: normalizeDateTimeInput(item.registration_deadline),
location: item.location ?? "",
participation_fee: item.participation_fee ?? "",
event_dates: Array.isArray(item.event_dates) ? item.event_dates.filter((d: any): d is string => typeof d === "string") : [],
post_content: postContent,
creator: normalizeUser(item.creator),
editor: normalizeUser(item.editor),
};
}));
setTotal(newsResult.count ?? 0);
setHeaderTree(tree);
setReady(true);
} catch (error) {
if (cancelled) return;
......@@ -122,8 +161,8 @@ export default function HeaderCategoryPostsPage() {
}, [categoryId, page, search]);
const flatCategories = useMemo(() => {
return flattenTree(buildHeaderCategoryTree(headerItems));
}, [headerItems]);
return flattenTree(headerTree);
}, [headerTree]);
const category = useMemo(
() => flatCategories.find((item) => item.id === categoryId) ?? null,
......@@ -193,7 +232,7 @@ export default function HeaderCategoryPostsPage() {
if (!deleteTarget) return;
try {
await deleteCmsNewsItem(deleteTarget.id);
await deleteApiV10PostId(deleteTarget.id);
setItems((current) => current.filter((item) => item.id !== deleteTarget.id));
setTotal((current) => Math.max(0, current - 1));
toast.success("Đã xóa bài viết");
......
......@@ -2,7 +2,7 @@
import * as React from "react";
import { AdminDeleteDialog } from "@/components/admin/admin-delete-dialog";
import { HeaderCategoryTreeItem } from "@/mockdata/header-config";
import type { HeaderCategoryTreeItem } from "@/api/vcci-news/types/header-config";
interface HeaderCategoryDeleteDialogProps {
target: HeaderCategoryTreeItem | null;
......
"use client";
import * as React from "react";
import Link from "next/link";
import {
ChevronDown,
ChevronRight,
......@@ -207,9 +205,9 @@ export function HeaderCategoryTable({
<TableCell className="w-[280px] text-sm text-gray-700">
<div className="mx-auto flex max-w-[220px] items-center justify-center gap-2">
<span className="block max-w-[180px] truncate">
{item.static_link || "-"}
{item.url || "-"}
</span>
{item.static_link ? (
{item.url ? (
<ExternalLink className="h-3.5 w-3.5 shrink-0 text-[#063e8e]" />
) : null}
</div>
......
"use client";
import React from "react";
import React, { useCallback, useEffect, useMemo, useState } from "react";
import { toast } from "sonner";
import {
HeaderCategoryDeleteDialog,
......@@ -11,15 +11,14 @@ import {
HeaderCategoryStats,
HeaderCategoryTable,
} from "./components";
import { buildStaticLink } from "@/lib/utils/header-link";
import {
CmsHeaderCategoryItem,
createHeaderConfigItem,
deleteHeaderConfigItem,
fetchHeaderConfigItems,
updateHeaderConfigItem,
} from "@/lib/api/cms-admin";
deleteApiV10CategoryId,
getApiV10CategoryTree,
postApiV10Category,
putApiV10CategoryId,
} from "@/api/vcci-news/endpoints/category";
import {
buildHeaderCategoryTree,
HeaderCategoryItem,
HeaderCategoryTreeItem,
toSlug,
......@@ -40,7 +39,7 @@ function isProtectedHomeCategory(itemId?: string | null) {
return itemId === PROTECTED_HOME_CATEGORY_ID;
}
function toFormValues(item?: CmsHeaderCategoryItem | null): HeaderCategoryFormValues {
function toFormValues(item?: HeaderCategoryItem | null): HeaderCategoryFormValues {
if (!item) return EMPTY_HEADER_CATEGORY_FORM;
return {
......@@ -54,35 +53,28 @@ function toFormValues(item?: CmsHeaderCategoryItem | null): HeaderCategoryFormVa
};
}
type ManagedHeaderCategoryItem = HeaderCategoryItem & {
code: string;
api_parent_id: string | null;
};
function useHeaderConfigModule() {
const [items, setItems] = React.useState<ManagedHeaderCategoryItem[]>([]);
const [rootStaticLink, setRootStaticLink] = React.useState("/");
const [isReady, setIsReady] = React.useState(false);
const [tree, setTree] = useState<HeaderCategoryTreeItem[]>([]);
const [rootStaticLink, setRootStaticLink] = useState("/");
const [isReady, setIsReady] = useState(false);
const load = React.useCallback(async () => {
const headerConfig = await fetchHeaderConfigItems();
const load = useCallback(async () => {
const response = await getApiV10CategoryTree();
const treeData = (response.responseData ?? []) as unknown as HeaderCategoryTreeItem[];
setItems(headerConfig.items as ManagedHeaderCategoryItem[]);
setRootStaticLink(headerConfig.rootStaticLink);
setTree(treeData);
setRootStaticLink("/");
setIsReady(true);
}, []);
React.useEffect(() => {
useEffect(() => {
void load().catch((error) => {
toast.error(error instanceof Error ? error.message : "Không thể tải cấu hình danh mục");
setIsReady(true);
});
}, [load]);
const tree = React.useMemo(() => buildHeaderCategoryTree(items), [items]);
return {
items,
tree,
rootStaticLink,
isReady,
......@@ -91,19 +83,19 @@ function useHeaderConfigModule() {
}
export default function HeaderConfigPage() {
const { items, tree, rootStaticLink, isReady, reload } = useHeaderConfigModule();
const { tree, rootStaticLink, isReady, reload } = useHeaderConfigModule();
const [expanded, setExpanded] = React.useState<Record<string, boolean>>({});
const [search, setSearch] = React.useState("");
const [formMode, setFormMode] = React.useState<HeaderCategoryFormMode>("create");
const [formOpen, setFormOpen] = React.useState(false);
const [formValues, setFormValues] = React.useState<HeaderCategoryFormValues>(
const [expanded, setExpanded] = useState<Record<string, boolean>>({});
const [search, setSearch] = useState("");
const [formMode, setFormMode] = useState<HeaderCategoryFormMode>("create");
const [formOpen, setFormOpen] = useState(false);
const [formValues, setFormValues] = useState<HeaderCategoryFormValues>(
EMPTY_HEADER_CATEGORY_FORM,
);
const [deleteTarget, setDeleteTarget] = React.useState<HeaderCategoryTreeItem | null>(null);
const [isSubmitting, setIsSubmitting] = React.useState(false);
const [deleteTarget, setDeleteTarget] = useState<HeaderCategoryTreeItem | null>(null);
const [isSubmitting, setIsSubmitting] = useState(false);
React.useEffect(() => {
useEffect(() => {
if (!isReady) return;
setExpanded((previous) => {
......@@ -126,7 +118,7 @@ export default function HeaderConfigPage() {
});
}, [isReady, tree]);
const flatRows = React.useMemo(() => {
const flatRows = useMemo(() => {
const rows: HeaderCategoryFlatRow[] = [];
const walk = (nodes: HeaderCategoryTreeItem[], depth = 0) => {
......@@ -142,12 +134,12 @@ export default function HeaderConfigPage() {
return rows;
}, [tree]);
const itemMap = React.useMemo(
() => new Map(items.map((item) => [item.id, item])),
[items],
const itemMap = useMemo(
() => new Map(flatRows.map((item) => [item.id, item])),
[flatRows],
);
const visibleRows = React.useMemo(() => {
const visibleRows = useMemo(() => {
return flatRows.filter((row) => {
const keyword = search.trim().toLowerCase();
const matchesSearch =
......@@ -169,17 +161,17 @@ export default function HeaderConfigPage() {
});
}, [expanded, flatRows, search]);
const categoryParentOptions = React.useMemo(
const categoryParentOptions = useMemo(
() => tree.filter((item) => item.type === "category"),
[tree],
);
const editingItem = React.useMemo(
const editingItem = useMemo(
() => flatRows.find((item) => item.id === formValues.id) ?? null,
[flatRows, formValues.id],
);
const canChangeParent = React.useMemo(() => {
const canChangeParent = useMemo(() => {
if (!editingItem) return true;
return editingItem.children.length === 0 || !formValues.parent_id;
}, [editingItem, formValues.parent_id]);
......@@ -224,7 +216,7 @@ export default function HeaderConfigPage() {
const parent = itemMap.get(parentId);
return {
apiParentId: parent?.id ?? "",
parentStaticLink: parent?.static_link ?? rootStaticLink,
parentStaticLink: parent?.url ?? rootStaticLink,
};
};
......@@ -278,17 +270,17 @@ export default function HeaderConfigPage() {
const payload = {
name: formValues.name.trim(),
slug: formValues.slug.trim() || toSlug(formValues.name),
url: buildStaticLink(formValues.slug.trim(), parentContext.parentStaticLink),
sort_order: Number(formValues.sort_order) || 1,
type: formValues.type,
api_parent_id: parentContext.apiParentId,
parent_static_link: parentContext.parentStaticLink,
parent_id: parentContext.apiParentId || undefined,
type: formValues.type === "category" ? "category" : formValues.type === "news" ? "news" : "page",
};
if (formMode === "create") {
await createHeaderConfigItem(payload);
await postApiV10Category(payload);
toast.success("Tạo danh mục thành công");
} else if (formValues.id) {
await updateHeaderConfigItem(formValues.id, payload);
await putApiV10CategoryId(formValues.id, payload);
toast.success("Cập nhật danh mục thành công");
}
......@@ -313,7 +305,7 @@ export default function HeaderConfigPage() {
setIsSubmitting(true);
try {
await deleteHeaderConfigItem(deleteTarget.id);
await deleteApiV10CategoryId(deleteTarget.id);
toast.success("Xóa danh mục thành công");
setDeleteTarget(null);
await reload();
......
......@@ -63,9 +63,5 @@ export default function AdminLayout({ children }: { children: React.ReactNode })
return <AdminAuthLoadingScreen />;
}
if (authStatus === 'blocked') {
return null;
}
return <AdminShell>{children}</AdminShell>;
}
......@@ -11,13 +11,17 @@ import {
Mail,
} from "lucide-react";
import { toast } from "sonner";
import { usePostApiV10AuthForgotPasswordRequest } from "@/api/vcci-news/endpoints/authentication";
import {
useGetApiV10AuthMe,
usePostApiV10AuthForgotPasswordRequest,
usePostApiV10AuthLogin,
} from "@/api/vcci-news/endpoints/authentication";
import { Button } from "@/components/ui/button";
import { Checkbox } from "@/components/ui/checkbox";
import { Input } from "@/components/ui/input";
import { Label } from "@/components/ui/label";
import { loginAdmin } from "@/lib/auth/admin-auth";
import useAuthStore from "@/store/useAuthStore";
import useUserStore, { type AuthenticatedAdminUser } from "@/store/useUserStore";
import { AuthShell, type AuthMode } from "./_components/auth-shell";
type ErrorResponse = {
......@@ -65,6 +69,30 @@ function getAuthErrorMessage(error: unknown, fallback: string) {
);
}
function normalizeUser(
user: any,
mustChangePassword: boolean,
): AuthenticatedAdminUser | null {
if (!user?.id || !user.email || !user.username) return null;
return {
id: user.id,
email: user.email,
username: user.username,
first_name: user.first_name ?? null,
last_name: user.last_name ?? null,
roles: Array.isArray(user.roles)
? user.roles.filter((v: unknown): v is string => typeof v === "string")
: [],
permissions: Array.isArray(user.permissions)
? user.permissions.filter((v: unknown): v is string => typeof v === "string")
: [],
status: user.status ?? null,
last_login_at: user.last_login_at ?? null,
must_change_password: mustChangePassword,
};
}
function InlineMessage({ type, message }: {
type: "error" | "success";
message: string;
......@@ -98,6 +126,8 @@ function AdminLoginPageContent() {
const hasHydrated = useAuthStore((state) => state._hasHydrated);
const isLoggedIn = useAuthStore((state) => state.appIsLoggedIn);
const rememberState = useAuthStore((state) => state.appUserRemember);
const setAuthSession = useAuthStore((state) => state.setAuthSession);
const setAppUser = useUserStore((state) => state.setAppUser);
const setAppUserRemember = useAuthStore((state) => state.setAppUserRemember);
const [mode, setMode] = useState<AuthMode>("login");
......@@ -105,7 +135,6 @@ function AdminLoginPageContent() {
const [password, setPassword] = useState("");
const [passwordVisible, setPasswordVisible] = useState(false);
const [remember, setRemember] = useState(false);
const [loginLoading, setLoginLoading] = useState(false);
const [loginError, setLoginError] = useState<string | null>(null);
const [forgotNote, setForgotNote] = useState("");
......@@ -113,6 +142,10 @@ function AdminLoginPageContent() {
const [forgotError, setForgotError] = useState<string | null>(null);
const [forgotMessage, setForgotMessage] = useState<string | null>(null);
const { refetch: getProfile } = useGetApiV10AuthMe({
query: { enabled: false },
});
useEffect(() => {
if (!rememberState?.remember) return;
......@@ -124,7 +157,7 @@ function AdminLoginPageContent() {
useEffect(() => {
if (!hasHydrated || !isLoggedIn) return;
const currentUser = useAuthStore.getState().appUser;
const currentUser = useUserStore.getState().appUser;
if (currentUser?.must_change_password) {
router.replace("/admin/change-password");
} else {
......@@ -132,33 +165,75 @@ function AdminLoginPageContent() {
}
}, [hasHydrated, isLoggedIn, redirect, router]);
const handleLogin = async (event: FormEvent<HTMLFormElement>) => {
event.preventDefault();
setLoginError(null);
setLoginLoading(true);
const { mutate: login, isPending: loginLoading } = usePostApiV10AuthLogin({
mutation: {
onSuccess: async (res: any) => {
const loginData = res?.responseData ?? res?.data?.responseData ?? res?.data;
if (!loginData?.access_token || !loginData?.refresh_token) {
setLoginError("Thiếu dữ liệu phiên đăng nhập từ API.");
return;
}
const mustChangePassword = loginData.must_change_password === true;
setAuthSession({
accessToken: loginData.access_token,
refreshToken: loginData.refresh_token,
persistSession: remember,
});
try {
const loginData = await loginAdmin(email.trim(), password, { persistSession: remember });
setAppUserRemember(
remember ? email.trim() : "",
remember ? password : "",
remember,
);
if (loginData?.must_change_password) {
try {
const profile = await getProfile();
const profileData =
(profile.data as any)?.responseData ??
(profile.data as any)?.data?.responseData;
const normalizedUser = normalizeUser(profileData, mustChangePassword);
if (normalizedUser) {
setAppUser(normalizedUser);
} else if (loginData.user) {
const fallbackUser = normalizeUser(loginData.user, mustChangePassword);
if (fallbackUser) setAppUser(fallbackUser);
}
} catch {
if (loginData.user) {
const fallbackUser = normalizeUser(loginData.user, mustChangePassword);
if (fallbackUser) setAppUser(fallbackUser);
}
}
if (mustChangePassword) {
toast.success("Đăng nhập thành công. Vui lòng đổi mật khẩu để tiếp tục.");
router.replace("/admin/change-password");
} else {
toast.success("Đăng nhập quản trị thành công");
router.replace(redirect);
}
} catch (error) {
},
onError: (err: any) => {
setLoginError(
getAuthErrorMessage(error, "Đăng nhập thất bại. Vui lòng thử lại."),
getAuthErrorMessage(err, "Đăng nhập thất bại. Vui lòng thử lại."),
);
} finally {
setLoginLoading(false);
}
},
},
});
const handleLogin = async (event: FormEvent<HTMLFormElement>) => {
event.preventDefault();
setLoginError(null);
login({
data: {
email: email.trim(),
password,
},
});
};
const forgotMutation = usePostApiV10AuthForgotPasswordRequest({
......
......@@ -7,7 +7,7 @@ import {
SelectTrigger,
SelectValue,
} from "@/components/ui/select";
import { type MemberField, type MemberRegion } from "@/mockdata/members";
import type { MemberField, MemberRegion } from "@/api/vcci-news/types/member";
import {
selectContentClassName,
selectItemClassName,
......
......@@ -7,7 +7,7 @@ import {
TableCell,
TableRow,
} from "@/components/ui/table";
import { type MemberItem } from "@/mockdata/members";
import type { MemberItem } from "@/api/vcci-news/types/member";
interface MemberRowProps {
item: MemberItem;
......
......@@ -12,7 +12,7 @@ import {
} from "@/components/ui/dialog";
import { Input } from "@/components/ui/input";
import { Label } from "@/components/ui/label";
import { type MemberField } from "@/mockdata/members";
import type { MemberField } from "@/api/vcci-news/types/member";
const fieldClassName =
"border-[#063e8e]/15 bg-white text-gray-700 placeholder:text-gray-700 focus-visible:ring-[#063e8e]/30";
......
......@@ -12,7 +12,7 @@ import {
} from "@/components/ui/dialog";
import { Input } from "@/components/ui/input";
import { Label } from "@/components/ui/label";
import { type MemberRegion } from "@/mockdata/members";
import type { MemberRegion } from "@/api/vcci-news/types/member";
const fieldClassName =
"border-[#063e8e]/15 bg-white text-gray-700 placeholder:text-gray-700 focus-visible:ring-[#063e8e]/30";
......
......@@ -30,15 +30,24 @@ import {
} from "@/components/ui/select";
import { Switch } from "@/components/ui/switch";
import {
type CmsHeaderCategoryItem,
type CmsTagItem,
createCmsNewsItem,
fetchCmsNewsItem,
fetchCmsNewsItems,
fetchCmsTags,
fetchHeaderConfigItems,
updateCmsNewsItem,
} from "@/lib/api/cms-admin";
type TagItem,
} from "@/api/vcci-news/types/tag";
import {
getApiV10Post,
getApiV10PostId,
postApiV10Post,
putApiV10PostId,
} from "@/api/vcci-news/endpoints/post";
import { getApiV10CategoryTree } from "@/api/vcci-news/endpoints/category";
import {
deleteApiV10PostTagPostId,
getApiV10PostTagPostId,
postApiV10PostTagPostIdBulk,
} from "@/api/vcci-news/endpoints/post-tag";
import {
getApiV10Tag,
postApiV10TagIds,
} from "@/api/vcci-news/endpoints/tag";
import {
ADMIN_NEWS_TYPE_OPTIONS,
cloneAdminNewsFormValues,
......@@ -48,7 +57,11 @@ import {
resolveAdminNewsType,
slugifyAdminNews,
} from "@/mockdata/admin-news";
import { buildHeaderCategoryTree } from "@/mockdata/header-config";
import { normalizeUser } from "@/lib/utils/cms-user";
import { normalizeDateTimeInput } from "@/lib/utils/datetime";
import { parsePostContent, parseLegacyPostContent } from "@/lib/utils/post-content";
import links from "@/links";
import type { HeaderCategoryTreeItem } from "@/api/vcci-news/types/header-config";
import {
fieldClassName,
readOnlyFieldClassName,
......@@ -79,8 +92,8 @@ export function AdminNewsFormContent() {
const isCreate = !newsId || newsId === "new";
const backPath = returnPath;
const [items, setItems] = useState<AdminNewsItem[]>([]);
const [headerItems, setHeaderItems] = useState<CmsHeaderCategoryItem[]>([]);
const [allTags, setAllTags] = useState<CmsTagItem[]>([]);
const [headerTree, setHeaderTree] = useState<HeaderCategoryTreeItem[]>([]);
const [allTags, setAllTags] = useState<TagItem[]>([]);
const [form, setForm] = useState<AdminNewsFormValues | null>(null);
const [pickerOpen, setPickerOpen] = useState(false);
const [tagSearch, setTagSearch] = useState("");
......@@ -98,22 +111,67 @@ export function AdminNewsFormContent() {
setForm(isCreate ? cloneAdminNewsFormValues() : null);
try {
const [nextHeaderConfig, nextTags] = await Promise.all([
fetchHeaderConfigItems(),
fetchCmsTags(),
const [treeResponse, nextTagsResponse] = await Promise.all([
getApiV10CategoryTree(),
getApiV10Tag({ page: 1, pageSize: 10, sortField: "name", sortOrder: "asc" }),
]);
const nextTagsResult = (nextTagsResponse.responseData ?? {}) as { rows?: TagItem[] };
const nextTags = nextTagsResult.rows ?? [];
const tree = (treeResponse.responseData ?? []) as unknown as HeaderCategoryTreeItem[];
const nextHeaderTree = tree;
const nextNewsItems = isCreate
? []
: (await fetchCmsNewsItems({
: await (async () => {
const response = await getApiV10Post({
page: 1,
pageSize: 10,
filters: newsId ? `id==${newsId}` : undefined,
})).items;
});
const result = (response.responseData ?? {}) as { rows?: any[] };
return (result.rows ?? []).map((item: any): AdminNewsItem => {
const structuredContent = parsePostContent(item.content_structure);
const postContent = structuredContent.length > 0 ? structuredContent : parseLegacyPostContent(item.content);
const categories = Array.isArray(item.categories) ? item.categories : [];
const primaryCategory = categories[0] ?? null;
const primaryCategoryType = primaryCategory?.type ?? null;
return {
id: item.id ?? "",
title: item.title ?? "",
slug: item.slug ?? "",
summary: item.summary ?? "",
type: item.type === "page" || primaryCategoryType === "post" || primaryCategoryType === "page" ? "baiviettrang" : "tintuc",
header_category_id: primaryCategory?.id ?? "",
category_ids: categories.map((c: any) => c.id),
tagsearch_values: [],
is_featured: Boolean(item.is_featured),
thumbnail: item.thumbnail?.id ? {
id: item.thumbnail.id,
name: item.thumbnail.original ?? item.thumbnail.path ?? "thumbnail",
alt: item.thumbnail.original ?? item.thumbnail.path ?? "thumbnail",
url: links.resolveImageUrl(item.thumbnail.path),
} : null,
is_hidden: Boolean(item.is_hidden),
created_at: item.created_at ?? "",
updated_at: item.updated_at ?? "",
published_at: normalizeDateTimeInput(item.published_at ?? item.release_at),
expired_at: normalizeDateTimeInput(item.expired_at),
started_at: normalizeDateTimeInput(item.started_at),
ended_at: normalizeDateTimeInput(item.ended_at),
registration_deadline: normalizeDateTimeInput(item.registration_deadline),
location: item.location ?? "",
participation_fee: item.participation_fee ?? "",
event_dates: Array.isArray(item.event_dates) ? item.event_dates.filter((d: any): d is string => typeof d === "string") : [],
post_content: postContent,
creator: normalizeUser(item.creator),
editor: normalizeUser(item.editor),
};
});
})();
if (cancelled) return;
setItems(nextNewsItems);
setHeaderItems(nextHeaderConfig.items);
setHeaderTree(nextHeaderTree);
setAllTags(nextTags);
if (isCreate) {
......@@ -130,9 +188,62 @@ export function AdminNewsFormContent() {
return;
}
const currentItem =
nextNewsItems.find((item) => item.id === newsId) ??
(newsId ? await fetchCmsNewsItem(newsId) : null);
const existingItem = nextNewsItems.find((item) => item.id === newsId);
const currentItem = existingItem
? existingItem
: newsId
? await (async () => {
const response = await getApiV10PostId(newsId);
const post = (response.responseData ?? {}) as any;
// Fetch tags for post
const tagsRes = await getApiV10PostTagPostId(newsId);
const tagsResult = (tagsRes.responseData ?? {}) as { rows?: { tag_id?: string }[] };
const tagIds = (tagsResult.rows ?? []).map(r => r.tag_id).filter(Boolean) as string[];
let tags: TagItem[] = [];
if (tagIds.length > 0) {
const tagsResponse = await postApiV10TagIds({ tag_ids: tagIds });
tags = (tagsResponse.responseData ?? []) as TagItem[];
}
const tagMap = new Map<string, TagItem[]>([[newsId, tags]]);
const tagItems = tagMap.get(newsId) ?? [];
const structuredContent = parsePostContent(post.content_structure);
const postContent = structuredContent.length > 0 ? structuredContent : parseLegacyPostContent(post.content);
const categories = Array.isArray(post.categories) ? post.categories : [];
const primaryCategory = categories[0] ?? null;
const primaryCategoryType = primaryCategory?.type ?? null;
return {
id: post.id ?? "",
title: post.title ?? "",
slug: post.slug ?? "",
summary: post.summary ?? "",
type: post.type === "page" || primaryCategoryType === "post" || primaryCategoryType === "page" ? "baiviettrang" : "tintuc",
header_category_id: primaryCategory?.id ?? "",
category_ids: categories.map((c: any) => c.id),
tagsearch_values: tagItems.map((t) => t.name),
is_featured: Boolean(post.is_featured),
thumbnail: post.thumbnail?.id ? {
id: post.thumbnail.id,
name: post.thumbnail.original ?? post.thumbnail.path ?? "thumbnail",
alt: post.thumbnail.original ?? post.thumbnail.path ?? "thumbnail",
url: links.resolveImageUrl(post.thumbnail.path),
} : null,
is_hidden: Boolean(post.is_hidden),
created_at: post.created_at ?? "",
updated_at: post.updated_at ?? "",
published_at: normalizeDateTimeInput(post.published_at ?? post.release_at),
expired_at: normalizeDateTimeInput(post.expired_at),
started_at: normalizeDateTimeInput(post.started_at),
ended_at: normalizeDateTimeInput(post.ended_at),
registration_deadline: normalizeDateTimeInput(post.registration_deadline),
location: post.location ?? "",
participation_fee: post.participation_fee ?? "",
event_dates: Array.isArray(post.event_dates) ? post.event_dates.filter((d: any): d is string => typeof d === "string") : [],
post_content: postContent,
creator: normalizeUser(post.creator),
editor: normalizeUser(post.editor),
} as AdminNewsItem;
})()
: null;
if (cancelled) return;
......@@ -166,8 +277,8 @@ export function AdminNewsFormContent() {
}, [isCreate, newsId]);
const headerOptions = useMemo(() => {
return flattenHeaderTree(buildHeaderCategoryTree(headerItems));
}, [headerItems]);
return flattenHeaderTree(headerTree);
}, [headerTree]);
const availableSearchTags = useMemo(() => {
if (form?.type !== "tintuc") return [];
......@@ -290,7 +401,7 @@ export function AdminNewsFormContent() {
};
const handleHeaderCategoryChange = (value: string) => {
const nextCategory = headerItems.find((item) => item.id === value) ?? null;
const nextCategory = headerOptions.find((item) => item.id === value) ?? null;
setForm((current) => {
if (!current) return current;
......@@ -371,43 +482,61 @@ export function AdminNewsFormContent() {
return;
}
setIsSubmitting(true);
const tagIds = form.type === "baiviettrang" ? [] : selectedTagIds;
try {
const payload = {
title: form.title.trim(),
slug: slugifyAdminNews(form.slug.trim()),
summary: form.summary,
type: form.type,
header_category_id: form.type === "tintuc" ? form.category_ids[0] ?? "" : form.header_category_id,
type: form.type === "baiviettrang" ? "page" : "news",
content: form.summary || "",
category_ids:
form.type === "baiviettrang"
? form.header_category_id
? [form.header_category_id]
: []
: form.category_ids,
tag_ids: form.type === "baiviettrang" ? [] : selectedTagIds,
is_featured: form.type === "tintuc" ? form.is_featured : false,
thumbnail_id: form.thumbnail && isUuid(form.thumbnail.id) ? form.thumbnail.id : null,
is_featured: form.type === "tintuc" ? form.is_featured : false,
is_hidden: form.is_hidden,
is_active: !form.is_hidden,
published_at: form.published_at || null,
expired_at: form.expired_at || null,
started_at: form.started_at || null,
ended_at: form.ended_at || null,
registration_deadline: form.registration_deadline || null,
location: form.location.trim(),
participation_fee: form.participation_fee.trim(),
location: form.location.trim() || null,
participation_fee: form.participation_fee.trim() || null,
event_dates: (form.event_dates ?? []).length > 0 ? form.event_dates : null,
release_mode: form.published_at ? "SCHEDULED" : "NOW",
release_at: form.published_at || null,
content_structure: {
post_content: form.post_content.map((section, index) => ({
...section,
position: index + 1,
})),
},
};
setIsSubmitting(true);
try {
if (isCreate) {
await createCmsNewsItem(payload);
const response = await postApiV10Post(payload as any);
const created = (response.responseData ?? {}) as { id?: string };
if (created.id && tagIds.length > 0) {
await postApiV10PostTagPostIdBulk(created.id, { tag_ids: tagIds });
}
} else if (newsId) {
await updateCmsNewsItem(newsId, payload);
await putApiV10PostId(newsId, payload as any);
// Xóa tags cũ rồi bulk add tags mới
const currentRes = await getApiV10PostTagPostId(newsId);
const currentResult = (currentRes.responseData ?? {}) as { rows?: { tag_id?: string }[] };
const currentTagIds = (currentResult.rows ?? []).map(r => r.tag_id).filter(Boolean) as string[];
await Promise.all(currentTagIds.map(tagId => deleteApiV10PostTagPostId(newsId, { tag_id: tagId })));
if (tagIds.length > 0) {
await postApiV10PostTagPostIdBulk(newsId, { tag_ids: tagIds });
}
}
toast.success(isCreate ? "Đã tạo bài viết" : "Đã cập nhật bài viết");
......
import type { AdminMediaItem, AdminNewsImageRef, AdminNewsType } from "@/mockdata/admin-news";
import type { HeaderCategoryItem, HeaderCategoryTreeItem } from "@/mockdata/header-config";
import type { HeaderCategoryItem, HeaderCategoryTreeItem } from "@/api/vcci-news/types/header-config";
export type HeaderCategoryOption = {
id: string;
......
......@@ -15,7 +15,7 @@ import {
} from "@/components/ui/command";
import { Popover, PopoverContent, PopoverTrigger } from "@/components/ui/popover";
import { cn } from "@/lib/utils";
import { type HeaderCategoryItem } from "@/mockdata/header-config";
import type { HeaderCategoryItem } from "@/api/vcci-news/types/header-config";
import { formatHeaderCategoryOptionLabel } from "./utils";
export function CategoryFilterCombobox({
......
......@@ -8,7 +8,6 @@ import {
Star,
Tag,
} from "lucide-react";
import Link from "next/link";
import { usePathname, useRouter, useSearchParams } from "next/navigation";
import { toast } from "sonner";
import { AdminDeleteDialog } from "@/components/admin/admin-delete-dialog";
......@@ -34,20 +33,23 @@ import {
TableRow,
} from "@/components/ui/table";
import {
deleteCmsNewsItem,
fetchCmsNewsItems,
fetchCmsPostCount,
fetchHeaderConfigItems,
toggleCmsNewsVisibility,
} from "@/lib/api/cms-admin";
deleteApiV10PostId,
getApiV10Post,
putApiV10PostId,
useGetApiV10PostStats,
} from "@/api/vcci-news/endpoints/post";
import { getApiV10CategoryTree } from "@/api/vcci-news/endpoints/category";
import {
ADMIN_NEWS_TYPE_LABELS,
ADMIN_NEWS_TYPE_OPTIONS,
type AdminNewsItem,
} from "@/mockdata/admin-news";
import { normalizeUser } from "@/lib/utils/cms-user";
import { normalizeDateTimeInput } from "@/lib/utils/datetime";
import { parsePostContent, parseLegacyPostContent } from "@/lib/utils/post-content";
import links from "@/links";
import {
buildHeaderCategoryTree,
type HeaderCategoryItem,
type HeaderCategoryTreeItem,
} from "@/mockdata/header-config";
import { AdminNewsTableLoading } from "./_components/admin-news-table-loading";
import { CategoryFilterCombobox } from "./_components/category-filter-combobox";
......@@ -71,7 +73,7 @@ export default function AdminNewsPage() {
const pathname = usePathname();
const searchParams = useSearchParams();
const [items, setItems] = useState<AdminNewsItem[]>([]);
const [headerItems, setHeaderItems] = useState<HeaderCategoryItem[]>([]);
const [headerTree, setHeaderTree] = useState<HeaderCategoryTreeItem[]>([]);
const [search, setSearch] = useState(() => searchParams.get("q") ?? "");
const [typeFilter, setTypeFilter] = useState(
() => searchParams.get("type") ?? "all",
......@@ -92,8 +94,6 @@ export default function AdminNewsPage() {
});
const [pageSize] = useState(10);
const [total, setTotal] = useState(0);
const [publishedTotal, setPublishedTotal] = useState(0);
const [featuredTotal, setFeaturedTotal] = useState(0);
const didMountRef = useRef(false);
const debouncedSearch = useDebouncedValue(search);
......@@ -129,9 +129,10 @@ export default function AdminNewsPage() {
);
useEffect(() => {
void fetchHeaderConfigItems()
.then((headerConfig) => {
setHeaderItems(headerConfig.items);
void getApiV10CategoryTree()
.then((response) => {
const tree = (response.responseData ?? []) as unknown as HeaderCategoryTreeItem[];
setHeaderTree(tree);
})
.catch((error) => {
toast.error(
......@@ -179,50 +180,65 @@ export default function AdminNewsPage() {
return [...baseFilterParts, ...statusFilterParts].join(",");
}, [baseFilterParts, statusFilterParts]);
const visibleStatsFilters = useMemo(() => {
return [...baseFilterParts, ...statusFilterParts, "is_hidden==false"].join(",");
}, [baseFilterParts, statusFilterParts]);
const featuredStatsFilters = useMemo(() => {
return [...baseFilterParts, ...statusFilterParts, "is_featured==true"].join(",");
}, [baseFilterParts, statusFilterParts]);
const loadStats = useCallback(async () => {
const [nextPublishedTotal, nextFeaturedTotal] = await Promise.all([
fetchCmsPostCount(visibleStatsFilters),
fetchCmsPostCount(featuredStatsFilters),
]);
setPublishedTotal(nextPublishedTotal);
setFeaturedTotal(nextFeaturedTotal);
}, [featuredStatsFilters, visibleStatsFilters]);
const statsResponse = useGetApiV10PostStats(
{ filters: apiFilters || undefined },
);
const statsData = statsResponse.data?.responseData;
const load = useCallback(async () => {
setReady(false);
const newsData = await fetchCmsNewsItems({
const response = await getApiV10Post({
page,
pageSize,
sortField: "created_at",
sortOrder: "desc",
filters: apiFilters,
filters: apiFilters?.trim() || undefined,
});
setItems(newsData.items);
setTotal(newsData.total);
const result = (response.responseData ?? {}) as { rows?: any[]; count?: number; page?: number; pageSize?: number };
setItems((result.rows ?? []).map((item: any): AdminNewsItem => {
const structuredContent = parsePostContent(item.content_structure);
const postContent = structuredContent.length > 0 ? structuredContent : parseLegacyPostContent(item.content);
const categories = Array.isArray(item.categories) ? item.categories : [];
const primaryCategory = categories[0] ?? null;
const primaryCategoryType = primaryCategory?.type ?? null;
return {
id: item.id ?? "",
title: item.title ?? "",
slug: item.slug ?? "",
summary: item.summary ?? "",
type: item.type === "page" || primaryCategoryType === "post" || primaryCategoryType === "page" ? "baiviettrang" : "tintuc",
header_category_id: primaryCategory?.id ?? "",
category_ids: categories.map((c: any) => c.id),
tagsearch_values: [],
is_featured: Boolean(item.is_featured),
thumbnail: item.thumbnail?.id ? {
id: item.thumbnail.id,
name: item.thumbnail.original ?? item.thumbnail.path ?? "thumbnail",
alt: item.thumbnail.original ?? item.thumbnail.path ?? "thumbnail",
url: links.resolveImageUrl(item.thumbnail.path),
} : null,
is_hidden: Boolean(item.is_hidden),
created_at: item.created_at ?? "",
updated_at: item.updated_at ?? "",
published_at: normalizeDateTimeInput(item.published_at ?? item.release_at),
expired_at: normalizeDateTimeInput(item.expired_at),
started_at: normalizeDateTimeInput(item.started_at),
ended_at: normalizeDateTimeInput(item.ended_at),
registration_deadline: normalizeDateTimeInput(item.registration_deadline),
location: item.location ?? "",
participation_fee: item.participation_fee ?? "",
event_dates: Array.isArray(item.event_dates) ? item.event_dates.filter((d: any): d is string => typeof d === "string") : [],
post_content: postContent,
creator: normalizeUser(item.creator),
editor: normalizeUser(item.editor),
};
}));
setTotal(result.count ?? 0);
setReady(true);
}, [apiFilters, page, pageSize]);
useEffect(() => {
void loadStats().catch((error) => {
toast.error(
error instanceof Error
? error.message
: "Kh\u00f4ng th\u1ec3 t\u1ea3i s\u1ed1 li\u1ec7u b\u00e0i vi\u1ebft",
);
});
}, [loadStats]);
useEffect(() => {
void load().catch((error) => {
toast.error(
......@@ -253,30 +269,30 @@ export default function AdminNewsPage() {
}, [apiFilters, typeFilter]);
const categoryOptions = useMemo(() => {
return flattenHeaderTree(buildHeaderCategoryTree(headerItems)).filter(
return flattenHeaderTree(headerTree).filter(
(item) => item.type === "news" || item.type === "page",
);
}, [headerItems]);
}, [headerTree]);
const stats = useMemo(() => {
return [
{
label: "Tổng bài viết",
value: total,
value: statsData?.total ?? total,
icon: <Tag className="h-4 w-4 text-[#063e8e]" />,
},
{
label: "Đang hiển thị",
value: publishedTotal,
value: statsData?.published ?? 0,
icon: <Tag className="h-4 w-4 text-[#063e8e]" />,
},
{
label: "Tin nổi bật",
value: featuredTotal,
value: statsData?.featured ?? 0,
icon: <Tag className="h-4 w-4 text-[#063e8e]" />,
},
];
}, [featuredTotal, publishedTotal, total]);
}, [statsData, total]);
const handleDelete = async () => {
if (!deleteTarget || isDeleting) return;
......@@ -284,10 +300,11 @@ export default function AdminNewsPage() {
setIsDeleting(true);
try {
await deleteCmsNewsItem(deleteTarget.id);
await deleteApiV10PostId(deleteTarget.id);
toast.success("Đã xóa bài viết");
setDeleteTarget(null);
await Promise.all([load(), loadStats()]);
await load();
statsResponse.refetch();
} catch (error) {
toast.error(
error instanceof Error ? error.message : "Không thể xóa bài viết",
......@@ -304,9 +321,13 @@ export default function AdminNewsPage() {
setTogglingVisibilityId(item.id);
try {
await toggleCmsNewsVisibility(item.id, nextIsHidden);
await putApiV10PostId(item.id, {
is_hidden: nextIsHidden,
is_active: !nextIsHidden,
} as any);
toast.success(nextIsHidden ? "Đã ẩn bài viết" : "Đã hiển thị bài viết");
await Promise.all([load(), loadStats()]);
await load();
statsResponse.refetch();
} catch (error) {
toast.error(
error instanceof Error ? error.message : "Không thể thay đổi trạng thái hiển thị",
......@@ -423,7 +444,7 @@ export default function AdminNewsPage() {
</TableRow>
) : (
items.map((item, index) => {
const categoryNames = getDisplayCategoryNames(item, headerItems);
const categoryNames = getDisplayCategoryNames(item, categoryOptions);
const primaryCategoryName = categoryNames[0] ?? "\u2014";
const extraCategoryCount = Math.max(categoryNames.length - 1, 0);
......
......@@ -3,6 +3,7 @@
import { useEffect } from "react";
import { useRouter } from "next/navigation";
import useAuthStore from "@/store/useAuthStore";
import useUserStore from "@/store/useUserStore";
import { usePermission } from "@/hooks/usePermission";
import { Loader2, ShieldX, Mail, Phone } from "lucide-react";
import { Button } from "@/components/ui/button";
......@@ -25,7 +26,7 @@ const ADMIN_LANDING_ROUTES = [
export default function AdminPage() {
const router = useRouter();
const appUser = useAuthStore((state) => state.appUser);
const appUser = useUserStore((state) => state.appUser);
const hasHydrated = useAuthStore((state) => state._hasHydrated);
const isLoggedIn = useAuthStore((state) => state.appIsLoggedIn);
......
......@@ -2,11 +2,11 @@
import { AdminDeleteDialog } from "@/components/admin/admin-delete-dialog";
import type { CmsTagItem } from "./types";
import type { TagItem } from "./types";
interface TagDeleteDialogProps {
target: CmsTagItem | null;
onTargetChange: (target: CmsTagItem | null) => void;
target: TagItem | null;
onTargetChange: (target: TagItem | null) => void;
isSubmitting: boolean;
onConfirm: () => void;
}
......
......@@ -15,20 +15,20 @@ import {
TableRow,
} from "@/components/ui/table";
import { PAGE_SIZE, type CmsTagItem } from "./types";
import { PAGE_SIZE, type TagItem } from "./types";
interface TagsTableProps {
search: string;
onSearchChange: (value: string) => void;
isReady: boolean;
onActionClick: () => void;
items: CmsTagItem[];
items: TagItem[];
total: number;
page: number;
totalPages: number;
onPageChange: (page: number) => void;
onEdit: (item: CmsTagItem) => void;
onDelete: (item: CmsTagItem) => void;
onEdit: (item: TagItem) => void;
onDelete: (item: TagItem) => void;
}
export function TagsTable({
......
import type { CmsTagItem } from "@/lib/api/cms-admin";
import type { TagItem } from "@/api/vcci-news/types/tag";
export interface TagFormValues {
id?: string;
......@@ -13,4 +13,4 @@ export const EMPTY_FORM: TagFormValues = {
slug: "",
};
export type { CmsTagItem };
export type { TagItem };
......@@ -3,12 +3,14 @@
import { useCallback, useEffect, useState } from "react";
import { toast } from "sonner";
import {
type CmsTagItem,
createCmsTag,
deleteCmsTag,
fetchCmsTagsPage,
updateCmsTag,
} from "@/lib/api/cms-admin";
type TagItem,
} from "@/api/vcci-news/types/tag";
import {
deleteApiV10TagId,
getApiV10Tag,
patchApiV10TagId,
postApiV10Tag,
} from "@/api/vcci-news/endpoints/tag";
import { TagDeleteDialog } from "./_components/tag-delete-dialog";
import { TagFormDialog } from "./_components/tag-form-dialog";
......@@ -17,13 +19,13 @@ import { EMPTY_FORM, PAGE_SIZE, type TagFormValues } from "./_components/types";
import { slugifyTag } from "./_components/utils";
export default function AdminTagsPage() {
const [items, setItems] = useState<CmsTagItem[]>([]);
const [items, setItems] = useState<TagItem[]>([]);
const [search, setSearch] = useState("");
const [isReady, setIsReady] = useState(false);
const [isSubmitting, setIsSubmitting] = useState(false);
const [formOpen, setFormOpen] = useState(false);
const [formValues, setFormValues] = useState<TagFormValues>(EMPTY_FORM);
const [deleteTarget, setDeleteTarget] = useState<CmsTagItem | null>(null);
const [deleteTarget, setDeleteTarget] = useState<TagItem | null>(null);
const [page, setPage] = useState(1);
const [total, setTotal] = useState(0);
......@@ -31,14 +33,17 @@ export default function AdminTagsPage() {
setIsReady(false);
const keyword = search.trim();
const result = await fetchCmsTagsPage({
const response = await getApiV10Tag({
page,
pageSize: PAGE_SIZE,
sortField: "name",
sortOrder: "asc",
filters: keyword ? `name@=${keyword}|slug@=${keyword}` : undefined,
});
const result = (response.responseData ?? {}) as { rows?: TagItem[]; count?: number };
setItems(result.items);
setTotal(result.total);
setItems(result.rows ?? []);
setTotal(result.count ?? 0);
setIsReady(true);
}, [page, search]);
......@@ -68,7 +73,7 @@ export default function AdminTagsPage() {
setFormOpen(true);
};
const openEdit = (item: CmsTagItem) => {
const openEdit = (item: TagItem) => {
setFormValues({
id: item.id,
name: item.name,
......@@ -94,10 +99,10 @@ export default function AdminTagsPage() {
try {
if (formValues.id) {
await updateCmsTag(formValues.id, payload);
await patchApiV10TagId(formValues.id, payload);
toast.success("Cập nhật tag thành công");
} else {
await createCmsTag(payload);
await postApiV10Tag(payload);
toast.success("Tạo tag thành công");
}
......@@ -117,7 +122,7 @@ export default function AdminTagsPage() {
setIsSubmitting(true);
try {
await deleteCmsTag(deleteTarget.id);
await deleteApiV10TagId(deleteTarget.id);
toast.success("Xóa tag thành công");
setDeleteTarget(null);
await load();
......
"use client";
import { useEffect, useRef, useState } from "react";
import { useEffect, useState } from "react";
import { usePathname, useRouter } from "next/navigation";
import { ensureValidAdminAccessToken, logoutAdmin } from "@/lib/auth/admin-auth";
import useAuthStore from "@/store/useAuthStore";
import useUserStore from "@/store/useUserStore";
const LOGIN_PATH = "/admin/login";
const CHANGE_PASSWORD_PATH = "/admin/change-password";
const PROACTIVE_REFRESH_INTERVAL_MS = 60 * 1000;
const getRefreshTokenExpiredAt = (session?: { refresh_expires_at?: string | null } | null) => {
if (!session?.refresh_expires_at) return null;
const time = new Date(session.refresh_expires_at).getTime();
return Number.isFinite(time) ? time : null;
};
const isSessionUsable = () => {
const state = useAuthStore.getState();
if (!state.appRefreshToken) return false;
const refreshExpiredAt = getRefreshTokenExpiredAt(state.appSession);
return !refreshExpiredAt || refreshExpiredAt > Date.now();
};
export function AdminAuthLoadingScreen() {
return (
......@@ -87,20 +73,26 @@ export function AdminAuthGuard({ children }: { children: React.ReactNode }) {
const hasHydrated = useAuthStore((state) => state._hasHydrated);
const isLoggedIn = useAuthStore((state) => state.appIsLoggedIn);
const accessToken = useAuthStore((state) => state.appAccessToken);
const accessTokenExpired = useAuthStore((state) => state.appAccessTokenExpired);
const refreshToken = useAuthStore((state) => state.appRefreshToken);
const isRefreshing = useAuthStore((state) => state.appIsRefreshing);
const mustChangePassword = useAuthStore((state) => state.appUser?.must_change_password === true);
const [authCheckState, setAuthCheckState] = useState<"idle" | "checking" | "ready">("idle");
const redirectParam =
typeof window === "undefined"
? encodeURIComponent(pathname || "/admin")
: encodeURIComponent(`${window.location.pathname}${window.location.search}`);
const mustChangePassword = useUserStore((state) => state.appUser?.must_change_password === true);
// Redirect to login if not authenticated
useEffect(() => {
if (hasHydrated && !isLoggedIn && pathname !== LOGIN_PATH) {
const currentPath = `${window.location.pathname}${window.location.search}`;
const isChangePasswordPage = currentPath.startsWith(CHANGE_PASSWORD_PATH);
const redirect =
currentPath.startsWith("/admin") &&
currentPath !== LOGIN_PATH &&
!isChangePasswordPage
? `?redirect=${encodeURIComponent(currentPath)}`
: "";
window.location.replace(`/admin/login${redirect}`);
}
}, [hasHydrated, isLoggedIn, pathname]);
// Force redirect to change-password page if user must change password
useEffect(() => {
if (
authCheckState === "ready" &&
isLoggedIn &&
mustChangePassword &&
pathname !== CHANGE_PASSWORD_PATH &&
......@@ -108,114 +100,16 @@ export function AdminAuthGuard({ children }: { children: React.ReactNode }) {
) {
router.replace(CHANGE_PASSWORD_PATH);
}
}, [authCheckState, isLoggedIn, mustChangePassword, pathname, router]);
useEffect(() => {
if (pathname === LOGIN_PATH) {
setAuthCheckState("ready");
return;
}
if (!hasHydrated) {
setAuthCheckState("idle");
return;
}
let cancelled = false;
const restoreSession = async () => {
setAuthCheckState("checking");
// Refresh token đã hết hạn → logout ngay, không cố refresh nữa
if (!isSessionUsable()) {
if (!cancelled) {
setAuthCheckState("ready");
void logoutAdmin({ silent: true, reason: "missing_refresh_token" });
}
return;
}
// Access token còn hạn → OK
const hasValidAccessToken = Boolean(
accessToken &&
isLoggedIn &&
accessTokenExpired !== null &&
accessTokenExpired > Date.now(),
);
if (hasValidAccessToken) {
if (!cancelled) {
setAuthCheckState("ready");
}
return;
}
// Access token hết hạn nhưng còn refresh token → thử refresh
try {
const nextToken = await ensureValidAdminAccessToken();
if (!nextToken && !cancelled) {
// refresh đã gọi logoutAdmin bên trong, không cần redirect thêm
setAuthCheckState("ready");
}
} catch {
if (!cancelled) {
setAuthCheckState("ready");
}
} finally {
if (!cancelled) {
setAuthCheckState("ready");
}
}
};
void restoreSession();
// Proactive token refresh: kiểm tra định kỳ và refresh trước khi hết hạn
const refreshInterval = setInterval(() => {
if (cancelled) return;
const state = useAuthStore.getState();
if (!state.appAccessToken || !state.appRefreshToken) return;
// Refresh token hết hạn → logout ngay
if (!isSessionUsable()) {
void logoutAdmin({ silent: true, reason: "missing_refresh_token" });
return;
}
void ensureValidAdminAccessToken().catch(() => null);
}, PROACTIVE_REFRESH_INTERVAL_MS);
return () => {
cancelled = true;
clearInterval(refreshInterval);
};
}, [
accessToken,
accessTokenExpired,
hasHydrated,
isLoggedIn,
pathname,
redirectParam,
refreshToken,
router,
]);
}, [isLoggedIn, mustChangePassword, pathname, router]);
if (pathname === LOGIN_PATH || pathname === CHANGE_PASSWORD_PATH) {
return <>{children}</>;
}
if (!hasHydrated || isRefreshing || authCheckState !== "ready") {
if (!hasHydrated || !isLoggedIn || !accessToken) {
return <AdminAuthLoadingScreen />;
}
if (!isLoggedIn || (!accessToken && refreshToken)) {
return <AdminAuthLoadingScreen />;
}
if (!isLoggedIn || !accessToken) {
return null;
}
// Nếu user phải đổi mật khẩu, không render nội dung admin (đã redirect ở useEffect)
if (mustChangePassword) {
return <AdminAuthLoadingScreen />;
}
......@@ -224,120 +118,32 @@ export function AdminAuthGuard({ children }: { children: React.ReactNode }) {
}
export function useAdminAuthStatus() {
const router = useRouter();
const pathname = usePathname();
const hasHydrated = useAuthStore((state) => state._hasHydrated);
const isLoggedIn = useAuthStore((state) => state.appIsLoggedIn);
const accessToken = useAuthStore((state) => state.appAccessToken);
const accessTokenExpired = useAuthStore((state) => state.appAccessTokenExpired);
const refreshToken = useAuthStore((state) => state.appRefreshToken);
const isRefreshing = useAuthStore((state) => state.appIsRefreshing);
const [authCheckState, setAuthCheckState] = useState<"idle" | "checking" | "ready">("idle");
const redirectParam =
typeof window === "undefined"
? encodeURIComponent(pathname || "/admin")
: encodeURIComponent(`${window.location.pathname}${window.location.search}`);
const pathname = usePathname();
useEffect(() => {
if (pathname === LOGIN_PATH) {
setAuthCheckState("ready");
return;
}
if (!hasHydrated) {
setAuthCheckState("idle");
return;
}
let cancelled = false;
const restoreSession = async () => {
setAuthCheckState("checking");
// Refresh token đã hết hạn → logout ngay
if (!isSessionUsable()) {
if (!cancelled) {
setAuthCheckState("ready");
void logoutAdmin({ silent: true, reason: "missing_refresh_token" });
}
return;
}
const hasValidAccessToken = Boolean(
accessToken &&
isLoggedIn &&
accessTokenExpired !== null &&
accessTokenExpired > Date.now(),
);
if (hasValidAccessToken) {
if (!cancelled) {
setAuthCheckState("ready");
}
return;
}
try {
const nextToken = await ensureValidAdminAccessToken();
if (!nextToken && !cancelled) {
setAuthCheckState("ready");
}
} catch {
if (!cancelled) {
setAuthCheckState("ready");
}
} finally {
if (!cancelled) {
setAuthCheckState("ready");
}
}
};
void restoreSession();
// Proactive token refresh
const refreshInterval = setInterval(() => {
if (cancelled) return;
const state = useAuthStore.getState();
if (!state.appAccessToken || !state.appRefreshToken) return;
if (!isSessionUsable()) {
void logoutAdmin({ silent: true, reason: "missing_refresh_token" });
return;
}
void ensureValidAdminAccessToken().catch(() => null);
}, PROACTIVE_REFRESH_INTERVAL_MS);
return () => {
cancelled = true;
clearInterval(refreshInterval);
};
}, [
accessToken,
accessTokenExpired,
hasHydrated,
isLoggedIn,
pathname,
redirectParam,
refreshToken,
router,
]);
if (hasHydrated && !isLoggedIn && pathname !== LOGIN_PATH) {
const currentPath = `${window.location.pathname}${window.location.search}`;
const isChangePasswordPage = currentPath.startsWith(CHANGE_PASSWORD_PATH);
const redirect =
currentPath.startsWith("/admin") &&
currentPath !== LOGIN_PATH &&
!isChangePasswordPage
? `?redirect=${encodeURIComponent(currentPath)}`
: "";
window.location.replace(`/admin/login${redirect}`);
}
}, [hasHydrated, isLoggedIn, pathname]);
if (pathname === LOGIN_PATH) {
return "ready" as const;
}
if (!hasHydrated || isRefreshing || authCheckState !== "ready") {
return "loading" as const;
}
if (!isLoggedIn || (!accessToken && refreshToken)) {
if (!hasHydrated || !isLoggedIn || !accessToken) {
return "loading" as const;
}
if (!isLoggedIn || !accessToken) {
return "blocked" as const;
}
return "ready" as const;
}
'use client';
import React from 'react';
import { usePathname } from 'next/navigation';
import { usePathname, useRouter } from 'next/navigation';
import { LogOut, Menu, ShieldCheck } from 'lucide-react';
import { toast } from 'sonner';
import { Button } from '@/components/ui/button';
import { logoutAdmin } from '@/lib/auth/admin-auth';
import { usePostApiV10AuthLogout } from '@/api/vcci-news/endpoints/authentication';
import { useSidebarStore } from '@/hooks/use-admin-sidebar';
import useAuthStore from '@/store/useAuthStore';
import useUserStore from '@/store/useUserStore';
const routeLabels: Record<string, string> = {
'/admin/base-config': 'Cấu hình chung',
......@@ -57,12 +58,30 @@ function formatRoles(roles?: string[]) {
export function AdminHeader() {
const { toggle } = useSidebarStore();
const router = useRouter();
const pathname = usePathname();
const title = getTitle(pathname);
const currentUser = useAuthStore((state) => state.appUser);
const currentUser = useUserStore((state) => state.appUser);
const { mutateAsync: logoutAsync } = usePostApiV10AuthLogout({
mutation: {
onError: (error) => {
console.error('Logout error:', error);
},
},
});
const handleLogout = async () => {
await logoutAdmin({ redirectToLogin: true });
try {
await logoutAsync();
} catch {
// Ignore logout API failure and continue clearing local auth state.
} finally {
useAuthStore.getState().resetStore();
useUserStore.getState().clearUser();
toast.success('Đã đăng xuất khỏi trang quản trị');
router.push('/admin/login');
}
};
return (
......
......@@ -20,15 +20,13 @@ import {
KeyRound,
Video,
} from "lucide-react";
import { useQuery } from "@tanstack/react-query";
import { useGetApiV10Logo } from "@/api/vcci-news/endpoints/logo";
import type { Logo } from "@/api/vcci-news/models/logo";
import logo from "@/assets/VCCI-HCM-logo-VN-2025.png";
import links from "@/links";
import { useSidebarStore } from "@/hooks/use-admin-sidebar";
import { usePermission } from "@/hooks/usePermission";
import { cn } from "@/lib/utils";
import useAuthStore from "@/store/useAuthStore";
import useUserStore from "@/store/useUserStore";
type LogoListEnvelope = {
data?: {
......@@ -122,8 +120,8 @@ export function AdminSidebar() {
const pathname = usePathname();
const { close, isOpen } = useSidebarStore();
const [expandedGroups, setExpandedGroups] = React.useState<Record<string, boolean>>({});
const userPermissions = useAuthStore((state) => state.appUser?.permissions) || [];
const userRoles = useAuthStore((state) => state.appUser?.roles) || [];
const userPermissions = useUserStore((state) => state.appUser?.permissions) || [];
const userRoles = useUserStore((state) => state.appUser?.roles) || [];
const { data: logoData } = useGetApiV10Logo(
{
......
"use client";
import { useMemo } from "react";
import useAuthStore from "@/store/useAuthStore";
import useUserStore from "@/store/useUserStore";
/**
* Hook để kiểm tra permission của user hiện tại
......@@ -20,7 +20,7 @@ export function usePermission(
resourceOrPermissions: string | string[],
actionOrMode?: string,
): boolean {
const appUser = useAuthStore((state) => state.appUser);
const appUser = useUserStore((state) => state.appUser);
return useMemo(() => {
const userPermissions = appUser?.permissions || [];
......@@ -66,7 +66,7 @@ export function usePermission(
* const isSystemAdmin = useHasRole("system_admin");
*/
export function useHasRole(roleName: string | string[]): boolean {
const appUser = useAuthStore((state) => state.appUser);
const appUser = useUserStore((state) => state.appUser);
return useMemo(() => {
const userRoles = appUser?.roles || [];
......@@ -86,7 +86,7 @@ export function useHasRole(roleName: string | string[]): boolean {
* const permissions = useAllPermissions();
*/
export function useAllPermissions(): string[] {
const appUser = useAuthStore((state) => state.appUser);
const appUser = useUserStore((state) => state.appUser);
return appUser?.permissions || [];
}
......@@ -97,7 +97,7 @@ export function useAllPermissions(): string[] {
* const roles = useAllRoles();
*/
export function useAllRoles(): string[] {
const appUser = useAuthStore((state) => state.appUser);
const appUser = useUserStore((state) => state.appUser);
return appUser?.roles || [];
}
......@@ -108,7 +108,7 @@ export function useAllRoles(): string[] {
* const hasAnyPermission = useHasAnyPermission();
*/
export function useHasAnyPermission(): boolean {
const appUser = useAuthStore((state) => state.appUser);
const appUser = useUserStore((state) => state.appUser);
return (appUser?.permissions?.length || 0) > 0;
}
......
"use client";
import { toCmsSlug } from "@/lib/utils/cms-slug";
import links from "@/links";
import { categoryFallbackRows } from "@/mockdata/categories";
import {
getApiV10Tag,
postApiV10Tag,
patchApiV10TagId,
deleteApiV10TagId,
postApiV10TagIds,
} from "@/api/vcci-news/endpoints/tag";
import {
getApiV10Category,
postApiV10Category,
putApiV10CategoryId,
deleteApiV10CategoryId,
} from "@/api/vcci-news/endpoints/category";
import {
getApiV10Post,
getApiV10PostId,
postApiV10Post,
putApiV10PostId,
deleteApiV10PostId,
} from "@/api/vcci-news/endpoints/post";
import {
getApiV10PostTagPostId,
postApiV10PostTagPostIdBulk,
deleteApiV10PostTagPostId,
} from "@/api/vcci-news/endpoints/post-tag";
export type CmsHeaderCategoryType = "category" | "page" | "news";
export interface CmsTagItem {
id: string;
name: string;
slug: string;
created_at?: string;
updated_at?: string;
}
export interface CmsCategoryItem {
id: string;
name: string;
slug: string;
type: string;
url?: string | null;
sort_order?: number | null;
parent_id?: string | null;
created_at?: string;
updated_at?: string;
}
export interface CmsFileItem {
id: string;
path?: string;
original?: string;
mime?: string;
}
export interface CmsPostContentImage {
position: number;
caption: string;
image: {
id: string;
name: string;
alt: string;
url: string;
};
}
export interface CmsPostContentSection {
id: string;
type: "text" | "image";
position: number;
content: string;
image_columns: number;
image_rows: number;
images: CmsPostContentImage[];
}
export interface CmsUserSummary {
id: string;
email: string;
username: string | null;
first_name: string | null;
last_name: string | null;
full_name: string;
avatar_url: string | null;
}
export interface CmsNewsItem {
id: string;
title: string;
slug: string;
summary: string;
type: "tintuc" | "baiviettrang";
header_category_id: string;
category_ids: string[];
tagsearch_values: string[];
tag_ids: string[];
is_featured: boolean;
thumbnail: {
id: string;
name: string;
alt: string;
url: string;
} | null;
is_hidden: boolean;
created_at: string;
updated_at: string;
published_at: string;
expired_at: string;
started_at: string;
ended_at: string;
registration_deadline: string;
location: string;
participation_fee: string;
event_dates: string[];
post_content: CmsPostContentSection[];
creator: CmsUserSummary | null;
editor: CmsUserSummary | null;
}
export interface CmsHeaderCategoryItem {
id: string;
code: string;
name: string;
slug: string;
static_link: string;
sort_order: number;
type: CmsHeaderCategoryType;
is_article: boolean;
parent_id: string | null;
api_parent_id: string | null;
level: number;
category_ids: string[];
tagsearch_values: string[];
description?: string;
created_at?: string;
updated_at?: string;
}
interface CmsPagedResult<T> {
count: number;
page?: number;
pageSize?: number;
rows: T[];
}
interface CmsPageConfigNode {
id: string;
code?: string | null;
name?: string | null;
static_link?: string | null;
static_link_en?: string | null;
is_article?: boolean | null;
level?: number | null;
sort_order?: number | null;
slug?: string | null;
description?: string | null;
type?: string | null;
categories?: string[];
children?: CmsPageConfigNode[];
}
interface CmsCategoryNode extends CmsCategoryItem {
children?: CmsCategoryNode[];
}
interface CmsRawPostItem {
id?: string;
title?: string;
content?: string | null;
release_at?: string | null;
is_active?: boolean | null;
release_mode?: string | null;
slug?: string | null;
summary?: string | null;
page_config_id?: string | null;
created_by?: string | null;
updated_by?: string | null;
status?: string | null;
type?: string | null;
categories?: CmsCategoryItem[];
thumbnail?: CmsFileItem | null;
is_featured?: boolean | null;
is_hidden?: boolean | null;
created_at?: string | null;
updated_at?: string | null;
published_at?: string | null;
expired_at?: string | null;
started_at?: string | null;
ended_at?: string | null;
registration_deadline?: string | null;
location?: string | null;
participation_fee?: string | null;
event_dates?: string[] | null;
content_structure?: Record<string, unknown> | null;
creator?: CmsRawUser | null;
editor?: CmsRawUser | null;
}
interface CmsRawUser {
id?: string | null;
email?: string | null;
username?: string | null;
first_name?: string | null;
last_name?: string | null;
full_name?: string | null;
avatar_url?: string | null;
}
interface CmsPivotItem {
post_id?: string;
category_id?: string;
tag_id?: string;
created_at?: string;
}
const normalizeUser = (user: CmsRawUser | null | undefined): CmsUserSummary | null => {
if (!user || typeof user !== "object" || !user.id) return null;
const firstName = String(user.first_name ?? "").trim();
const lastName = String(user.last_name ?? "").trim();
const fullName =
String(user.full_name ?? "").trim() ||
[firstName, lastName].filter(Boolean).join(" ").trim() ||
String(user.username ?? "").trim() ||
String(user.email ?? "").trim();
return {
id: String(user.id),
email: String(user.email ?? ""),
username: user.username ? String(user.username) : null,
first_name: firstName || null,
last_name: lastName || null,
full_name: fullName,
avatar_url: user.avatar_url ? String(user.avatar_url) : null,
};
};
const normalizeDateTimeInput = (value?: string | null) => {
if (!value) return "";
return value.length >= 16 ? value.slice(0, 16) : value;
};
const toSlugFromPath = (staticLink?: string | null) => {
const normalized = (staticLink ?? "").trim();
if (!normalized || normalized === "/") return "";
const segments = normalized.split("/").filter(Boolean);
return segments.at(-1) ?? "";
};
const deriveHeaderType = (node: CmsPageConfigNode): CmsHeaderCategoryType => {
if ((node.children?.length ?? 0) > 0) return "category";
if (node.type === "news" || node.type === "page" || node.type === "category") {
return node.type;
}
return node.is_article ? "news" : "page";
};
const deriveCategoryHeaderType = (type?: string | null): CmsHeaderCategoryType => {
if (type === "category") return "category";
if (type === "news") return "news";
return "page";
};
const normalizeTagNames = (values: string[]) => {
const seen = new Set<string>();
return values
.map((value) => value.trim())
.filter((value) => {
if (!value) return false;
const key = value.toLowerCase();
if (seen.has(key)) return false;
seen.add(key);
return true;
});
};
const parsePostContent = (contentStructure?: Record<string, unknown> | null): CmsPostContentSection[] => {
const sections = Array.isArray(contentStructure?.post_content)
? (contentStructure?.post_content as Record<string, unknown>[])
: [];
return sections.map((section, index) => {
const images = Array.isArray(section.images) ? (section.images as Record<string, unknown>[]) : [];
return {
id: typeof section.id === "string" ? section.id : `section-${index + 1}`,
type: section.type === "image" ? "image" : "text",
position: typeof section.position === "number" ? section.position : index + 1,
content: typeof section.content === "string" ? section.content : "",
image_columns:
typeof section.image_columns === "number" ? section.image_columns : 2,
image_rows: typeof section.image_rows === "number" ? section.image_rows : 2,
images: images.map((image, imageIndex) => ({
position: typeof image.position === "number" ? image.position : imageIndex + 1,
caption: typeof image.caption === "string" ? image.caption : "",
image: {
id: typeof image.image === "object" && image.image && "id" in image.image
? String((image.image as Record<string, unknown>).id ?? "")
: "",
name:
typeof image.image === "object" && image.image && "name" in image.image
? String((image.image as Record<string, unknown>).name ?? "")
: "",
alt:
typeof image.image === "object" && image.image && "alt" in image.image
? String((image.image as Record<string, unknown>).alt ?? "")
: "",
url:
typeof image.image === "object" && image.image && "url" in image.image
? String((image.image as Record<string, unknown>).url ?? "")
: "",
},
})),
};
});
};
const parseLegacyPostContent = (content?: string | null): CmsPostContentSection[] => {
const normalizedContent = typeof content === "string" ? content.trim() : "";
if (!normalizedContent) {
return [];
}
return [
{
id: "legacy-content-section",
type: "text",
position: 1,
content: normalizedContent,
image_columns: 2,
image_rows: 2,
images: [],
},
];
};
const transformPost = (
post: CmsRawPostItem,
tagMap?: Map<string, CmsTagItem[]>,
): CmsNewsItem => {
const tagItems = tagMap?.get(post.id ?? "") ?? [];
const categories = Array.isArray(post.categories) ? post.categories : [];
const primaryCategory = categories[0] ?? null;
const primaryCategoryType = primaryCategory?.type ?? null;
const structuredContent = parsePostContent(post.content_structure);
const fallbackContent =
structuredContent.length > 0 ? structuredContent : parseLegacyPostContent(post.content);
return {
id: post.id ?? "",
title: post.title ?? "",
slug: post.slug ?? "",
summary: post.summary ?? "",
type:
post.type === "page" ||
primaryCategoryType === "post" ||
primaryCategoryType === "page"
? "baiviettrang"
: "tintuc",
header_category_id: primaryCategory?.id ?? "",
category_ids: categories.map((item) => item.id),
tagsearch_values: tagItems.map((item) => item.name),
tag_ids: tagItems.map((item) => item.id),
is_featured: Boolean(post.is_featured),
thumbnail: post.thumbnail?.id
? {
id: post.thumbnail.id,
name: post.thumbnail.original ?? post.thumbnail.path ?? "thumbnail",
alt: post.thumbnail.original ?? post.thumbnail.path ?? "thumbnail",
url: links.resolveImageUrl(post.thumbnail.path),
}
: null,
is_hidden: Boolean(post.is_hidden),
created_at: post.created_at ?? "",
updated_at: post.updated_at ?? "",
published_at: normalizeDateTimeInput(post.published_at ?? post.release_at),
expired_at: normalizeDateTimeInput(post.expired_at),
started_at: normalizeDateTimeInput(post.started_at),
ended_at: normalizeDateTimeInput(post.ended_at),
registration_deadline: normalizeDateTimeInput(post.registration_deadline),
location: post.location ?? "",
participation_fee: post.participation_fee ?? "",
event_dates: Array.isArray(post.event_dates)
? post.event_dates.filter((d): d is string => typeof d === "string")
: [],
post_content: fallbackContent,
creator: normalizeUser(post.creator),
editor: normalizeUser(post.editor),
};
};
async function fetchAllTagsInternal() {
const response = await getApiV10Tag({
page: 1,
pageSize: 10,
sortField: "name",
sortOrder: "asc",
});
const result = (response.responseData ?? {}) as unknown as CmsPagedResult<CmsTagItem>;
return result.rows ?? [];
}
async function fetchTagsForPost(postId: string) {
const response = await getApiV10PostTagPostId(postId);
const result = (response.responseData ?? {}) as unknown as CmsPagedResult<CmsPivotItem>;
const tagIds = (result.rows ?? []).map((item) => item.tag_id).filter(Boolean) as string[];
if (tagIds.length === 0) return [];
const tagsResponse = await postApiV10TagIds({ tag_ids: tagIds });
return (tagsResponse.responseData ?? []) as CmsTagItem[];
}
function buildHeaderItemsFromTree(
node: CmsPageConfigNode,
rootId: string,
parentId: string | null = null,
): CmsHeaderCategoryItem[] {
const rows: CmsHeaderCategoryItem[] = [];
const children = Array.isArray(node.children) ? node.children : [];
children.forEach((child) => {
rows.push({
id: child.id,
code: child.code ?? toSlugFromPath(child.static_link) ?? child.id,
name: child.name ?? "",
slug: child.slug ?? toSlugFromPath(child.static_link),
static_link: child.static_link ?? "",
sort_order: child.sort_order ?? 0,
type: deriveHeaderType(child),
is_article: Boolean(child.is_article),
parent_id: parentId,
api_parent_id: child.id ? node.id : rootId,
level: child.level ?? (parentId ? 2 : 1),
category_ids: Array.isArray(child.categories) ? child.categories : [],
tagsearch_values: [],
description: child.description ?? "",
});
rows.push(...buildHeaderItemsFromTree(child, rootId, child.id));
});
return rows;
}
function buildCategoryTree(rows: CmsCategoryItem[]) {
const nodeMap = new Map<string, CmsCategoryNode>();
const roots: CmsCategoryNode[] = [];
const sortNodes = (nodes: CmsCategoryNode[]) => {
nodes.sort((left, right) => {
const leftOrder = left.sort_order ?? Number.MAX_SAFE_INTEGER;
const rightOrder = right.sort_order ?? Number.MAX_SAFE_INTEGER;
if (leftOrder !== rightOrder) return leftOrder - rightOrder;
return left.name.localeCompare(right.name, "vi");
});
};
rows.forEach((row) => {
nodeMap.set(row.id, { ...row, children: [] });
});
rows.forEach((row) => {
const node = nodeMap.get(row.id);
if (!node) return;
if (row.parent_id) {
const parent = nodeMap.get(row.parent_id);
if (parent) {
parent.children?.push(node);
sortNodes(parent.children ?? []);
return;
}
}
roots.push(node);
});
sortNodes(roots);
return roots;
}
function buildHeaderItemsFromCategories(
nodes: CmsCategoryNode[],
parentId: string | null = null,
depth = 0,
): CmsHeaderCategoryItem[] {
return nodes.flatMap((node, index) => {
const type = deriveCategoryHeaderType(node.type);
const item: CmsHeaderCategoryItem = {
id: node.id,
code: node.slug || node.id,
name: node.name,
slug: node.slug,
static_link: node.url ?? "",
sort_order: node.sort_order ?? index + 1,
type,
is_article: type === "news",
parent_id: parentId,
api_parent_id: node.parent_id ?? null,
level: depth + 1,
category_ids: type === "category" ? [] : [node.id],
tagsearch_values: [],
description: "",
created_at: node.created_at,
updated_at: node.updated_at,
};
return [
item,
...buildHeaderItemsFromCategories(node.children ?? [], node.id, depth + 1),
];
});
}
const buildStaticLink = (slug: string, parentStaticLink?: string | null) => {
const cleanSlug = slug.trim().replace(/^\/+|\/+$/g, "");
if (!cleanSlug) {
return parentStaticLink?.trim() || "/";
}
const cleanParent = (parentStaticLink ?? "").trim().replace(/\/+$/, "");
if (!cleanParent || cleanParent === "/") {
return `/${cleanSlug}`;
}
return `${cleanParent}/${cleanSlug}`;
};
const toCategoryApiType = (type: CmsHeaderCategoryType) => {
if (type === "category") return "category";
if (type === "news") return "news";
return "page";
};
const toTagSlug = (value: string) => toCmsSlug(value);
export async function fetchCmsCategories() {
const response = await getApiV10Category({
page: 1,
pageSize: 200,
sortField: "sort_order",
sortOrder: "asc",
}).catch(() => ({
responseData: {
count: categoryFallbackRows.length,
page: 1,
pageSize: 50,
rows: categoryFallbackRows as CmsCategoryItem[],
},
}));
const result = (response.responseData ?? {}) as unknown as CmsPagedResult<CmsCategoryItem>;
return (result.rows ?? []).filter((item) => item.type !== "category");
}
export async function fetchCmsTags() {
return fetchAllTagsInternal();
}
export async function fetchCmsTagsPage(params?: {
page?: number;
pageSize?: number;
filters?: string;
}) {
const response = await getApiV10Tag({
page: params?.page ?? 1,
pageSize: params?.pageSize ?? 10,
sortField: "name",
sortOrder: "asc",
filters: params?.filters?.trim() || undefined,
});
const result = (response.responseData ?? {}) as unknown as CmsPagedResult<CmsTagItem>;
return {
items: result.rows ?? [],
total: result.count ?? 0,
page: result.page ?? params?.page ?? 1,
pageSize: result.pageSize ?? params?.pageSize ?? 10,
};
}
export async function createCmsTag(input: { name: string; slug?: string }) {
const response = await postApiV10Tag({
name: input.name.trim(),
slug: input.slug?.trim() || toTagSlug(input.name),
});
return response.responseData as CmsTagItem;
}
export async function updateCmsTag(
id: string,
input: { name: string; slug?: string },
) {
const response = await patchApiV10TagId(id, {
name: input.name.trim(),
slug: input.slug?.trim() || toTagSlug(input.name),
});
return response.responseData as CmsTagItem;
}
export async function deleteCmsTag(id: string) {
await deleteApiV10TagId(id);
}
export async function ensureTagsExist(names: string[]) {
const normalizedNames = normalizeTagNames(names);
if (normalizedNames.length === 0) return [];
const existingTags = await fetchAllTagsInternal();
const tagMap = new Map(
existingTags.map((item) => [item.name.trim().toLowerCase(), item]),
);
for (const name of normalizedNames) {
const key = name.toLowerCase();
if (tagMap.has(key)) continue;
const response = await postApiV10Tag({
name,
slug: toTagSlug(name),
});
const created = response.responseData as CmsTagItem;
tagMap.set(key, created);
}
return normalizedNames
.map((name) => tagMap.get(name.toLowerCase()))
.filter((item): item is CmsTagItem => Boolean(item));
}
export async function syncPostTags(postId: string, tagIds: string[]) {
const response = await getApiV10PostTagPostId(postId);
const result = (response.responseData ?? {}) as unknown as CmsPagedResult<CmsPivotItem>;
const currentIds = new Set(
(result.rows ?? []).map((item) => item.tag_id).filter(Boolean) as string[],
);
const nextIds = new Set(tagIds.filter(Boolean));
const toCreate = Array.from(nextIds).filter((id) => !currentIds.has(id));
const toDelete = Array.from(currentIds).filter((id) => !nextIds.has(id));
if (toCreate.length > 0) {
await postApiV10PostTagPostIdBulk(postId, { tag_ids: toCreate });
}
await Promise.all(
toDelete.map((tagId) =>
deleteApiV10PostTagPostId(postId, { tag_id: tagId }),
),
);
}
export async function fetchHeaderConfigItems() {
const response = await getApiV10Category({
page: 1,
pageSize: 200,
sortField: "sort_order",
sortOrder: "asc",
}).catch(() => ({
responseData: {
count: categoryFallbackRows.length,
page: 1,
pageSize: 50,
rows: categoryFallbackRows as CmsCategoryItem[],
},
}));
const result = (response.responseData ?? {}) as unknown as CmsPagedResult<CmsCategoryItem>;
const roots = buildCategoryTree(result.rows ?? []);
const items = buildHeaderItemsFromCategories(roots);
return {
rootId: "",
rootStaticLink: "/",
items,
};
}
export async function createHeaderConfigItem(input: {
name: string;
slug: string;
sort_order: number;
type: CmsHeaderCategoryType;
ui_parent_id?: string | null;
api_parent_id: string;
parent_static_link?: string | null;
}) {
const payload = {
name: input.name,
slug: input.slug || toTagSlug(input.name),
url: buildStaticLink(input.slug, input.parent_static_link),
sort_order: input.sort_order,
parent_id: input.api_parent_id || undefined,
type: toCategoryApiType(input.type),
};
const response = await postApiV10Category(payload);
return response.responseData as CmsCategoryItem;
}
export async function updateHeaderConfigItem(
id: string,
input: {
name: string;
slug: string;
sort_order: number;
type: CmsHeaderCategoryType;
api_parent_id: string;
parent_static_link?: string | null;
},
) {
const payload = {
name: input.name,
slug: input.slug || toTagSlug(input.name),
url: buildStaticLink(input.slug, input.parent_static_link),
sort_order: input.sort_order,
parent_id: input.api_parent_id || undefined,
type: toCategoryApiType(input.type),
};
const response = await putApiV10CategoryId(id, payload);
return response.responseData as CmsCategoryItem;
}
export async function deleteHeaderConfigItem(id: string) {
await deleteApiV10CategoryId(id);
}
export async function fetchCmsNewsItems(params?: {
page?: number;
pageSize?: number;
sortField?: string;
sortOrder?: string;
filters?: string;
}) {
const response = await getApiV10Post({
page: params?.page ?? 1,
pageSize: params?.pageSize ?? 10,
sortField: params?.sortField ?? "created_at",
sortOrder: (params?.sortOrder ?? "desc") as "asc" | "desc",
filters: params?.filters?.trim() || undefined,
});
const result = (response.responseData ?? {}) as CmsPagedResult<CmsRawPostItem>;
const rows = result.rows ?? [];
return {
items: rows.map((item) => transformPost(item)),
total: result.count ?? 0,
page: result.page ?? 1,
pageSize: result.pageSize ?? 10,
};
}
export async function fetchCmsPostCount(filters?: string) {
const response = await getApiV10Post({
page: 1,
pageSize: 1,
filters: filters?.trim() || undefined,
});
const result = (response.responseData ?? {}) as unknown as CmsPagedResult<CmsRawPostItem>;
return result.count ?? 0;
}
export async function fetchCmsNewsItem(id: string) {
const response = await getApiV10PostId(id);
const post = (response.responseData ?? {}) as CmsRawPostItem;
const tags = post.id ? await fetchTagsForPost(post.id) : [];
const tagMap = new Map<string, CmsTagItem[]>([[post.id ?? "", tags]]);
return transformPost(post, tagMap);
}
export async function createCmsNewsItem(input: {
title: string;
slug: string;
summary: string;
type: "tintuc" | "baiviettrang";
header_category_id: string;
category_ids: string[];
tag_ids: string[];
is_featured: boolean;
thumbnail_id?: string | null;
is_hidden: boolean;
published_at?: string | null;
expired_at?: string | null;
started_at?: string | null;
ended_at?: string | null;
registration_deadline?: string | null;
location?: string;
participation_fee?: string;
event_dates?: string[] | null;
post_content: CmsPostContentSection[];
}) {
const payload = {
title: input.title,
slug: input.slug,
summary: input.summary,
type: input.type === "baiviettrang" ? "page" : "news",
content: input.summary || "",
category_ids: input.category_ids,
thumbnail_id: input.thumbnail_id ?? null,
is_featured: input.is_featured,
is_hidden: input.is_hidden,
is_active: !input.is_hidden,
published_at: input.published_at || null,
expired_at: input.expired_at || null,
started_at: input.started_at || null,
ended_at: input.ended_at || null,
registration_deadline: input.registration_deadline || null,
location: input.location?.trim() || null,
participation_fee: input.participation_fee?.trim() || null,
event_dates: input.event_dates ?? null,
release_mode: input.published_at ? "SCHEDULED" : "NOW",
release_at: input.published_at || null,
content_structure: {
post_content: input.post_content,
},
};
const response = await postApiV10Post(payload as any);
const created = (response.responseData ?? {}) as CmsRawPostItem;
if (created.id) {
await syncPostTags(created.id, input.tag_ids);
}
return created;
}
export async function updateCmsNewsItem(
id: string,
input: {
title: string;
slug: string;
summary: string;
type: "tintuc" | "baiviettrang";
header_category_id: string;
category_ids: string[];
tag_ids: string[];
is_featured: boolean;
thumbnail_id?: string | null;
is_hidden: boolean;
published_at?: string | null;
expired_at?: string | null;
started_at?: string | null;
ended_at?: string | null;
registration_deadline?: string | null;
location?: string;
participation_fee?: string;
event_dates?: string[] | null;
post_content: CmsPostContentSection[];
},
) {
const payload = {
title: input.title,
slug: input.slug,
summary: input.summary,
type: input.type === "baiviettrang" ? "page" : "news",
content: input.summary || "",
category_ids: input.category_ids,
thumbnail_id: input.thumbnail_id ?? null,
is_featured: input.is_featured,
is_hidden: input.is_hidden,
is_active: !input.is_hidden,
published_at: input.published_at || null,
expired_at: input.expired_at || null,
started_at: input.started_at || null,
ended_at: input.ended_at || null,
registration_deadline: input.registration_deadline || null,
location: input.location?.trim() || null,
participation_fee: input.participation_fee?.trim() || null,
event_dates: input.event_dates ?? null,
release_mode: input.published_at ? "SCHEDULED" : "NOW",
release_at: input.published_at || null,
content_structure: {
post_content: input.post_content,
},
};
const response = await putApiV10PostId(id, payload as any);
const updated = (response.responseData ?? {}) as CmsRawPostItem;
await syncPostTags(id, input.tag_ids);
return updated;
}
export async function deleteCmsNewsItem(id: string) {
await deleteApiV10PostId(id);
}
export async function toggleCmsNewsVisibility(id: string, isHidden: boolean) {
const response = await putApiV10PostId(id, {
is_hidden: isHidden,
is_active: !isHidden,
} as any);
return (response.responseData ?? {}) as CmsRawPostItem;
}
"use client";
import { toast } from "sonner";
import useAuthStore, {
type AuthenticatedAdminSession,
type AuthenticatedAdminUser,
} from "@/store/useAuthStore";
import links from "@/links";
const AUTH_BASE_URL = `${links.apiEndpoint}/api/v1.0/auth`;
const SESSION_EXPIRED_MESSAGE = "Phiên đăng nhập đã hết hạn. Vui lòng đăng nhập lại.";
interface AuthEnvelope<T> {
message?: string | null;
message_en?: string | null;
responseData?: T;
data?: {
responseData?: T;
};
}
interface AuthErrorPayload {
message?: string | null;
message_en?: string | null;
error?: {
message?: {
vi?: string | null;
en?: string | null;
};
};
}
interface LoginResponseData {
user?: Partial<AuthenticatedAdminUser> | null;
session?: Partial<AuthenticatedAdminSession> | null;
access_token?: string | null;
refresh_token?: string | null;
expires_in?: number | null;
token_type?: string | null;
must_change_password?: boolean | null;
}
interface LoginResponseEnvelope {
responseData?: LoginResponseData;
message?: string | null;
message_en?: string | null;
}
type MeResponseData = Partial<AuthenticatedAdminUser>;
interface RefreshResponseData {
session?: Partial<AuthenticatedAdminSession> | null;
access_token?: string | null;
refresh_token?: string | null;
expires_in?: number | null;
token_type?: string | null;
}
interface RefreshResponseEnvelope {
responseData?: RefreshResponseData;
}
interface AuthRequestOptions extends RequestInit {
skipAuthHeader?: boolean;
authToken?: string | null;
noEnvelope?: boolean;
}
type AuthFailureReason = "missing_refresh_token" | "refresh_failed";
let refreshPromise: Promise<string | null> | null = null;
let forcedLogoutPromise: Promise<void> | null = null;
const ACCESS_TOKEN_EXPIRY_SKEW_SECONDS = 300;
const isObject = (value: unknown): value is Record<string, unknown> =>
typeof value === "object" && value !== null && !Array.isArray(value);
const getEnvelopeData = <T>(payload: AuthEnvelope<T>) =>
payload.responseData ?? payload.data?.responseData;
const getErrorMessage = (payload: unknown, fallback: string) => {
if (!isObject(payload)) return fallback;
const apiPayload = payload as AuthErrorPayload;
return (
apiPayload.error?.message?.vi ??
apiPayload.message ??
apiPayload.message_en ??
fallback
);
};
const normalizeUser = (user?: Partial<AuthenticatedAdminUser> | null): AuthenticatedAdminUser | null => {
if (!user?.id || !user.email || !user.username) return null;
return {
id: user.id,
email: user.email,
username: user.username,
first_name: user.first_name ?? null,
last_name: user.last_name ?? null,
roles: Array.isArray(user.roles) ? user.roles.filter((value): value is string => typeof value === "string") : [],
permissions: Array.isArray(user.permissions)
? user.permissions.filter((value): value is string => typeof value === "string")
: [],
status: user.status ?? null,
last_login_at: user.last_login_at ?? null,
must_change_password: user.must_change_password === true,
};
};
const normalizeSession = (
session?: Partial<AuthenticatedAdminSession> | null,
): AuthenticatedAdminSession | null => {
if (!session) return null;
return {
id: typeof session.id === "string" ? session.id : null,
expires_at: typeof session.expires_at === "string" ? session.expires_at : null,
refresh_expires_at:
typeof session.refresh_expires_at === "string" ? session.refresh_expires_at : null,
};
};
const getJwtExpiresAt = (token?: string | null) => {
if (!token) return null;
const [, payload] = token.split(".");
if (!payload) return null;
try {
const normalizedPayload = payload.replace(/-/g, "+").replace(/_/g, "/");
const decoded =
typeof window === "undefined"
? Buffer.from(normalizedPayload, "base64").toString("utf8")
: window.atob(normalizedPayload.padEnd(Math.ceil(normalizedPayload.length / 4) * 4, "="));
const parsed = JSON.parse(decoded) as { exp?: unknown };
const exp = typeof parsed.exp === "number" ? parsed.exp : null;
return exp ? (exp - ACCESS_TOKEN_EXPIRY_SKEW_SECONDS) * 1000 : null;
} catch {
return null;
}
};
async function requestAuth<T>(
path: string,
init?: AuthRequestOptions,
): Promise<T> {
const headers = new Headers(init?.headers);
headers.set("Content-Type", "application/json");
if (!init?.skipAuthHeader) {
const token = init?.authToken ?? useAuthStore.getState().appAccessToken;
if (token && !headers.has("Authorization")) {
headers.set("Authorization", `Bearer ${token}`);
}
}
const response = await fetch(`${AUTH_BASE_URL}${path}`, {
...init,
credentials: "include",
headers,
});
const data = (await response.json().catch(() => ({}))) as AuthEnvelope<T> & AuthErrorPayload;
if (!response.ok) {
const error = new Error(getErrorMessage(data, "Yêu cầu xác thực thất bại.")) as Error & {
status?: number;
payload?: unknown;
};
error.status = response.status;
error.payload = data;
throw error;
}
return init?.noEnvelope ? (data as T) : getEnvelopeData(data) as T;
}
const redirectToLogin = () => {
if (typeof window === "undefined") return;
const currentPath = `${window.location.pathname}${window.location.search}`;
// Không bao giờ redirect về trang đổi mật khẩu sau khi login lại
const isChangePasswordPage = currentPath.startsWith("/admin/change-password");
const redirect =
currentPath.startsWith("/admin") &&
currentPath !== "/admin/login" &&
!isChangePasswordPage
? `?redirect=${encodeURIComponent(currentPath)}`
: "";
window.location.replace(`/admin/login${redirect}`);
};
const markSessionExpiredAndNotify = () => {
const store = useAuthStore.getState();
if (!store.appSessionExpiredNotified) {
store.markSessionExpiredNotified(true);
toast.error(SESSION_EXPIRED_MESSAGE);
}
};
export async function loginAdmin(
email: string,
password: string,
options?: { persistSession?: boolean },
) {
const payload = await requestAuth<LoginResponseEnvelope>("/login", {
method: "POST",
body: JSON.stringify({
email,
password,
}),
skipAuthHeader: true,
});
const loginData = payload?.responseData ?? payload as LoginResponseData;
if (!loginData?.access_token || !loginData?.refresh_token || !loginData?.expires_in) {
throw new Error("Thiếu dữ liệu phiên đăng nhập từ API.");
}
const me = await requestAuth<MeResponseData>("/me", {
method: "GET",
authToken: loginData.access_token,
}).catch(() => loginData.user ?? null);
// Merge user data: ưu tiên permissions từ login response nếu /me trả về rỗng
const mergedUser = me
? {
...me,
// Nếu /me trả về permissions rỗng, dùng permissions từ login response
permissions: (me as { permissions?: unknown[] }).permissions?.length
? me.permissions
: loginData.user?.permissions,
// must_change_password đến từ login response (BE), không có trong /me
must_change_password: loginData.must_change_password === true,
}
: {
...loginData.user,
must_change_password: loginData.must_change_password === true,
} as Partial<AuthenticatedAdminUser>;
const normalizedUser = normalizeUser(mergedUser);
useAuthStore.getState().setAuthSession({
accessToken: loginData.access_token,
refreshToken: loginData.refresh_token,
expiresIn: loginData.expires_in,
accessTokenExpired: getJwtExpiresAt(loginData.access_token),
user: normalizedUser,
session: normalizeSession(loginData.session),
persistSession: options?.persistSession === true,
});
useAuthStore.getState().setAppUser(normalizedUser);
return loginData;
}
export async function logoutAdmin(options?: {
silent?: boolean;
redirectToLogin?: boolean;
reason?: AuthFailureReason;
}) {
const { silent = false, redirectToLogin: shouldRedirect = true, reason } = options ?? {};
if (forcedLogoutPromise) {
return forcedLogoutPromise;
}
forcedLogoutPromise = (async () => {
const store = useAuthStore.getState();
const refreshToken = store.appRefreshToken;
try {
await requestAuth("/logout", {
method: "DELETE",
});
} catch {
// Ignore logout API failure and continue clearing local auth state.
} finally {
if (reason === "refresh_failed") {
markSessionExpiredAndNotify();
}
useAuthStore.getState().resetStore();
if (reason === "refresh_failed") {
useAuthStore.getState().markSessionExpiredNotified(true);
} else if (!silent) {
toast.success("Đã đăng xuất khỏi trang quản trị");
}
if (shouldRedirect) {
redirectToLogin();
}
}
})();
try {
await forcedLogoutPromise;
} finally {
forcedLogoutPromise = null;
}
}
export async function refreshAdminAccessToken() {
if (refreshPromise) {
return refreshPromise;
}
refreshPromise = (async () => {
const store = useAuthStore.getState();
const refreshToken = store.appRefreshToken;
if (!refreshToken) {
await logoutAdmin({ silent: true, reason: "missing_refresh_token" });
return null;
}
// Refresh token đã hết hạn → không gọi API, logout ngay
if (store.appSession?.refresh_expires_at) {
const refreshExpiredAt = new Date(store.appSession.refresh_expires_at).getTime();
if (Number.isFinite(refreshExpiredAt) && refreshExpiredAt <= Date.now()) {
await logoutAdmin({ silent: true, reason: "missing_refresh_token" });
return null;
}
}
store.setAppRefreshing(true);
try {
const payload = await requestAuth<RefreshResponseEnvelope>("/refresh", {
method: "POST",
body: JSON.stringify({
refresh_token: refreshToken,
}),
skipAuthHeader: true,
});
const refreshData = payload?.responseData ?? payload as RefreshResponseData;
if (!refreshData?.access_token || !refreshData?.expires_in) {
throw new Error("Thiếu access token mới từ API.");
}
useAuthStore.getState().updateAccessToken({
accessToken: refreshData.access_token,
expiresIn: refreshData.expires_in,
accessTokenExpired: getJwtExpiresAt(refreshData.access_token),
refreshToken: refreshData.refresh_token ?? refreshToken,
session: normalizeSession(refreshData.session),
});
return refreshData.access_token;
} catch (error) {
await logoutAdmin({ silent: true, reason: "refresh_failed" });
throw error;
} finally {
useAuthStore.getState().setAppRefreshing(false);
refreshPromise = null;
}
})();
return refreshPromise;
}
export async function ensureValidAdminAccessToken() {
const store = useAuthStore.getState();
// No access token at all — try refresh if we have a usable refresh token
if (!store.appAccessToken) {
if (store.appRefreshToken && isRefreshTokenUsable(store.appSession)) {
return refreshAdminAccessToken();
}
// No usable refresh token — logout
if (store.appIsLoggedIn) {
await logoutAdmin({ silent: true, reason: "missing_refresh_token" });
}
return null;
}
// Determine expiry: prefer stored appAccessTokenExpired, fallback to JWT exp
const expiryMs =
store.appAccessTokenExpired ?? getJwtExpiresAt(store.appAccessToken);
// If we can't determine expiry, assume token is still valid (let 401 handle it)
if (expiryMs === null) {
return store.appAccessToken;
}
// Token still valid — use it
if (expiryMs > Date.now()) {
return store.appAccessToken;
}
// Token expired — need refresh; check refresh token usability first
if (!store.appRefreshToken || !isRefreshTokenUsable(store.appSession)) {
await logoutAdmin({ silent: true, reason: "missing_refresh_token" });
return null;
}
return refreshAdminAccessToken();
}
const isRefreshTokenUsable = (session: AuthenticatedAdminSession | null): boolean => {
if (!session?.refresh_expires_at) return true; // không biết → cho phép thử
const time = new Date(session.refresh_expires_at).getTime();
return Number.isFinite(time) && time > Date.now();
};
export async function handleAdminUnauthorized() {
await logoutAdmin({ silent: true, reason: "refresh_failed" });
}
export const adminSessionExpiredMessage = SESSION_EXPIRED_MESSAGE;
import type { RawUser, UserSummary } from "@/api/vcci-news/types/user";
/**
* Normalize a raw user object from the API into a UserSummary.
* Returns null if the user is missing or has no id.
*/
export function normalizeUser(user: RawUser | null | undefined): UserSummary | null {
if (!user || typeof user !== "object" || !user.id) return null;
const firstName = String(user.first_name ?? "").trim();
const lastName = String(user.last_name ?? "").trim();
const fullName =
String(user.full_name ?? "").trim() ||
[firstName, lastName].filter(Boolean).join(" ").trim() ||
String(user.username ?? "").trim() ||
String(user.email ?? "").trim();
return {
id: String(user.id),
email: String(user.email ?? ""),
username: user.username ? String(user.username) : null,
first_name: firstName || null,
last_name: lastName || null,
full_name: fullName,
avatar_url: user.avatar_url ? String(user.avatar_url) : null,
};
}
/**
* Normalize a datetime string for use in datetime-local inputs.
* Trims to 16 characters (YYYY-MM-DDTHH:mm) if longer.
*/
export function normalizeDateTimeInput(value?: string | null): string {
if (!value) return "";
return value.length >= 16 ? value.slice(0, 16) : value;
}
/**
* Build a static link from a slug and optional parent static link.
*
* Examples:
* - buildStaticLink("", "/") → "/"
* - buildStaticLink("about", "/") → "/about"
* - buildStaticLink("about", "/parent") → "/parent/about"
* - buildStaticLink("about", "/parent/") → "/parent/about"
*/
export function buildStaticLink(slug: string, parentStaticLink?: string | null): string {
const cleanSlug = slug.trim().replace(/^\/+|\/+$/g, "");
if (!cleanSlug) {
return parentStaticLink?.trim() || "/";
}
const cleanParent = (parentStaticLink ?? "").trim().replace(/\/+$/, "");
if (!cleanParent || cleanParent === "/") {
return `/${cleanSlug}`;
}
return `${cleanParent}/${cleanSlug}`;
}
import type { AdminNewsContentSection } from "@/api/vcci-news/types/post";
export function parsePostContent(
contentStructure?: Record<string, unknown> | null,
): AdminNewsContentSection[] {
const sections = Array.isArray(contentStructure?.post_content)
? (contentStructure?.post_content as Record<string, unknown>[])
: [];
return sections.map((section, index) => {
const images = Array.isArray(section.images)
? (section.images as Record<string, unknown>[])
: [];
return {
id:
typeof section.id === "string" ? section.id : `section-${index + 1}`,
type: section.type === "image" ? "image" : "text",
position:
typeof section.position === "number" ? section.position : index + 1,
content: typeof section.content === "string" ? section.content : "",
image_columns:
typeof section.image_columns === "number"
? section.image_columns
: 2,
image_rows:
typeof section.image_rows === "number" ? section.image_rows : 2,
images: images.map((image, imageIndex) => ({
position:
typeof image.position === "number" ? image.position : imageIndex + 1,
caption: typeof image.caption === "string" ? image.caption : "",
image: {
id:
typeof image.image === "object" &&
image.image &&
"id" in image.image
? String((image.image as Record<string, unknown>).id ?? "")
: "",
name:
typeof image.image === "object" &&
image.image &&
"name" in image.image
? String((image.image as Record<string, unknown>).name ?? "")
: "",
alt:
typeof image.image === "object" &&
image.image &&
"alt" in image.image
? String((image.image as Record<string, unknown>).alt ?? "")
: "",
url:
typeof image.image === "object" &&
image.image &&
"url" in image.image
? String((image.image as Record<string, unknown>).url ?? "")
: "",
},
})),
};
});
}
export function parseLegacyPostContent(
content?: string | null,
): AdminNewsContentSection[] {
const normalizedContent =
typeof content === "string" ? content.trim() : "";
if (!normalizedContent) {
return [];
}
return [
{
id: "legacy-content-section",
type: "text",
position: 1,
content: normalizedContent,
image_columns: 2,
image_rows: 2,
images: [],
},
];
}
"use client";
import { toCmsSlug } from "@/lib/utils/cms-slug";
import type { AdminNewsItem, AdminNewsFormValues, AdminNewsImageRef, AdminMediaItem } from "@/api/vcci-news/types/post";
import { ADMIN_NEWS_TYPE_OPTIONS, ADMIN_NEWS_TYPE_LABELS } from "@/api/vcci-news/types/post";
import type { AdminNewsType } from "@/api/vcci-news/types/post";
export type { AdminNewsItem, AdminNewsFormValues, AdminNewsImageRef, AdminMediaItem, AdminNewsType, AdminNewsContentSection, AdminNewsContentImage } from "@/api/vcci-news/types/post";
export { ADMIN_NEWS_TYPE_OPTIONS, ADMIN_NEWS_TYPE_LABELS } from "@/api/vcci-news/types/post";
export const ADMIN_NEWS_STORAGE_KEY = "vcci-news.admin-news.data.v3";
export const ADMIN_MEDIA_STORAGE_KEY = "vcci-news.admin-media-library.data.v1";
export const ADMIN_NEWS_TYPE_OPTIONS = [
{ value: "tintuc", label: "Tin tức" },
{ value: "baiviettrang", label: "Bài viết trang" },
] as const;
export type AdminNewsType = (typeof ADMIN_NEWS_TYPE_OPTIONS)[number]["value"];
export const ADMIN_NEWS_TYPE_LABELS: Record<AdminNewsType, string> =
ADMIN_NEWS_TYPE_OPTIONS.reduce(
(result, option) => {
result[option.value] = option.label;
return result;
},
{} as Record<AdminNewsType, string>,
);
export interface AdminMediaItem {
id: string;
name: string;
alt: string;
url: string;
mime: string;
size: number;
created_at: string;
updated_at: string;
source: "seed" | "upload";
}
export interface AdminNewsImageRef {
id: string;
name: string;
alt: string;
url: string;
}
export interface AdminNewsContentImage {
position: number;
image: AdminNewsImageRef;
caption: string;
}
export interface AdminNewsContentSection {
id: string;
type: "text" | "image";
position: number;
content: string;
image_columns: number;
image_rows: number;
images: AdminNewsContentImage[];
}
export interface AdminNewsUser {
id: string;
email: string;
username: string | null;
first_name: string | null;
last_name: string | null;
full_name: string;
avatar_url: string | null;
}
export interface AdminNewsItem {
id: string;
title: string;
slug: string;
summary: string;
type: AdminNewsType;
header_category_id: string;
category_ids: string[];
tagsearch_values: string[];
is_featured: boolean;
thumbnail: AdminNewsImageRef | null;
is_hidden: boolean;
created_at: string;
updated_at: string;
published_at: string;
expired_at: string;
started_at: string;
ended_at: string;
registration_deadline: string;
location: string;
participation_fee: string;
event_dates?: string[];
post_content: AdminNewsContentSection[];
creator?: AdminNewsUser | null;
editor?: AdminNewsUser | null;
}
export interface AdminNewsFormValues {
title: string;
slug: string;
summary: string;
type: AdminNewsType | "";
header_category_id: string;
category_ids: string[];
tagsearch_values: string[];
is_featured: boolean;
thumbnail: AdminNewsImageRef | null;
is_hidden: boolean;
created_at: string;
updated_at: string;
published_at: string;
expired_at: string;
started_at: string;
ended_at: string;
registration_deadline: string;
location: string;
participation_fee: string;
event_dates?: string[];
post_content: AdminNewsContentSection[];
}
export const EMPTY_ADMIN_NEWS_FORM: AdminNewsFormValues = {
title: "",
slug: "",
......
"use client";
export const ADVERTISEMENTS_STORAGE_KEY = "vcci-news.admin-advertisements.data.v1";
/** Loại quảng cáo: vuông (sidebar) hoặc ngang (banner full-width) */
export type AdvertisementType = "square" | "horizontal";
/** Chế độ hiển thị */
export type AdvertisementStatus = "active" | "inactive";
import type {
AdvertisementType,
AdvertisementItem,
AdvertisementFormValues,
} from "@/api/vcci-news/types/advertisement";
export interface AdvertisementItem {
id: string;
/** Tên quảng cáo */
name: string;
/** Đường dẫn ảnh (có thể là .gif) */
image: string;
/** Alt text cho ảnh */
alt: string;
/** Đường link khi bấm vào */
link: string;
/** Loại quảng cáo: vuông | ngang */
type: AdvertisementType;
/** Chế độ hiển thị */
status: AdvertisementStatus;
/** Thứ tự hiển thị (số nhỏ hiện trước) */
sortOrder: number;
/** Ngày tạo (ISO string) */
createdAt: string;
/** Tạo bởi ai */
createdBy: string;
/** Ngày sửa (ISO string) */
updatedAt: string;
/** Sửa bởi ai */
updatedBy: string;
}
export type {
AdvertisementType,
AdvertisementStatus,
AdvertisementItem,
AdvertisementFormValues,
} from "@/api/vcci-news/types/advertisement";
export interface AdvertisementFormValues {
id?: string;
name: string;
image: string;
alt: string;
link: string;
type: AdvertisementType;
status: AdvertisementStatus;
sortOrder: number;
}
export const ADVERTISEMENTS_STORAGE_KEY = "vcci-news.admin-advertisements.data.v1";
const ADVERTISEMENT_SEED: AdvertisementItem[] = [
// === Loại ngang (banner full-width giữa các section) ===
......
......@@ -2,53 +2,23 @@
import type { AdminMediaItem } from "@/mockdata/admin-news";
import { readAdminMediaItems } from "@/mockdata/admin-news";
import type {
BaseConfigLogoItem,
BaseConfigBannerItem,
BaseConfigBranchItem,
BaseConfigSocialItem,
BaseConfigData,
} from "@/api/vcci-news/types/base-config";
export const BASE_CONFIG_STORAGE_KEY = "vcci-news.admin-base-config.data.v1";
export interface BaseConfigLogoItem {
id: string;
name: string;
imageId: string;
isActive: boolean;
}
export type {
BaseConfigLogoItem,
BaseConfigBannerItem,
BaseConfigBranchItem,
BaseConfigSocialItem,
BaseConfigData,
} from "@/api/vcci-news/types/base-config";
export interface BaseConfigBannerItem {
id: string;
name: string;
imageId: string;
isActive: boolean;
displayTimeSeconds: number;
sortOrder: number;
}
export interface BaseConfigBranchItem {
id: string;
branchName: string;
address: string;
hotline: string;
email: string;
fax: string;
mapsEmbedUrl: string;
sortOrder: number;
isVisible: boolean;
}
export interface BaseConfigSocialItem {
id: string;
label: string;
url: string;
isVisible: boolean;
sortOrder: number;
}
export interface BaseConfigData {
logo: BaseConfigLogoItem | null;
banners: BaseConfigBannerItem[];
websiteName: string;
websiteLink: string;
socials: BaseConfigSocialItem[];
branches: BaseConfigBranchItem[];
}
export const BASE_CONFIG_STORAGE_KEY = "vcci-news.admin-base-config.data.v1";
export const EMPTY_BASE_CONFIG_BRANCH: BaseConfigBranchItem = {
id: "",
......
"use client";
import type {
ContactPurpose,
NewsletterSubscriptionItem,
ContactRequestItem,
MembershipApplicationItem,
} from "@/api/vcci-news/types/contact";
export type {
ContactPurpose,
NewsletterSubscriptionItem,
ContactRequestItem,
MembershipApplicationItem,
} from "@/api/vcci-news/types/contact";
export const NEWSLETTER_SUBSCRIPTIONS_STORAGE_KEY =
"vcci-news.admin-contact-management.newsletter-subscriptions.v1";
export const CONTACT_REQUESTS_STORAGE_KEY =
......@@ -7,51 +21,13 @@ export const CONTACT_REQUESTS_STORAGE_KEY =
export const MEMBERSHIP_APPLICATIONS_STORAGE_KEY =
"vcci-news.admin-contact-management.membership-applications.v1";
export const CONTACT_PURPOSE_OPTIONS = [
export const CONTACT_PURPOSE_OPTIONS: ContactPurpose[] = [
"Hội viên VCCI",
"Xuất xứ hàng hóa C/O",
"Xúc tiến thương mại",
"Quảng cáo",
"Mục đích khác",
] as const;
export type ContactPurpose = (typeof CONTACT_PURPOSE_OPTIONS)[number];
export interface NewsletterSubscriptionItem {
id: string;
email: string;
submittedAt: string;
}
export interface ContactRequestItem {
id: string;
purpose: ContactPurpose;
contactName: string;
contactPosition: string;
contactEmail: string;
contactPhone: string;
message: string;
organizationName: string;
businessField: string;
email: string;
website: string;
submittedAt: string;
}
export interface MembershipApplicationItem {
id: string;
organizationName: string;
membershipType: string;
contactName: string;
contactPosition: string;
contactEmail: string;
contactPhone: string;
address: string;
businessField: string;
website: string;
note: string;
submittedAt: string;
}
];
const NEWSLETTER_SUBSCRIPTION_SEED: NewsletterSubscriptionItem[] = [
{
......
"use client";
import { toSlug } from "@/mockdata/header-config";
import type {
HeaderCategoryPostItem,
HeaderCategoryPostFormValues,
} from "@/api/vcci-news/types/header-category-post";
export interface HeaderCategoryPostItem {
id: string;
category_id: string;
title: string;
slug: string;
excerpt: string;
content: string;
thumbnail: string;
published_at: string;
is_active: boolean;
created_at: string;
updated_at: string;
}
export type {
HeaderCategoryPostItem,
HeaderCategoryPostFormValues,
} from "@/api/vcci-news/types/header-category-post";
export const HEADER_CATEGORY_POSTS_STORAGE_KEY =
"vcci-news.header-category-posts.data.v1";
......@@ -184,17 +179,6 @@ export const headerCategoryPostSeed: HeaderCategoryPostItem[] = [
},
];
export interface HeaderCategoryPostFormValues {
id?: string;
title: string;
slug: string;
excerpt: string;
content: string;
thumbnail: string;
published_at: string;
is_active: boolean;
}
export const EMPTY_HEADER_CATEGORY_POST_FORM: HeaderCategoryPostFormValues = {
title: "",
slug: "",
......
"use client";
import { toCmsSlug } from "@/lib/utils/cms-slug";
export type HeaderCategoryType = "category" | "page" | "news";
export interface HeaderCategoryItem {
id: string;
name: string;
slug: string;
static_link: string;
sort_order: number;
type: HeaderCategoryType;
is_article: boolean;
parent_id: string | null;
level: number;
category_ids: string[];
tagsearch_values: string[];
description?: string;
created_at?: string;
updated_at?: string;
}
export interface HeaderCategoryTreeItem extends HeaderCategoryItem {
children: HeaderCategoryTreeItem[];
}
export interface HeaderArticleCategoryOption {
id: string;
name: string;
}
import type {
HeaderCategoryType,
HeaderCategoryItem,
HeaderCategoryTreeItem,
HeaderArticleCategoryOption,
} from "@/api/vcci-news/types/header-config";
export type {
HeaderCategoryType,
HeaderCategoryItem,
HeaderCategoryTreeItem,
HeaderArticleCategoryOption,
} from "@/api/vcci-news/types/header-config";
export const HEADER_CONFIG_STORAGE_KEY = "vcci-news.header-config.data.v1";
const DEFAULT_HEADER_CATEGORY_SEARCH_TAGS: Record<string, string[]> = {
"activity-news": [
"Doanh nghiệp hội viên",
"Xúc tiến thương mại",
"Chuyển đổi số",
"Kết nối giao thương",
"Bản tin nổi bật",
],
"activity-events": [
"Hội thảo",
"Đăng ký",
"Sự kiện nổi bật",
"Lịch sự kiện",
"Mời tham dự",
],
"library-highlight": [
"Album ảnh",
"Thư viện số",
"Khoảnh khắc nổi bật",
"Hình ảnh sự kiện",
],
};
export const headerCategorySeed: HeaderCategoryItem[] = [
{
id: "root-home",
name: "Trang chủ",
slug: "",
static_link: "/",
url: "/",
sort_order: 1,
type: "page",
is_article: false,
......@@ -74,7 +36,7 @@ export const headerCategorySeed: HeaderCategoryItem[] = [
id: "intro",
name: "Giới thiệu",
slug: "gioi-thieu",
static_link: "/gioi-thieu",
url: "/gioi-thieu",
sort_order: 2,
type: "category",
is_article: false,
......@@ -88,7 +50,7 @@ export const headerCategorySeed: HeaderCategoryItem[] = [
id: "intro-about",
name: "Về VCCI News",
slug: "ve-vcci-news",
static_link: "/gioi-thieu/ve-vcci-news",
url: "/gioi-thieu/ve-vcci-news",
sort_order: 1,
type: "page",
is_article: false,
......@@ -102,7 +64,7 @@ export const headerCategorySeed: HeaderCategoryItem[] = [
id: "intro-org",
name: "Cơ cấu tổ chức",
slug: "co-cau-to-chuc",
static_link: "/gioi-thieu/co-cau-to-chuc",
url: "/gioi-thieu/co-cau-to-chuc",
sort_order: 2,
type: "page",
is_article: false,
......@@ -116,7 +78,7 @@ export const headerCategorySeed: HeaderCategoryItem[] = [
id: "activity",
name: "Hoạt động",
slug: "hoat-dong",
static_link: "/hoat-dong",
url: "/hoat-dong",
sort_order: 3,
type: "category",
is_article: false,
......@@ -130,7 +92,7 @@ export const headerCategorySeed: HeaderCategoryItem[] = [
id: "activity-news",
name: "Tin tức",
slug: "tin-tuc",
static_link: "/hoat-dong/tin-tuc",
url: "/hoat-dong/tin-tuc",
sort_order: 1,
type: "news",
is_article: true,
......@@ -148,7 +110,7 @@ export const headerCategorySeed: HeaderCategoryItem[] = [
id: "activity-events",
name: "Sự kiện",
slug: "su-kien",
static_link: "/hoat-dong/su-kien",
url: "/hoat-dong/su-kien",
sort_order: 2,
type: "news",
is_article: true,
......@@ -162,7 +124,7 @@ export const headerCategorySeed: HeaderCategoryItem[] = [
id: "library",
name: "Thư viện ảnh",
slug: "thu-vien-anh",
static_link: "/thu-vien-anh",
url: "/thu-vien-anh",
sort_order: 4,
type: "category",
is_article: false,
......@@ -176,7 +138,7 @@ export const headerCategorySeed: HeaderCategoryItem[] = [
id: "library-highlight",
name: "Album nổi bật",
slug: "album-noi-bat",
static_link: "/thu-vien-anh/album-noi-bat",
url: "/thu-vien-anh/album-noi-bat",
sort_order: 1,
type: "news",
is_article: true,
......@@ -199,143 +161,6 @@ export function toSlug(value: string) {
return toCmsSlug(value);
}
function normalizeTagsearchValues(values?: string[]) {
if (!Array.isArray(values)) return [];
const seen = new Set<string>();
return values
.map((value) => value.trim())
.filter((value) => {
if (!value) return false;
const key = value.toLowerCase();
if (seen.has(key)) return false;
seen.add(key);
return true;
});
}
function getDefaultTagsearchValues(itemId: string) {
return DEFAULT_HEADER_CATEGORY_SEARCH_TAGS[itemId] ?? [];
}
function buildStaticLink(
item: Pick<HeaderCategoryItem, "slug" | "parent_id">,
items: HeaderCategoryItem[],
) {
if (!item.slug.trim()) return "/";
const segments = [item.slug.trim()];
let currentParentId = item.parent_id;
while (currentParentId) {
const parent = items.find((entry) => entry.id === currentParentId);
if (!parent) break;
if (parent.slug.trim()) {
segments.unshift(parent.slug.trim());
}
currentParentId = parent.parent_id;
}
return `/${segments.join("/")}`;
}
function assignLevel(item: HeaderCategoryItem, items: HeaderCategoryItem[]) {
let level = 1;
let currentParentId = item.parent_id;
while (currentParentId) {
const parent = items.find((entry) => entry.id === currentParentId);
if (!parent) break;
level += 1;
currentParentId = parent.parent_id;
}
return level;
}
export function normalizeHeaderCategories(items: HeaderCategoryItem[]) {
const sanitizedItems = items.map((item) => {
const normalizedType =
(item.type as unknown as string) === "image" ? "news" : item.type;
return {
...item,
type: normalizedType as HeaderCategoryType,
is_article: normalizedType === "news",
category_ids: Array.isArray(item.category_ids) ? item.category_ids : [],
tagsearch_values: normalizeTagsearchValues(item.tagsearch_values),
};
});
const parentIds = new Set(
sanitizedItems
.filter((item) => item.parent_id)
.map((item) => item.parent_id as string),
);
return sanitizedItems.map((item) => {
const next = { ...item };
if (parentIds.has(next.id)) {
next.type = "category";
next.category_ids = [];
next.tagsearch_values = [];
}
next.level = assignLevel(next, sanitizedItems);
next.static_link =
next.slug === "" && !next.parent_id ? "/" : buildStaticLink(next, sanitizedItems);
next.is_article = next.type === "news";
if (next.type !== "news") {
next.category_ids = [];
next.tagsearch_values = [];
} else if (next.tagsearch_values.length === 0) {
next.tagsearch_values = getDefaultTagsearchValues(next.id);
}
return next;
});
}
export function buildHeaderCategoryTree(items: HeaderCategoryItem[]): HeaderCategoryTreeItem[] {
const normalized = normalizeHeaderCategories(items);
const map = new Map<string, HeaderCategoryTreeItem>();
normalized.forEach((item) => {
map.set(item.id, { ...item, children: [] });
});
const roots: HeaderCategoryTreeItem[] = [];
normalized.forEach((item) => {
const current = map.get(item.id);
if (!current) return;
if (item.parent_id) {
const parent = map.get(item.parent_id);
if (parent) {
parent.children.push(current);
parent.children.sort(
(a, b) => a.sort_order - b.sort_order || a.name.localeCompare(b.name, "vi"),
);
} else {
roots.push(current);
}
} else {
roots.push(current);
}
});
return roots.sort(
(a, b) => a.sort_order - b.sort_order || a.name.localeCompare(b.name, "vi"),
);
}
export function getHeaderCategoryTypeLabel(type: HeaderCategoryType) {
switch (type) {
case "category":
......@@ -354,5 +179,5 @@ export function createHeaderCategoryId() {
}
export function getHeaderCategorySeed() {
return normalizeHeaderCategories(headerCategorySeed);
return headerCategorySeed;
}
"use client";
import type {
MemberField,
MemberRegion,
MemberImageRef,
MemberItem,
MemberFormValues,
} from "@/api/vcci-news/types/member";
export type {
MemberField,
MemberRegion,
MemberImageRef,
MemberItem,
MemberFormValues,
} from "@/api/vcci-news/types/member";
export const MEMBER_STORAGE_KEY = "vcci-news.admin-members.data.v1";
export const MEMBER_FIELD_STORAGE_KEY = "vcci-news.admin-member-fields.data.v1";
export const MEMBER_REGION_STORAGE_KEY = "vcci-news.admin-member-regions.data.v1";
export interface MemberField {
id: string;
name: string;
}
export interface MemberRegion {
id: string;
name: string;
}
export interface MemberImageRef {
id: string;
name: string;
alt: string;
url: string;
}
import type { AdminNewsContentSection } from "@/mockdata/admin-news";
export interface MemberItem {
id: string;
name: string;
is_featured: boolean;
image: MemberImageRef | null;
region_id: string;
field_id: string;
address: string;
phone: string;
fax: string;
email: string;
website: string;
introduction: AdminNewsContentSection[];
created_at: string;
updated_at: string;
}
export interface MemberFormValues {
id?: string;
name: string;
is_featured: boolean;
image: MemberImageRef | null;
region_id: string;
field_id: string;
address: string;
phone: string;
fax: string;
email: string;
website: string;
introduction: AdminNewsContentSection[];
}
// ---------------------------------------------------------------------------
// ID generators
// ---------------------------------------------------------------------------
......
"use client";
export const VIDEO_STORAGE_KEY = "vcci-news.admin-videos.data.v1";
import type { VideoItem, VideoFormValues } from "@/api/vcci-news/types/video";
export interface VideoItem {
id: string;
name: string;
url: string;
}
export type { VideoItem, VideoFormValues } from "@/api/vcci-news/types/video";
export interface VideoFormValues {
id?: string;
name: string;
url: string;
}
export const VIDEO_STORAGE_KEY = "vcci-news.admin-videos.data.v1";
const VIDEO_SEED: VideoItem[] = [
{
......
import { create } from "zustand";
import { createJSONStorage, devtools, persist } from "zustand/middleware";
export interface AuthenticatedAdminUser {
id: string;
email: string;
username: string;
first_name: string | null;
last_name: string | null;
roles: string[];
permissions: string[];
status: string | null;
last_login_at: string | null;
must_change_password?: boolean;
}
export interface AuthenticatedAdminSession {
id: string | null;
expires_at: string | null;
refresh_expires_at: string | null;
}
export interface AuthSessionPayload {
accessToken: string;
refreshToken: string;
expiresIn: number;
accessTokenExpired?: number | null;
user: AuthenticatedAdminUser | null;
session: AuthenticatedAdminSession | null;
persistSession?: boolean;
}
export interface AuthRefreshPayload {
accessToken: string;
expiresIn: number;
accessTokenExpired?: number | null;
refreshToken?: string | null;
session?: AuthenticatedAdminSession | null;
}
export interface AuthStoreStateType {
appIsLoggedIn: boolean;
appAccessToken: string | null;
appAccessTokenExpired: number | null;
appRefreshToken: string | null;
appSession: AuthenticatedAdminSession | null;
appUser: AuthenticatedAdminUser | null;
appPersistSession: boolean;
appIsRefreshing: boolean;
appSessionExpiredNotified: boolean;
appUserRemember: {
username: string;
password: string;
......@@ -55,40 +19,16 @@ export interface AuthStoreStateType {
} | null;
_hasHydrated: boolean;
setHasHydrated: (hasHydrated?: boolean) => void;
setAppIsLoggedIn: (isLoggedIn: boolean) => void;
setAuthSession: (payload: AuthSessionPayload) => void;
updateAccessToken: (payload: AuthRefreshPayload) => void;
setAppUser: (user: AuthenticatedAdminUser | null) => void;
setAppToken: (accessToken: string, accessTokenExpired: number, refreshToken?: string) => void;
setAppRefreshing: (isRefreshing: boolean) => void;
markSessionExpiredNotified: (notified: boolean) => void;
removeAppToken: () => void;
setAppUserRemember: (username: string, password: string, remember: boolean) => void;
resetStore: () => void;
}
const ACCESS_TOKEN_EXPIRY_SKEW_SECONDS = 300;
const getAccessTokenExpiredAt = (expiresIn: number) =>
Date.now() + Math.max(expiresIn - ACCESS_TOKEN_EXPIRY_SKEW_SECONDS, 0) * 1000;
const getRefreshTokenExpiredAt = (session?: AuthenticatedAdminSession | null) => {
if (!session?.refresh_expires_at) return null;
const time = new Date(session.refresh_expires_at).getTime();
return Number.isFinite(time) ? time : null;
};
const baseState = {
appIsLoggedIn: false,
appAccessToken: null,
appAccessTokenExpired: null,
appRefreshToken: null,
appSession: null,
appUser: null,
appPersistSession: false,
appIsRefreshing: false,
appSessionExpiredNotified: false,
appUserRemember: null,
_hasHydrated: false,
};
......@@ -96,53 +36,9 @@ const baseState = {
const clearSessionState = {
appIsLoggedIn: false,
appAccessToken: null,
appAccessTokenExpired: null,
appRefreshToken: null,
appSession: null,
appUser: null,
appPersistSession: false,
appIsRefreshing: false,
appSessionExpiredNotified: false,
};
const normalizePersistedAuthState = (
persistedState: unknown,
currentState: AuthStoreStateType,
) => {
const storageState =
typeof persistedState === "object" &&
persistedState !== null &&
"state" in persistedState &&
typeof (persistedState as { state?: unknown }).state === "object" &&
(persistedState as { state?: unknown }).state !== null
? (persistedState as { state: unknown }).state
: persistedState;
const persisted =
typeof storageState === "object" && storageState !== null
? (storageState as Partial<AuthStoreStateType>)
: {};
const rememberState = persisted.appUserRemember ?? currentState.appUserRemember;
const persistSession = persisted.appPersistSession === true;
const refreshTokenExpiredAt = getRefreshTokenExpiredAt(persisted.appSession ?? null);
const hasUsableSession =
Boolean(persisted.appRefreshToken) &&
(!refreshTokenExpiredAt || refreshTokenExpiredAt > Date.now());
if (!hasUsableSession) {
return {
...currentState,
...clearSessionState,
appUserRemember: rememberState,
};
}
return {
...currentState,
...persisted,
appPersistSession: persistSession,
appUserRemember: rememberState,
appIsRefreshing: false,
};
appUserRemember: null,
};
const useAuthStore = create<AuthStoreStateType>()(
......@@ -154,66 +50,13 @@ const useAuthStore = create<AuthStoreStateType>()(
set(() => ({
_hasHydrated: hasHydrated,
})),
setAppIsLoggedIn: (isLoggedIn: boolean) =>
set(() => ({
appIsLoggedIn: isLoggedIn,
})),
setAuthSession: ({
accessToken,
refreshToken,
expiresIn,
accessTokenExpired,
user,
session,
persistSession = false,
}) =>
setAuthSession: ({ accessToken, refreshToken, persistSession = false }) =>
set(() => ({
appIsLoggedIn: true,
appAccessToken: accessToken,
appAccessTokenExpired: accessTokenExpired ?? getAccessTokenExpiredAt(expiresIn),
appRefreshToken: refreshToken,
appSession: session,
appUser: user,
appPersistSession: persistSession,
appIsRefreshing: false,
appSessionExpiredNotified: false,
})),
updateAccessToken: ({ accessToken, expiresIn, accessTokenExpired, refreshToken, session }) =>
set(() => ({
appIsLoggedIn: true,
appAccessToken: accessToken,
appAccessTokenExpired: accessTokenExpired ?? getAccessTokenExpiredAt(expiresIn),
appRefreshToken: refreshToken ?? get().appRefreshToken,
appSession: session ?? get().appSession,
appIsRefreshing: false,
})),
setAppUser: (user: AuthenticatedAdminUser | null) =>
set(() => ({
appUser: user,
})),
setAppToken: (accessToken: string, accessTokenExpired: number, refreshToken?: string) =>
set(() => ({
appIsLoggedIn: true,
appAccessToken: accessToken,
appAccessTokenExpired: getAccessTokenExpiredAt(accessTokenExpired),
appRefreshToken: refreshToken ?? get().appRefreshToken,
appIsRefreshing: false,
})),
setAppRefreshing: (isRefreshing: boolean) =>
set(() => ({
appIsRefreshing: isRefreshing,
})),
markSessionExpiredNotified: (notified: boolean) =>
set(() => ({
appSessionExpiredNotified: notified,
})),
removeAppToken: () => {
set(() => ({
...clearSessionState,
appUserRemember: get().appUserRemember,
_hasHydrated: get()._hasHydrated,
}));
},
setAppUserRemember: (username, password, remember) =>
set(() => ({
appPersistSession: remember,
......@@ -270,15 +113,9 @@ const useAuthStore = create<AuthStoreStateType>()(
appPersistSession: state.appPersistSession,
appIsLoggedIn: state.appIsLoggedIn,
appAccessToken: state.appAccessToken,
appAccessTokenExpired: state.appAccessTokenExpired,
appRefreshToken: state.appRefreshToken,
appSession: state.appSession,
appUser: state.appUser,
appSessionExpiredNotified: state.appSessionExpiredNotified,
appUserRemember: state.appUserRemember,
}),
merge: (persistedState, currentState) =>
normalizePersistedAuthState(persistedState, currentState),
onRehydrateStorage: () => {
return (state: AuthStoreStateType | undefined, error: unknown) => {
if (error) {
......
import { create } from "zustand";
import { createJSONStorage, devtools, persist } from "zustand/middleware";
import useAuthStore from "@/store/useAuthStore";
export interface AuthenticatedAdminUser {
id: string;
email: string;
username: string;
first_name: string | null;
last_name: string | null;
roles: string[];
permissions: string[];
status: string | null;
last_login_at: string | null;
must_change_password?: boolean;
}
export interface UserStoreStateType {
appUser: AuthenticatedAdminUser | null;
_hasHydrated: boolean;
setHasHydrated: (hasHydrated?: boolean) => void;
setAppUser: (user: AuthenticatedAdminUser | null) => void;
clearUser: () => void;
}
const useUserStore = create<UserStoreStateType>()(
devtools(
persist(
(set) => ({
appUser: null,
_hasHydrated: false,
setHasHydrated: (hasHydrated = true) =>
set(() => ({ _hasHydrated: hasHydrated })),
setAppUser: (user: AuthenticatedAdminUser | null) =>
set(() => ({ appUser: user })),
clearUser: () =>
set(() => ({ appUser: null })),
}),
{
name: "app-user-storage",
storage: createJSONStorage(() => ({
getItem: (name) => {
if (typeof window === "undefined") return null;
return localStorage.getItem(name) ?? sessionStorage.getItem(name);
},
setItem: (name, value) => {
if (typeof window === "undefined") return;
try {
if (useAuthStore.getState().appPersistSession === true) {
localStorage.setItem(name, value);
sessionStorage.removeItem(name);
} else {
sessionStorage.setItem(name, value);
localStorage.removeItem(name);
}
} catch {
localStorage.setItem(name, value);
}
},
removeItem: (name) => {
if (typeof window === "undefined") return;
localStorage.removeItem(name);
sessionStorage.removeItem(name);
},
})),
partialize: (state) => ({ appUser: state.appUser }),
onRehydrateStorage: () => {
return (state: UserStoreStateType | undefined, error: unknown) => {
if (error) {
useUserStore.persist.clearStorage();
}
(state ?? useUserStore.getState()).setHasHydrated(true);
};
},
},
),
),
);
export default useUserStore;
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment