This commit is contained in:
2026-03-16 09:09:54 -04:00
parent e289d1a29b
commit ecf77fd105
37 changed files with 3505 additions and 119 deletions

34
AGENTS.md Normal file
View File

@@ -0,0 +1,34 @@
# Agent Context Hints
## Architecture Context
- **Backend**: Go with Gin router (legacy), SQLC for database queries, currently migrating to V1 API (oapi-codegen)
- **Frontend**: React with Vite, currently migrating from Go templates (using the V1 API)
- **API**: OpenAPI 3.0 spec, generates Go server (oapi-codegen) and TS client (orval)
## Data Flow (CRITICAL for migrations)
1. Database schema → SQL queries (`database/query.sql`, `database/query.sql.go`)
2. SQLC models → API handlers (`api/v1/*.go`)
3. Go templates show **intended UI** structure (`templates/pages/*.tmpl`)
4. API spec defines actual API contract (`api/v1/openapi.yaml`)
5. Generated TS client → React components
## When Migrating from Go Templates
- Check template AND database query results (Go templates may show fields API doesn't return)
- Template columns often map to: document_id, title, author, start_time, duration, start/end_percentage
- Go template rendering: `{{ template "component/table" }}` with "Columns" and "Keys"
## API Regeneration Commands
- Go backend: `go generate ./api/v1/generate.go`
- TS client: `cd frontend && npm run generate:api`
## Key Files
- Database queries: `database/query.sql` → SQLc Query shows actual fields returned
- SQLC models: `database/query.sql.go` → SQLc Generated Go struct definitions
- Go templates: `templates/pages/*.tmpl` → Legacy UI reference
- API spec: `api/v1/openapi.yaml` → contract definition
- Generated TS types: `frontend/src/generated/model/*.ts`
## Common Gotchas
- API implementation may not map all fields from DB query (check `api/v1/activity.go` mapping)
- `start_time` is `interface{}` in Go models, needs type assertion
- Go templates use `LOCAL_TIME()` SQL function for timezone-aware display

BIN
antholume Executable file

Binary file not shown.

View File

@@ -2,8 +2,6 @@ package v1
import ( import (
"context" "context"
"strconv"
"time"
"reichard.io/antholume/database" "reichard.io/antholume/database"
) )
@@ -48,12 +46,24 @@ func (s *Server) GetActivity(ctx context.Context, request GetActivityRequestObje
apiActivities := make([]Activity, len(activities)) apiActivities := make([]Activity, len(activities))
for i, a := range activities { for i, a := range activities {
// Convert StartTime from interface{} to string
startTimeStr := ""
if a.StartTime != nil {
if str, ok := a.StartTime.(string); ok {
startTimeStr = str
}
}
apiActivities[i] = Activity{ apiActivities[i] = Activity{
ActivityType: a.DeviceID, DocumentId: a.DocumentID,
DocumentId: a.DocumentID, DeviceId: a.DeviceID,
Id: strconv.Itoa(i), StartTime: startTimeStr,
Timestamp: time.Now(), Title: a.Title,
UserId: auth.UserName, Author: a.Author,
Duration: a.Duration,
StartPercentage: float32(a.StartPercentage),
EndPercentage: float32(a.EndPercentage),
ReadPercentage: float32(a.ReadPercentage),
} }
} }

140
api/v1/admin.go Normal file
View File

@@ -0,0 +1,140 @@
package v1
import (
"context"
"time"
)
// GET /admin
func (s *Server) GetAdmin(ctx context.Context, request GetAdminRequestObject) (GetAdminResponseObject, error) {
_, ok := s.getSessionFromContext(ctx)
if !ok {
return GetAdmin401JSONResponse{Code: 401, Message: "Unauthorized"}, nil
}
// Get database info from the main API
// This is a placeholder - you'll need to implement this in the main API or database
// For now, return empty data
response := GetAdmin200JSONResponse{
DatabaseInfo: &DatabaseInfo{
DocumentsSize: 0,
ActivitySize: 0,
ProgressSize: 0,
DevicesSize: 0,
},
}
return response, nil
}
// POST /admin
func (s *Server) PostAdminAction(ctx context.Context, request PostAdminActionRequestObject) (PostAdminActionResponseObject, error) {
_, ok := s.getSessionFromContext(ctx)
if !ok {
return PostAdminAction401JSONResponse{Code: 401, Message: "Unauthorized"}, nil
}
// TODO: Implement admin actions (backup, restore, etc.)
// For now, this is a placeholder
return PostAdminAction200ApplicationoctetStreamResponse{}, nil
}
// GET /admin/users
func (s *Server) GetUsers(ctx context.Context, request GetUsersRequestObject) (GetUsersResponseObject, error) {
_, ok := s.getSessionFromContext(ctx)
if !ok {
return GetUsers401JSONResponse{Code: 401, Message: "Unauthorized"}, nil
}
// Get users from database
users, err := s.db.Queries.GetUsers(ctx)
if err != nil {
return GetUsers500JSONResponse{Code: 500, Message: err.Error()}, nil
}
apiUsers := make([]User, len(users))
for i, user := range users {
createdAt, _ := time.Parse("2006-01-02T15:04:05", user.CreatedAt)
apiUsers[i] = User{
Id: user.ID,
Admin: user.Admin,
CreatedAt: createdAt,
}
}
response := GetUsers200JSONResponse{
Users: &apiUsers,
}
return response, nil
}
// POST /admin/users
func (s *Server) UpdateUser(ctx context.Context, request UpdateUserRequestObject) (UpdateUserResponseObject, error) {
_, ok := s.getSessionFromContext(ctx)
if !ok {
return UpdateUser401JSONResponse{Code: 401, Message: "Unauthorized"}, nil
}
// TODO: Implement user creation, update, deletion
// For now, this is a placeholder
return UpdateUser200JSONResponse{
Users: &[]User{},
}, nil
}
// GET /admin/import
func (s *Server) GetImportDirectory(ctx context.Context, request GetImportDirectoryRequestObject) (GetImportDirectoryResponseObject, error) {
_, ok := s.getSessionFromContext(ctx)
if !ok {
return GetImportDirectory401JSONResponse{Code: 401, Message: "Unauthorized"}, nil
}
// TODO: Implement directory listing
// For now, this is a placeholder
return GetImportDirectory200JSONResponse{
CurrentPath: ptrOf("/data"),
Items: &[]DirectoryItem{},
}, nil
}
// POST /admin/import
func (s *Server) PostImport(ctx context.Context, request PostImportRequestObject) (PostImportResponseObject, error) {
_, ok := s.getSessionFromContext(ctx)
if !ok {
return PostImport401JSONResponse{Code: 401, Message: "Unauthorized"}, nil
}
// TODO: Implement import functionality
// For now, this is a placeholder
return PostImport200JSONResponse{
Results: &[]ImportResult{},
}, nil
}
// GET /admin/import-results
func (s *Server) GetImportResults(ctx context.Context, request GetImportResultsRequestObject) (GetImportResultsResponseObject, error) {
_, ok := s.getSessionFromContext(ctx)
if !ok {
return GetImportResults401JSONResponse{Code: 401, Message: "Unauthorized"}, nil
}
// TODO: Implement import results retrieval
// For now, this is a placeholder
return GetImportResults200JSONResponse{
Results: &[]ImportResult{},
}, nil
}
// GET /admin/logs
func (s *Server) GetLogs(ctx context.Context, request GetLogsRequestObject) (GetLogsResponseObject, error) {
_, ok := s.getSessionFromContext(ctx)
if !ok {
return GetLogs401JSONResponse{Code: 401, Message: "Unauthorized"}, nil
}
// TODO: Implement log retrieval
// For now, this is a placeholder
return GetLogs200JSONResponse{
Logs: &[]string{},
Filter: request.Params.Filter,
}, nil
}

File diff suppressed because it is too large Load Diff

View File

@@ -91,23 +91,36 @@ components:
Activity: Activity:
type: object type: object
properties: properties:
id:
type: string
user_id:
type: string
document_id: document_id:
type: string type: string
activity_type: device_id:
type: string type: string
timestamp: start_time:
type: string type: string
format: date-time title:
type: string
author:
type: string
duration:
type: integer
format: int64
start_percentage:
type: number
format: float
end_percentage:
type: number
format: float
read_percentage:
type: number
format: float
required: required:
- id
- user_id
- document_id - document_id
- activity_type - device_id
- timestamp - start_time
- duration
- start_percentage
- end_percentage
- read_percentage
SearchItem: SearchItem:
type: object type: object
@@ -482,6 +495,95 @@ components:
- user_statistics - user_statistics
- user - user
BackupType:
type: string
enum: [COVERS, DOCUMENTS]
ImportType:
type: string
enum: [DIRECT, COPY]
OperationType:
type: string
enum: [CREATE, UPDATE, DELETE]
User:
type: object
properties:
id:
type: string
admin:
type: boolean
created_at:
type: string
format: date-time
required:
- id
- admin
- created_at
UsersResponse:
type: object
properties:
users:
type: array
items:
$ref: '#/components/schemas/User'
ImportResult:
type: object
properties:
id:
type: string
name:
type: string
path:
type: string
status:
type: string
enum: [FAILED, SUCCESS, EXISTS]
error:
type: string
ImportResultsResponse:
type: object
properties:
results:
type: array
items:
$ref: '#/components/schemas/ImportResult'
DirectoryItem:
type: object
properties:
name:
type: string
path:
type: string
DirectoryListResponse:
type: object
properties:
current_path:
type: string
items:
type: array
items:
$ref: '#/components/schemas/DirectoryItem'
LogEntry:
type: string
LogsResponse:
type: object
properties:
logs:
type: array
items:
$ref: '#/components/schemas/LogEntry'
filter:
type: string
securitySchemes: securitySchemes:
BearerAuth: BearerAuth:
type: http type: http
@@ -1058,3 +1160,306 @@ paths:
application/json: application/json:
schema: schema:
$ref: '#/components/schemas/ErrorResponse' $ref: '#/components/schemas/ErrorResponse'
/admin:
get:
summary: Get admin page data
operationId: getAdmin
tags:
- Admin
security:
- BearerAuth: []
responses:
200:
description: Successful response
content:
application/json:
schema:
type: object
properties:
database_info:
$ref: '#/components/schemas/DatabaseInfo'
401:
description: Unauthorized
content:
application/json:
schema:
$ref: '#/components/schemas/ErrorResponse'
post:
summary: Perform admin action (backup, restore, etc.)
operationId: postAdminAction
tags:
- Admin
requestBody:
required: true
content:
application/x-www-form-urlencoded:
schema:
type: object
properties:
action:
type: string
enum: [BACKUP, RESTORE, METADATA_MATCH, CACHE_TABLES]
backup_types:
type: array
items:
$ref: '#/components/schemas/BackupType'
restore_file:
type: string
format: binary
required:
- action
security:
- BearerAuth: []
responses:
200:
description: Action completed successfully
content:
application/octet-stream:
schema:
type: string
format: binary
400:
description: Bad request
content:
application/json:
schema:
$ref: '#/components/schemas/ErrorResponse'
401:
description: Unauthorized
content:
application/json:
schema:
$ref: '#/components/schemas/ErrorResponse'
500:
description: Internal server error
content:
application/json:
schema:
$ref: '#/components/schemas/ErrorResponse'
/admin/users:
get:
summary: Get all users
operationId: getUsers
tags:
- Admin
security:
- BearerAuth: []
responses:
200:
description: Successful response
content:
application/json:
schema:
$ref: '#/components/schemas/UsersResponse'
401:
description: Unauthorized
content:
application/json:
schema:
$ref: '#/components/schemas/ErrorResponse'
500:
description: Internal server error
content:
application/json:
schema:
$ref: '#/components/schemas/ErrorResponse'
post:
summary: Create, update, or delete user
operationId: updateUser
tags:
- Admin
requestBody:
required: true
content:
application/x-www-form-urlencoded:
schema:
type: object
properties:
operation:
$ref: '#/components/schemas/OperationType'
user:
type: string
password:
type: string
is_admin:
type: boolean
required:
- operation
- user
security:
- BearerAuth: []
responses:
200:
description: User updated successfully
content:
application/json:
schema:
$ref: '#/components/schemas/UsersResponse'
400:
description: Bad request
content:
application/json:
schema:
$ref: '#/components/schemas/ErrorResponse'
401:
description: Unauthorized
content:
application/json:
schema:
$ref: '#/components/schemas/ErrorResponse'
500:
description: Internal server error
content:
application/json:
schema:
$ref: '#/components/schemas/ErrorResponse'
/admin/import:
get:
summary: Get import directory list
operationId: getImportDirectory
tags:
- Admin
parameters:
- name: directory
in: query
schema:
type: string
- name: select
in: query
schema:
type: string
security:
- BearerAuth: []
responses:
200:
description: Successful response
content:
application/json:
schema:
$ref: '#/components/schemas/DirectoryListResponse'
401:
description: Unauthorized
content:
application/json:
schema:
$ref: '#/components/schemas/ErrorResponse'
500:
description: Internal server error
content:
application/json:
schema:
$ref: '#/components/schemas/ErrorResponse'
post:
summary: Perform import
operationId: postImport
tags:
- Admin
requestBody:
required: true
content:
application/x-www-form-urlencoded:
schema:
type: object
properties:
directory:
type: string
type:
$ref: '#/components/schemas/ImportType'
required:
- directory
- type
security:
- BearerAuth: []
responses:
200:
description: Import completed
content:
application/json:
schema:
$ref: '#/components/schemas/ImportResultsResponse'
400:
description: Bad request
content:
application/json:
schema:
$ref: '#/components/schemas/ErrorResponse'
401:
description: Unauthorized
content:
application/json:
schema:
$ref: '#/components/schemas/ErrorResponse'
500:
description: Internal server error
content:
application/json:
schema:
$ref: '#/components/schemas/ErrorResponse'
/admin/import-results:
get:
summary: Get import results
operationId: getImportResults
tags:
- Admin
security:
- BearerAuth: []
responses:
200:
description: Successful response
content:
application/json:
schema:
$ref: '#/components/schemas/ImportResultsResponse'
401:
description: Unauthorized
content:
application/json:
schema:
$ref: '#/components/schemas/ErrorResponse'
500:
description: Internal server error
content:
application/json:
schema:
$ref: '#/components/schemas/ErrorResponse'
/admin/logs:
get:
summary: Get logs with optional filter
operationId: getLogs
tags:
- Admin
parameters:
- name: filter
in: query
schema:
type: string
security:
- BearerAuth: []
responses:
200:
description: Successful response
content:
application/json:
schema:
$ref: '#/components/schemas/LogsResponse'
401:
description: Unauthorized
content:
application/json:
schema:
$ref: '#/components/schemas/ErrorResponse'
500:
description: Internal server error
content:
application/json:
schema:
$ref: '#/components/schemas/ErrorResponse'

View File

@@ -10,6 +10,7 @@ import SettingsPage from './pages/SettingsPage';
import LoginPage from './pages/LoginPage'; import LoginPage from './pages/LoginPage';
import AdminPage from './pages/AdminPage'; import AdminPage from './pages/AdminPage';
import AdminImportPage from './pages/AdminImportPage'; import AdminImportPage from './pages/AdminImportPage';
import AdminImportResultsPage from './pages/AdminImportResultsPage';
import AdminUsersPage from './pages/AdminUsersPage'; import AdminUsersPage from './pages/AdminUsersPage';
import AdminLogsPage from './pages/AdminLogsPage'; import AdminLogsPage from './pages/AdminLogsPage';
import { ProtectedRoute } from './auth/ProtectedRoute'; import { ProtectedRoute } from './auth/ProtectedRoute';
@@ -91,6 +92,14 @@ export function Routes() {
</ProtectedRoute> </ProtectedRoute>
} }
/> />
<Route
path="admin/import-results"
element={
<ProtectedRoute>
<AdminImportResultsPage />
</ProtectedRoute>
}
/>
<Route <Route
path="admin/users" path="admin/users"
element={ element={

View File

@@ -0,0 +1,91 @@
import React from 'react';
export interface Column<T> {
key: keyof T;
header: string;
render?: (value: any, row: T, index: number) => React.ReactNode;
className?: string;
}
export interface TableProps<T> {
columns: Column<T>[];
data: T[];
loading?: boolean;
emptyMessage?: string;
rowKey?: keyof T | ((row: T) => string);
}
export function Table<T extends Record<string, any>>({
columns,
data,
loading = false,
emptyMessage = 'No Results',
rowKey,
}: TableProps<T>) {
const getRowKey = (row: T, index: number): string => {
if (typeof rowKey === 'function') {
return rowKey(row);
}
if (rowKey) {
return String(row[rowKey] ?? index);
}
return `row-${index}`;
};
if (loading) {
return (
<div className="text-gray-500 dark:text-white p-4">Loading...</div>
);
}
return (
<div className="overflow-x-auto">
<div className="inline-block min-w-full overflow-hidden rounded shadow">
<table className="min-w-full bg-white dark:bg-gray-700">
<thead>
<tr className="border-b dark:border-gray-600">
{columns.map((column) => (
<th
key={String(column.key)}
className={`text-left p-3 text-gray-500 dark:text-white ${column.className || ''}`}
>
{column.header}
</th>
))}
</tr>
</thead>
<tbody>
{data.length === 0 ? (
<tr>
<td
colSpan={columns.length}
className="text-center p-3 text-gray-700 dark:text-gray-300"
>
{emptyMessage}
</td>
</tr>
) : (
data.map((row, index) => (
<tr
key={getRowKey(row, index)}
className="border-b dark:border-gray-600"
>
{columns.map((column) => (
<td
key={`${getRowKey(row, index)}-${String(column.key)}`}
className={`p-3 text-gray-700 dark:text-gray-300 ${column.className || ''}`}
>
{column.render
? column.render(row[column.key], row, index)
: row[column.key]}
</td>
))}
</tr>
))
)}
</tbody>
</table>
</div>
</div>
);
}

View File

@@ -34,24 +34,34 @@ import type {
import type { import type {
ActivityResponse, ActivityResponse,
CreateDocumentBody, CreateDocumentBody,
DirectoryListResponse,
DocumentResponse, DocumentResponse,
DocumentsResponse, DocumentsResponse,
ErrorResponse, ErrorResponse,
GetActivityParams, GetActivityParams,
GetAdmin200,
GetDocumentsParams, GetDocumentsParams,
GetImportDirectoryParams,
GetLogsParams,
GetProgressListParams, GetProgressListParams,
GetSearchParams, GetSearchParams,
GraphDataResponse, GraphDataResponse,
HomeResponse, HomeResponse,
ImportResultsResponse,
LoginRequest, LoginRequest,
LoginResponse, LoginResponse,
LogsResponse,
PostAdminActionBody,
PostImportBody,
PostSearchBody, PostSearchBody,
ProgressListResponse, ProgressListResponse,
ProgressResponse, ProgressResponse,
SearchResponse, SearchResponse,
SettingsResponse, SettingsResponse,
StreaksResponse, StreaksResponse,
UserStatisticsResponse UpdateUserBody,
UserStatisticsResponse,
UsersResponse
} from './model'; } from './model';
@@ -1412,3 +1422,670 @@ export const usePostSearch = <TError = AxiosError<ErrorResponse>,
return useMutation(mutationOptions, queryClient); return useMutation(mutationOptions, queryClient);
} }
/**
* @summary Get admin page data
*/
export const getAdmin = (
options?: AxiosRequestConfig
): Promise<AxiosResponse<GetAdmin200>> => {
return axios.default.get(
`/api/v1/admin`,options
);
}
export const getGetAdminQueryKey = () => {
return [
`/api/v1/admin`
] as const;
}
export const getGetAdminQueryOptions = <TData = Awaited<ReturnType<typeof getAdmin>>, TError = AxiosError<ErrorResponse>>( options?: { query?:Partial<UseQueryOptions<Awaited<ReturnType<typeof getAdmin>>, TError, TData>>, axios?: AxiosRequestConfig}
) => {
const {query: queryOptions, axios: axiosOptions} = options ?? {};
const queryKey = queryOptions?.queryKey ?? getGetAdminQueryKey();
const queryFn: QueryFunction<Awaited<ReturnType<typeof getAdmin>>> = ({ signal }) => getAdmin({ signal, ...axiosOptions });
return { queryKey, queryFn, ...queryOptions} as UseQueryOptions<Awaited<ReturnType<typeof getAdmin>>, TError, TData> & { queryKey: DataTag<QueryKey, TData, TError> }
}
export type GetAdminQueryResult = NonNullable<Awaited<ReturnType<typeof getAdmin>>>
export type GetAdminQueryError = AxiosError<ErrorResponse>
export function useGetAdmin<TData = Awaited<ReturnType<typeof getAdmin>>, TError = AxiosError<ErrorResponse>>(
options: { query:Partial<UseQueryOptions<Awaited<ReturnType<typeof getAdmin>>, TError, TData>> & Pick<
DefinedInitialDataOptions<
Awaited<ReturnType<typeof getAdmin>>,
TError,
Awaited<ReturnType<typeof getAdmin>>
> , 'initialData'
>, axios?: AxiosRequestConfig}
, queryClient?: QueryClient
): DefinedUseQueryResult<TData, TError> & { queryKey: DataTag<QueryKey, TData, TError> }
export function useGetAdmin<TData = Awaited<ReturnType<typeof getAdmin>>, TError = AxiosError<ErrorResponse>>(
options?: { query?:Partial<UseQueryOptions<Awaited<ReturnType<typeof getAdmin>>, TError, TData>> & Pick<
UndefinedInitialDataOptions<
Awaited<ReturnType<typeof getAdmin>>,
TError,
Awaited<ReturnType<typeof getAdmin>>
> , 'initialData'
>, axios?: AxiosRequestConfig}
, queryClient?: QueryClient
): UseQueryResult<TData, TError> & { queryKey: DataTag<QueryKey, TData, TError> }
export function useGetAdmin<TData = Awaited<ReturnType<typeof getAdmin>>, TError = AxiosError<ErrorResponse>>(
options?: { query?:Partial<UseQueryOptions<Awaited<ReturnType<typeof getAdmin>>, TError, TData>>, axios?: AxiosRequestConfig}
, queryClient?: QueryClient
): UseQueryResult<TData, TError> & { queryKey: DataTag<QueryKey, TData, TError> }
/**
* @summary Get admin page data
*/
export function useGetAdmin<TData = Awaited<ReturnType<typeof getAdmin>>, TError = AxiosError<ErrorResponse>>(
options?: { query?:Partial<UseQueryOptions<Awaited<ReturnType<typeof getAdmin>>, TError, TData>>, axios?: AxiosRequestConfig}
, queryClient?: QueryClient
): UseQueryResult<TData, TError> & { queryKey: DataTag<QueryKey, TData, TError> } {
const queryOptions = getGetAdminQueryOptions(options)
const query = useQuery(queryOptions, queryClient) as UseQueryResult<TData, TError> & { queryKey: DataTag<QueryKey, TData, TError> };
query.queryKey = queryOptions.queryKey ;
return query;
}
/**
* @summary Perform admin action (backup, restore, etc.)
*/
export const postAdminAction = (
postAdminActionBody: PostAdminActionBody, options?: AxiosRequestConfig
): Promise<AxiosResponse<Blob>> => {
const formUrlEncoded = new URLSearchParams();
formUrlEncoded.append(`action`, postAdminActionBody.action)
if(postAdminActionBody.backup_types !== undefined) {
postAdminActionBody.backup_types.forEach(value => formUrlEncoded.append(`backup_types`, value));
}
if(postAdminActionBody.restore_file !== undefined) {
formUrlEncoded.append(`restore_file`, postAdminActionBody.restore_file)
}
return axios.default.post(
`/api/v1/admin`,
formUrlEncoded,{
responseType: 'blob',
...options,}
);
}
export const getPostAdminActionMutationOptions = <TError = AxiosError<ErrorResponse>,
TContext = unknown>(options?: { mutation?:UseMutationOptions<Awaited<ReturnType<typeof postAdminAction>>, TError,{data: PostAdminActionBody}, TContext>, axios?: AxiosRequestConfig}
): UseMutationOptions<Awaited<ReturnType<typeof postAdminAction>>, TError,{data: PostAdminActionBody}, TContext> => {
const mutationKey = ['postAdminAction'];
const {mutation: mutationOptions, axios: axiosOptions} = options ?
options.mutation && 'mutationKey' in options.mutation && options.mutation.mutationKey ?
options
: {...options, mutation: {...options.mutation, mutationKey}}
: {mutation: { mutationKey, }, axios: undefined};
const mutationFn: MutationFunction<Awaited<ReturnType<typeof postAdminAction>>, {data: PostAdminActionBody}> = (props) => {
const {data} = props ?? {};
return postAdminAction(data,axiosOptions)
}
return { mutationFn, ...mutationOptions }}
export type PostAdminActionMutationResult = NonNullable<Awaited<ReturnType<typeof postAdminAction>>>
export type PostAdminActionMutationBody = PostAdminActionBody
export type PostAdminActionMutationError = AxiosError<ErrorResponse>
/**
* @summary Perform admin action (backup, restore, etc.)
*/
export const usePostAdminAction = <TError = AxiosError<ErrorResponse>,
TContext = unknown>(options?: { mutation?:UseMutationOptions<Awaited<ReturnType<typeof postAdminAction>>, TError,{data: PostAdminActionBody}, TContext>, axios?: AxiosRequestConfig}
, queryClient?: QueryClient): UseMutationResult<
Awaited<ReturnType<typeof postAdminAction>>,
TError,
{data: PostAdminActionBody},
TContext
> => {
const mutationOptions = getPostAdminActionMutationOptions(options);
return useMutation(mutationOptions, queryClient);
}
/**
* @summary Get all users
*/
export const getUsers = (
options?: AxiosRequestConfig
): Promise<AxiosResponse<UsersResponse>> => {
return axios.default.get(
`/api/v1/admin/users`,options
);
}
export const getGetUsersQueryKey = () => {
return [
`/api/v1/admin/users`
] as const;
}
export const getGetUsersQueryOptions = <TData = Awaited<ReturnType<typeof getUsers>>, TError = AxiosError<ErrorResponse>>( options?: { query?:Partial<UseQueryOptions<Awaited<ReturnType<typeof getUsers>>, TError, TData>>, axios?: AxiosRequestConfig}
) => {
const {query: queryOptions, axios: axiosOptions} = options ?? {};
const queryKey = queryOptions?.queryKey ?? getGetUsersQueryKey();
const queryFn: QueryFunction<Awaited<ReturnType<typeof getUsers>>> = ({ signal }) => getUsers({ signal, ...axiosOptions });
return { queryKey, queryFn, ...queryOptions} as UseQueryOptions<Awaited<ReturnType<typeof getUsers>>, TError, TData> & { queryKey: DataTag<QueryKey, TData, TError> }
}
export type GetUsersQueryResult = NonNullable<Awaited<ReturnType<typeof getUsers>>>
export type GetUsersQueryError = AxiosError<ErrorResponse>
export function useGetUsers<TData = Awaited<ReturnType<typeof getUsers>>, TError = AxiosError<ErrorResponse>>(
options: { query:Partial<UseQueryOptions<Awaited<ReturnType<typeof getUsers>>, TError, TData>> & Pick<
DefinedInitialDataOptions<
Awaited<ReturnType<typeof getUsers>>,
TError,
Awaited<ReturnType<typeof getUsers>>
> , 'initialData'
>, axios?: AxiosRequestConfig}
, queryClient?: QueryClient
): DefinedUseQueryResult<TData, TError> & { queryKey: DataTag<QueryKey, TData, TError> }
export function useGetUsers<TData = Awaited<ReturnType<typeof getUsers>>, TError = AxiosError<ErrorResponse>>(
options?: { query?:Partial<UseQueryOptions<Awaited<ReturnType<typeof getUsers>>, TError, TData>> & Pick<
UndefinedInitialDataOptions<
Awaited<ReturnType<typeof getUsers>>,
TError,
Awaited<ReturnType<typeof getUsers>>
> , 'initialData'
>, axios?: AxiosRequestConfig}
, queryClient?: QueryClient
): UseQueryResult<TData, TError> & { queryKey: DataTag<QueryKey, TData, TError> }
export function useGetUsers<TData = Awaited<ReturnType<typeof getUsers>>, TError = AxiosError<ErrorResponse>>(
options?: { query?:Partial<UseQueryOptions<Awaited<ReturnType<typeof getUsers>>, TError, TData>>, axios?: AxiosRequestConfig}
, queryClient?: QueryClient
): UseQueryResult<TData, TError> & { queryKey: DataTag<QueryKey, TData, TError> }
/**
* @summary Get all users
*/
export function useGetUsers<TData = Awaited<ReturnType<typeof getUsers>>, TError = AxiosError<ErrorResponse>>(
options?: { query?:Partial<UseQueryOptions<Awaited<ReturnType<typeof getUsers>>, TError, TData>>, axios?: AxiosRequestConfig}
, queryClient?: QueryClient
): UseQueryResult<TData, TError> & { queryKey: DataTag<QueryKey, TData, TError> } {
const queryOptions = getGetUsersQueryOptions(options)
const query = useQuery(queryOptions, queryClient) as UseQueryResult<TData, TError> & { queryKey: DataTag<QueryKey, TData, TError> };
query.queryKey = queryOptions.queryKey ;
return query;
}
/**
* @summary Create, update, or delete user
*/
export const updateUser = (
updateUserBody: UpdateUserBody, options?: AxiosRequestConfig
): Promise<AxiosResponse<UsersResponse>> => {
const formUrlEncoded = new URLSearchParams();
formUrlEncoded.append(`operation`, updateUserBody.operation)
formUrlEncoded.append(`user`, updateUserBody.user)
if(updateUserBody.password !== undefined) {
formUrlEncoded.append(`password`, updateUserBody.password)
}
if(updateUserBody.is_admin !== undefined) {
formUrlEncoded.append(`is_admin`, updateUserBody.is_admin.toString())
}
return axios.default.post(
`/api/v1/admin/users`,
formUrlEncoded,options
);
}
export const getUpdateUserMutationOptions = <TError = AxiosError<ErrorResponse>,
TContext = unknown>(options?: { mutation?:UseMutationOptions<Awaited<ReturnType<typeof updateUser>>, TError,{data: UpdateUserBody}, TContext>, axios?: AxiosRequestConfig}
): UseMutationOptions<Awaited<ReturnType<typeof updateUser>>, TError,{data: UpdateUserBody}, TContext> => {
const mutationKey = ['updateUser'];
const {mutation: mutationOptions, axios: axiosOptions} = options ?
options.mutation && 'mutationKey' in options.mutation && options.mutation.mutationKey ?
options
: {...options, mutation: {...options.mutation, mutationKey}}
: {mutation: { mutationKey, }, axios: undefined};
const mutationFn: MutationFunction<Awaited<ReturnType<typeof updateUser>>, {data: UpdateUserBody}> = (props) => {
const {data} = props ?? {};
return updateUser(data,axiosOptions)
}
return { mutationFn, ...mutationOptions }}
export type UpdateUserMutationResult = NonNullable<Awaited<ReturnType<typeof updateUser>>>
export type UpdateUserMutationBody = UpdateUserBody
export type UpdateUserMutationError = AxiosError<ErrorResponse>
/**
* @summary Create, update, or delete user
*/
export const useUpdateUser = <TError = AxiosError<ErrorResponse>,
TContext = unknown>(options?: { mutation?:UseMutationOptions<Awaited<ReturnType<typeof updateUser>>, TError,{data: UpdateUserBody}, TContext>, axios?: AxiosRequestConfig}
, queryClient?: QueryClient): UseMutationResult<
Awaited<ReturnType<typeof updateUser>>,
TError,
{data: UpdateUserBody},
TContext
> => {
const mutationOptions = getUpdateUserMutationOptions(options);
return useMutation(mutationOptions, queryClient);
}
/**
* @summary Get import directory list
*/
export const getImportDirectory = (
params?: GetImportDirectoryParams, options?: AxiosRequestConfig
): Promise<AxiosResponse<DirectoryListResponse>> => {
return axios.default.get(
`/api/v1/admin/import`,{
...options,
params: {...params, ...options?.params},}
);
}
export const getGetImportDirectoryQueryKey = (params?: GetImportDirectoryParams,) => {
return [
`/api/v1/admin/import`, ...(params ? [params]: [])
] as const;
}
export const getGetImportDirectoryQueryOptions = <TData = Awaited<ReturnType<typeof getImportDirectory>>, TError = AxiosError<ErrorResponse>>(params?: GetImportDirectoryParams, options?: { query?:Partial<UseQueryOptions<Awaited<ReturnType<typeof getImportDirectory>>, TError, TData>>, axios?: AxiosRequestConfig}
) => {
const {query: queryOptions, axios: axiosOptions} = options ?? {};
const queryKey = queryOptions?.queryKey ?? getGetImportDirectoryQueryKey(params);
const queryFn: QueryFunction<Awaited<ReturnType<typeof getImportDirectory>>> = ({ signal }) => getImportDirectory(params, { signal, ...axiosOptions });
return { queryKey, queryFn, ...queryOptions} as UseQueryOptions<Awaited<ReturnType<typeof getImportDirectory>>, TError, TData> & { queryKey: DataTag<QueryKey, TData, TError> }
}
export type GetImportDirectoryQueryResult = NonNullable<Awaited<ReturnType<typeof getImportDirectory>>>
export type GetImportDirectoryQueryError = AxiosError<ErrorResponse>
export function useGetImportDirectory<TData = Awaited<ReturnType<typeof getImportDirectory>>, TError = AxiosError<ErrorResponse>>(
params: undefined | GetImportDirectoryParams, options: { query:Partial<UseQueryOptions<Awaited<ReturnType<typeof getImportDirectory>>, TError, TData>> & Pick<
DefinedInitialDataOptions<
Awaited<ReturnType<typeof getImportDirectory>>,
TError,
Awaited<ReturnType<typeof getImportDirectory>>
> , 'initialData'
>, axios?: AxiosRequestConfig}
, queryClient?: QueryClient
): DefinedUseQueryResult<TData, TError> & { queryKey: DataTag<QueryKey, TData, TError> }
export function useGetImportDirectory<TData = Awaited<ReturnType<typeof getImportDirectory>>, TError = AxiosError<ErrorResponse>>(
params?: GetImportDirectoryParams, options?: { query?:Partial<UseQueryOptions<Awaited<ReturnType<typeof getImportDirectory>>, TError, TData>> & Pick<
UndefinedInitialDataOptions<
Awaited<ReturnType<typeof getImportDirectory>>,
TError,
Awaited<ReturnType<typeof getImportDirectory>>
> , 'initialData'
>, axios?: AxiosRequestConfig}
, queryClient?: QueryClient
): UseQueryResult<TData, TError> & { queryKey: DataTag<QueryKey, TData, TError> }
export function useGetImportDirectory<TData = Awaited<ReturnType<typeof getImportDirectory>>, TError = AxiosError<ErrorResponse>>(
params?: GetImportDirectoryParams, options?: { query?:Partial<UseQueryOptions<Awaited<ReturnType<typeof getImportDirectory>>, TError, TData>>, axios?: AxiosRequestConfig}
, queryClient?: QueryClient
): UseQueryResult<TData, TError> & { queryKey: DataTag<QueryKey, TData, TError> }
/**
* @summary Get import directory list
*/
export function useGetImportDirectory<TData = Awaited<ReturnType<typeof getImportDirectory>>, TError = AxiosError<ErrorResponse>>(
params?: GetImportDirectoryParams, options?: { query?:Partial<UseQueryOptions<Awaited<ReturnType<typeof getImportDirectory>>, TError, TData>>, axios?: AxiosRequestConfig}
, queryClient?: QueryClient
): UseQueryResult<TData, TError> & { queryKey: DataTag<QueryKey, TData, TError> } {
const queryOptions = getGetImportDirectoryQueryOptions(params,options)
const query = useQuery(queryOptions, queryClient) as UseQueryResult<TData, TError> & { queryKey: DataTag<QueryKey, TData, TError> };
query.queryKey = queryOptions.queryKey ;
return query;
}
/**
* @summary Perform import
*/
export const postImport = (
postImportBody: PostImportBody, options?: AxiosRequestConfig
): Promise<AxiosResponse<ImportResultsResponse>> => {
const formUrlEncoded = new URLSearchParams();
formUrlEncoded.append(`directory`, postImportBody.directory)
formUrlEncoded.append(`type`, postImportBody.type)
return axios.default.post(
`/api/v1/admin/import`,
formUrlEncoded,options
);
}
export const getPostImportMutationOptions = <TError = AxiosError<ErrorResponse>,
TContext = unknown>(options?: { mutation?:UseMutationOptions<Awaited<ReturnType<typeof postImport>>, TError,{data: PostImportBody}, TContext>, axios?: AxiosRequestConfig}
): UseMutationOptions<Awaited<ReturnType<typeof postImport>>, TError,{data: PostImportBody}, TContext> => {
const mutationKey = ['postImport'];
const {mutation: mutationOptions, axios: axiosOptions} = options ?
options.mutation && 'mutationKey' in options.mutation && options.mutation.mutationKey ?
options
: {...options, mutation: {...options.mutation, mutationKey}}
: {mutation: { mutationKey, }, axios: undefined};
const mutationFn: MutationFunction<Awaited<ReturnType<typeof postImport>>, {data: PostImportBody}> = (props) => {
const {data} = props ?? {};
return postImport(data,axiosOptions)
}
return { mutationFn, ...mutationOptions }}
export type PostImportMutationResult = NonNullable<Awaited<ReturnType<typeof postImport>>>
export type PostImportMutationBody = PostImportBody
export type PostImportMutationError = AxiosError<ErrorResponse>
/**
* @summary Perform import
*/
export const usePostImport = <TError = AxiosError<ErrorResponse>,
TContext = unknown>(options?: { mutation?:UseMutationOptions<Awaited<ReturnType<typeof postImport>>, TError,{data: PostImportBody}, TContext>, axios?: AxiosRequestConfig}
, queryClient?: QueryClient): UseMutationResult<
Awaited<ReturnType<typeof postImport>>,
TError,
{data: PostImportBody},
TContext
> => {
const mutationOptions = getPostImportMutationOptions(options);
return useMutation(mutationOptions, queryClient);
}
/**
* @summary Get import results
*/
export const getImportResults = (
options?: AxiosRequestConfig
): Promise<AxiosResponse<ImportResultsResponse>> => {
return axios.default.get(
`/api/v1/admin/import-results`,options
);
}
export const getGetImportResultsQueryKey = () => {
return [
`/api/v1/admin/import-results`
] as const;
}
export const getGetImportResultsQueryOptions = <TData = Awaited<ReturnType<typeof getImportResults>>, TError = AxiosError<ErrorResponse>>( options?: { query?:Partial<UseQueryOptions<Awaited<ReturnType<typeof getImportResults>>, TError, TData>>, axios?: AxiosRequestConfig}
) => {
const {query: queryOptions, axios: axiosOptions} = options ?? {};
const queryKey = queryOptions?.queryKey ?? getGetImportResultsQueryKey();
const queryFn: QueryFunction<Awaited<ReturnType<typeof getImportResults>>> = ({ signal }) => getImportResults({ signal, ...axiosOptions });
return { queryKey, queryFn, ...queryOptions} as UseQueryOptions<Awaited<ReturnType<typeof getImportResults>>, TError, TData> & { queryKey: DataTag<QueryKey, TData, TError> }
}
export type GetImportResultsQueryResult = NonNullable<Awaited<ReturnType<typeof getImportResults>>>
export type GetImportResultsQueryError = AxiosError<ErrorResponse>
export function useGetImportResults<TData = Awaited<ReturnType<typeof getImportResults>>, TError = AxiosError<ErrorResponse>>(
options: { query:Partial<UseQueryOptions<Awaited<ReturnType<typeof getImportResults>>, TError, TData>> & Pick<
DefinedInitialDataOptions<
Awaited<ReturnType<typeof getImportResults>>,
TError,
Awaited<ReturnType<typeof getImportResults>>
> , 'initialData'
>, axios?: AxiosRequestConfig}
, queryClient?: QueryClient
): DefinedUseQueryResult<TData, TError> & { queryKey: DataTag<QueryKey, TData, TError> }
export function useGetImportResults<TData = Awaited<ReturnType<typeof getImportResults>>, TError = AxiosError<ErrorResponse>>(
options?: { query?:Partial<UseQueryOptions<Awaited<ReturnType<typeof getImportResults>>, TError, TData>> & Pick<
UndefinedInitialDataOptions<
Awaited<ReturnType<typeof getImportResults>>,
TError,
Awaited<ReturnType<typeof getImportResults>>
> , 'initialData'
>, axios?: AxiosRequestConfig}
, queryClient?: QueryClient
): UseQueryResult<TData, TError> & { queryKey: DataTag<QueryKey, TData, TError> }
export function useGetImportResults<TData = Awaited<ReturnType<typeof getImportResults>>, TError = AxiosError<ErrorResponse>>(
options?: { query?:Partial<UseQueryOptions<Awaited<ReturnType<typeof getImportResults>>, TError, TData>>, axios?: AxiosRequestConfig}
, queryClient?: QueryClient
): UseQueryResult<TData, TError> & { queryKey: DataTag<QueryKey, TData, TError> }
/**
* @summary Get import results
*/
export function useGetImportResults<TData = Awaited<ReturnType<typeof getImportResults>>, TError = AxiosError<ErrorResponse>>(
options?: { query?:Partial<UseQueryOptions<Awaited<ReturnType<typeof getImportResults>>, TError, TData>>, axios?: AxiosRequestConfig}
, queryClient?: QueryClient
): UseQueryResult<TData, TError> & { queryKey: DataTag<QueryKey, TData, TError> } {
const queryOptions = getGetImportResultsQueryOptions(options)
const query = useQuery(queryOptions, queryClient) as UseQueryResult<TData, TError> & { queryKey: DataTag<QueryKey, TData, TError> };
query.queryKey = queryOptions.queryKey ;
return query;
}
/**
* @summary Get logs with optional filter
*/
export const getLogs = (
params?: GetLogsParams, options?: AxiosRequestConfig
): Promise<AxiosResponse<LogsResponse>> => {
return axios.default.get(
`/api/v1/admin/logs`,{
...options,
params: {...params, ...options?.params},}
);
}
export const getGetLogsQueryKey = (params?: GetLogsParams,) => {
return [
`/api/v1/admin/logs`, ...(params ? [params]: [])
] as const;
}
export const getGetLogsQueryOptions = <TData = Awaited<ReturnType<typeof getLogs>>, TError = AxiosError<ErrorResponse>>(params?: GetLogsParams, options?: { query?:Partial<UseQueryOptions<Awaited<ReturnType<typeof getLogs>>, TError, TData>>, axios?: AxiosRequestConfig}
) => {
const {query: queryOptions, axios: axiosOptions} = options ?? {};
const queryKey = queryOptions?.queryKey ?? getGetLogsQueryKey(params);
const queryFn: QueryFunction<Awaited<ReturnType<typeof getLogs>>> = ({ signal }) => getLogs(params, { signal, ...axiosOptions });
return { queryKey, queryFn, ...queryOptions} as UseQueryOptions<Awaited<ReturnType<typeof getLogs>>, TError, TData> & { queryKey: DataTag<QueryKey, TData, TError> }
}
export type GetLogsQueryResult = NonNullable<Awaited<ReturnType<typeof getLogs>>>
export type GetLogsQueryError = AxiosError<ErrorResponse>
export function useGetLogs<TData = Awaited<ReturnType<typeof getLogs>>, TError = AxiosError<ErrorResponse>>(
params: undefined | GetLogsParams, options: { query:Partial<UseQueryOptions<Awaited<ReturnType<typeof getLogs>>, TError, TData>> & Pick<
DefinedInitialDataOptions<
Awaited<ReturnType<typeof getLogs>>,
TError,
Awaited<ReturnType<typeof getLogs>>
> , 'initialData'
>, axios?: AxiosRequestConfig}
, queryClient?: QueryClient
): DefinedUseQueryResult<TData, TError> & { queryKey: DataTag<QueryKey, TData, TError> }
export function useGetLogs<TData = Awaited<ReturnType<typeof getLogs>>, TError = AxiosError<ErrorResponse>>(
params?: GetLogsParams, options?: { query?:Partial<UseQueryOptions<Awaited<ReturnType<typeof getLogs>>, TError, TData>> & Pick<
UndefinedInitialDataOptions<
Awaited<ReturnType<typeof getLogs>>,
TError,
Awaited<ReturnType<typeof getLogs>>
> , 'initialData'
>, axios?: AxiosRequestConfig}
, queryClient?: QueryClient
): UseQueryResult<TData, TError> & { queryKey: DataTag<QueryKey, TData, TError> }
export function useGetLogs<TData = Awaited<ReturnType<typeof getLogs>>, TError = AxiosError<ErrorResponse>>(
params?: GetLogsParams, options?: { query?:Partial<UseQueryOptions<Awaited<ReturnType<typeof getLogs>>, TError, TData>>, axios?: AxiosRequestConfig}
, queryClient?: QueryClient
): UseQueryResult<TData, TError> & { queryKey: DataTag<QueryKey, TData, TError> }
/**
* @summary Get logs with optional filter
*/
export function useGetLogs<TData = Awaited<ReturnType<typeof getLogs>>, TError = AxiosError<ErrorResponse>>(
params?: GetLogsParams, options?: { query?:Partial<UseQueryOptions<Awaited<ReturnType<typeof getLogs>>, TError, TData>>, axios?: AxiosRequestConfig}
, queryClient?: QueryClient
): UseQueryResult<TData, TError> & { queryKey: DataTag<QueryKey, TData, TError> } {
const queryOptions = getGetLogsQueryOptions(params,options)
const query = useQuery(queryOptions, queryClient) as UseQueryResult<TData, TError> & { queryKey: DataTag<QueryKey, TData, TError> };
query.queryKey = queryOptions.queryKey ;
return query;
}

View File

@@ -7,9 +7,13 @@
*/ */
export interface Activity { export interface Activity {
id: string;
user_id: string;
document_id: string; document_id: string;
activity_type: string; device_id: string;
timestamp: string; start_time: string;
title?: string;
author?: string;
duration: number;
start_percentage: number;
end_percentage: number;
read_percentage: number;
} }

View File

@@ -0,0 +1,16 @@
/**
* Generated by orval v7.21.0 🍺
* Do not edit manually.
* AnthoLume API v1
* REST API for AnthoLume document management system
* OpenAPI spec version: 1.0.0
*/
export type BackupType = typeof BackupType[keyof typeof BackupType];
// eslint-disable-next-line @typescript-eslint/no-redeclare
export const BackupType = {
COVERS: 'COVERS',
DOCUMENTS: 'DOCUMENTS',
} as const;

View File

@@ -0,0 +1,12 @@
/**
* Generated by orval v7.21.0 🍺
* Do not edit manually.
* AnthoLume API v1
* REST API for AnthoLume document management system
* OpenAPI spec version: 1.0.0
*/
export interface DirectoryItem {
name?: string;
path?: string;
}

View File

@@ -0,0 +1,13 @@
/**
* Generated by orval v7.21.0 🍺
* Do not edit manually.
* AnthoLume API v1
* REST API for AnthoLume document management system
* OpenAPI spec version: 1.0.0
*/
import type { DirectoryItem } from './directoryItem';
export interface DirectoryListResponse {
current_path?: string;
items?: DirectoryItem[];
}

View File

@@ -0,0 +1,12 @@
/**
* Generated by orval v7.21.0 🍺
* Do not edit manually.
* AnthoLume API v1
* REST API for AnthoLume document management system
* OpenAPI spec version: 1.0.0
*/
import type { DatabaseInfo } from './databaseInfo';
export type GetAdmin200 = {
database_info?: DatabaseInfo;
};

View File

@@ -0,0 +1,12 @@
/**
* Generated by orval v7.21.0 🍺
* Do not edit manually.
* AnthoLume API v1
* REST API for AnthoLume document management system
* OpenAPI spec version: 1.0.0
*/
export type GetImportDirectoryParams = {
directory?: string;
select?: string;
};

View File

@@ -0,0 +1,11 @@
/**
* Generated by orval v7.21.0 🍺
* Do not edit manually.
* AnthoLume API v1
* REST API for AnthoLume document management system
* OpenAPI spec version: 1.0.0
*/
export type GetLogsParams = {
filter?: string;
};

View File

@@ -0,0 +1,16 @@
/**
* Generated by orval v7.21.0 🍺
* Do not edit manually.
* AnthoLume API v1
* REST API for AnthoLume document management system
* OpenAPI spec version: 1.0.0
*/
import type { ImportResultStatus } from './importResultStatus';
export interface ImportResult {
id?: string;
name?: string;
path?: string;
status?: ImportResultStatus;
error?: string;
}

View File

@@ -0,0 +1,17 @@
/**
* Generated by orval v7.21.0 🍺
* Do not edit manually.
* AnthoLume API v1
* REST API for AnthoLume document management system
* OpenAPI spec version: 1.0.0
*/
export type ImportResultStatus = typeof ImportResultStatus[keyof typeof ImportResultStatus];
// eslint-disable-next-line @typescript-eslint/no-redeclare
export const ImportResultStatus = {
FAILED: 'FAILED',
SUCCESS: 'SUCCESS',
EXISTS: 'EXISTS',
} as const;

View File

@@ -0,0 +1,12 @@
/**
* Generated by orval v7.21.0 🍺
* Do not edit manually.
* AnthoLume API v1
* REST API for AnthoLume document management system
* OpenAPI spec version: 1.0.0
*/
import type { ImportResult } from './importResult';
export interface ImportResultsResponse {
results?: ImportResult[];
}

View File

@@ -0,0 +1,16 @@
/**
* Generated by orval v7.21.0 🍺
* Do not edit manually.
* AnthoLume API v1
* REST API for AnthoLume document management system
* OpenAPI spec version: 1.0.0
*/
export type ImportType = typeof ImportType[keyof typeof ImportType];
// eslint-disable-next-line @typescript-eslint/no-redeclare
export const ImportType = {
DIRECT: 'DIRECT',
COPY: 'COPY',
} as const;

View File

@@ -8,25 +8,41 @@
export * from './activity'; export * from './activity';
export * from './activityResponse'; export * from './activityResponse';
export * from './backupType';
export * from './createDocumentBody'; export * from './createDocumentBody';
export * from './databaseInfo'; export * from './databaseInfo';
export * from './device'; export * from './device';
export * from './directoryItem';
export * from './directoryListResponse';
export * from './document'; export * from './document';
export * from './documentResponse'; export * from './documentResponse';
export * from './documentsResponse'; export * from './documentsResponse';
export * from './errorResponse'; export * from './errorResponse';
export * from './getActivityParams'; export * from './getActivityParams';
export * from './getAdmin200';
export * from './getDocumentsParams'; export * from './getDocumentsParams';
export * from './getImportDirectoryParams';
export * from './getLogsParams';
export * from './getProgressListParams'; export * from './getProgressListParams';
export * from './getSearchParams'; export * from './getSearchParams';
export * from './getSearchSource'; export * from './getSearchSource';
export * from './graphDataPoint'; export * from './graphDataPoint';
export * from './graphDataResponse'; export * from './graphDataResponse';
export * from './homeResponse'; export * from './homeResponse';
export * from './importResult';
export * from './importResultStatus';
export * from './importResultsResponse';
export * from './importType';
export * from './leaderboardData'; export * from './leaderboardData';
export * from './leaderboardEntry'; export * from './leaderboardEntry';
export * from './logEntry';
export * from './loginRequest'; export * from './loginRequest';
export * from './loginResponse'; export * from './loginResponse';
export * from './logsResponse';
export * from './operationType';
export * from './postAdminActionBody';
export * from './postAdminActionBodyAction';
export * from './postImportBody';
export * from './postSearchBody'; export * from './postSearchBody';
export * from './progress'; export * from './progress';
export * from './progressListResponse'; export * from './progressListResponse';
@@ -36,7 +52,10 @@ export * from './searchResponse';
export * from './setting'; export * from './setting';
export * from './settingsResponse'; export * from './settingsResponse';
export * from './streaksResponse'; export * from './streaksResponse';
export * from './updateUserBody';
export * from './user';
export * from './userData'; export * from './userData';
export * from './userStatisticsResponse'; export * from './userStatisticsResponse';
export * from './userStreak'; export * from './userStreak';
export * from './usersResponse';
export * from './wordCount'; export * from './wordCount';

View File

@@ -0,0 +1,9 @@
/**
* Generated by orval v7.21.0 🍺
* Do not edit manually.
* AnthoLume API v1
* REST API for AnthoLume document management system
* OpenAPI spec version: 1.0.0
*/
export type LogEntry = string;

View File

@@ -0,0 +1,13 @@
/**
* Generated by orval v7.21.0 🍺
* Do not edit manually.
* AnthoLume API v1
* REST API for AnthoLume document management system
* OpenAPI spec version: 1.0.0
*/
import type { LogEntry } from './logEntry';
export interface LogsResponse {
logs?: LogEntry[];
filter?: string;
}

View File

@@ -0,0 +1,17 @@
/**
* Generated by orval v7.21.0 🍺
* Do not edit manually.
* AnthoLume API v1
* REST API for AnthoLume document management system
* OpenAPI spec version: 1.0.0
*/
export type OperationType = typeof OperationType[keyof typeof OperationType];
// eslint-disable-next-line @typescript-eslint/no-redeclare
export const OperationType = {
CREATE: 'CREATE',
UPDATE: 'UPDATE',
DELETE: 'DELETE',
} as const;

View File

@@ -0,0 +1,15 @@
/**
* Generated by orval v7.21.0 🍺
* Do not edit manually.
* AnthoLume API v1
* REST API for AnthoLume document management system
* OpenAPI spec version: 1.0.0
*/
import type { PostAdminActionBodyAction } from './postAdminActionBodyAction';
import type { BackupType } from './backupType';
export type PostAdminActionBody = {
action: PostAdminActionBodyAction;
backup_types?: BackupType[];
restore_file?: Blob;
};

View File

@@ -0,0 +1,18 @@
/**
* Generated by orval v7.21.0 🍺
* Do not edit manually.
* AnthoLume API v1
* REST API for AnthoLume document management system
* OpenAPI spec version: 1.0.0
*/
export type PostAdminActionBodyAction = typeof PostAdminActionBodyAction[keyof typeof PostAdminActionBodyAction];
// eslint-disable-next-line @typescript-eslint/no-redeclare
export const PostAdminActionBodyAction = {
BACKUP: 'BACKUP',
RESTORE: 'RESTORE',
METADATA_MATCH: 'METADATA_MATCH',
CACHE_TABLES: 'CACHE_TABLES',
} as const;

View File

@@ -0,0 +1,13 @@
/**
* Generated by orval v7.21.0 🍺
* Do not edit manually.
* AnthoLume API v1
* REST API for AnthoLume document management system
* OpenAPI spec version: 1.0.0
*/
import type { ImportType } from './importType';
export type PostImportBody = {
directory: string;
type: ImportType;
};

View File

@@ -0,0 +1,15 @@
/**
* Generated by orval v7.21.0 🍺
* Do not edit manually.
* AnthoLume API v1
* REST API for AnthoLume document management system
* OpenAPI spec version: 1.0.0
*/
import type { OperationType } from './operationType';
export type UpdateUserBody = {
operation: OperationType;
user: string;
password?: string;
is_admin?: boolean;
};

View File

@@ -0,0 +1,13 @@
/**
* Generated by orval v7.21.0 🍺
* Do not edit manually.
* AnthoLume API v1
* REST API for AnthoLume document management system
* OpenAPI spec version: 1.0.0
*/
export interface User {
id: string;
admin: boolean;
created_at: string;
}

View File

@@ -0,0 +1,12 @@
/**
* Generated by orval v7.21.0 🍺
* Do not edit manually.
* AnthoLume API v1
* REST API for AnthoLume document management system
* OpenAPI spec version: 1.0.0
*/
import type { User } from './user';
export interface UsersResponse {
users?: User[];
}

View File

@@ -1,43 +1,53 @@
import { Link } from 'react-router-dom';
import { useGetActivity } from '../generated/anthoLumeAPIV1'; import { useGetActivity } from '../generated/anthoLumeAPIV1';
import { Table } from '../components/Table';
export default function ActivityPage() { export default function ActivityPage() {
const { data, isLoading } = useGetActivity({ offset: 0, limit: 100 }); const { data, isLoading } = useGetActivity({ offset: 0, limit: 100 });
const activities = data?.data?.activities; const activities = data?.data?.activities;
if (isLoading) { const columns = [
return <div className="text-gray-500 dark:text-white">Loading...</div>; {
} key: 'document_id' as const,
header: 'Document',
render: (_: any, row: any) => (
<Link
to={`/documents/${row.document_id}`}
className="text-blue-600 dark:text-blue-400 hover:underline"
>
{row.author || 'Unknown'} - {row.title || 'Unknown'}
</Link>
),
},
{
key: 'start_time' as const,
header: 'Time',
render: (value: any) => value || 'N/A',
},
{
key: 'duration' as const,
header: 'Duration',
render: (value: any) => {
if (!value) return 'N/A';
// Format duration (in seconds) to readable format
const hours = Math.floor(value / 3600);
const minutes = Math.floor((value % 3600) / 60);
const seconds = value % 60;
if (hours > 0) {
return `${hours}h ${minutes}m ${seconds}s`;
} else if (minutes > 0) {
return `${minutes}m ${seconds}s`;
} else {
return `${seconds}s`;
}
},
},
{
key: 'end_percentage' as const,
header: 'Percent',
render: (value: any) => (value != null ? `${value}%` : '0%'),
},
];
return ( return <Table columns={columns} data={activities || []} loading={isLoading} />;
<div className="overflow-x-auto">
<div className="inline-block min-w-full overflow-hidden rounded shadow">
<table className="min-w-full bg-white dark:bg-gray-700">
<thead>
<tr className="border-b dark:border-gray-600">
<th className="text-left p-3 text-gray-500 dark:text-white">Activity Type</th>
<th className="text-left p-3 text-gray-500 dark:text-white">Document</th>
<th className="text-left p-3 text-gray-500 dark:text-white">Timestamp</th>
</tr>
</thead>
<tbody>
{activities?.map((activity: any) => (
<tr key={activity.id} className="border-b dark:border-gray-600">
<td className="p-3 text-gray-700 dark:text-gray-300">
{activity.activity_type}
</td>
<td className="p-3">
<a href={`/documents/${activity.document_id}`} className="text-blue-600 dark:text-blue-400">
{activity.document_id}
</a>
</td>
<td className="p-3 text-gray-700 dark:text-gray-300">
{new Date(activity.timestamp).toLocaleString()}
</td>
</tr>
))}
</tbody>
</table>
</div>
</div>
);
} }

View File

@@ -1,8 +1,181 @@
import { useState } from 'react';
import { useGetImportDirectory, usePostImport } from '../generated/anthoLumeAPIV1';
import { Button } from '../components/Button';
import { FolderOpen } from 'lucide-react';
export default function AdminImportPage() { export default function AdminImportPage() {
const [currentPath, setCurrentPath] = useState<string>('');
const [selectedDirectory, setSelectedDirectory] = useState<string>('');
const [importType, setImportType] = useState<'DIRECT' | 'COPY'>('DIRECT');
const { data: directoryData, isLoading } = useGetImportDirectory(
currentPath ? { directory: currentPath } : {}
);
const postImport = usePostImport();
const directories = directoryData?.data?.items || [];
const currentPathDisplay = directoryData?.data?.current_path ?? currentPath ?? '/data';
const handleSelectDirectory = (directory: string) => {
setSelectedDirectory(`${currentPath}/${directory}`);
};
const handleNavigateUp = () => {
if (currentPathDisplay !== '/') {
const parts = currentPathDisplay.split('/');
parts.pop();
setCurrentPath(parts.join('/') || '');
}
};
const handleImport = () => {
if (!selectedDirectory) return;
postImport.mutate(
{
data: {
directory: selectedDirectory,
type: importType,
},
},
{
onSuccess: (response) => {
console.log('Import completed:', response.data);
// Redirect to import results page
window.location.href = '/admin/import-results';
},
onError: (error) => {
console.error('Import failed:', error);
alert('Import failed: ' + (error as any).message);
},
}
);
};
const handleCancel = () => {
setSelectedDirectory('');
};
if (isLoading && !currentPath) {
return <div className="text-gray-500 dark:text-white">Loading...</div>;
}
if (selectedDirectory) {
return (
<div className="overflow-x-auto">
<div className="inline-block min-w-full overflow-hidden rounded shadow">
<div
className="flex flex-col grow gap-2 p-4 rounded shadow-lg bg-white dark:bg-gray-700 text-gray-500 dark:text-white"
>
<p className="text-lg font-semibold text-gray-500">
Selected Import Directory
</p>
<form className="flex gap-4 flex-col" onSubmit={handleImport}>
<div className="flex justify-between gap-4 w-full">
<div className="flex gap-4 items-center">
<FolderOpen size={20} />
<p className="font-medium text-lg break-all">
{selectedDirectory}
</p>
</div>
<div className="flex flex-col justify-around gap-2 mr-4">
<div className="inline-flex gap-2 items-center">
<input
type="radio"
id="direct"
checked={importType === 'DIRECT'}
onChange={() => setImportType('DIRECT')}
/>
<label htmlFor="direct">Direct</label>
</div>
<div className="inline-flex gap-2 items-center">
<input
type="radio"
id="copy"
checked={importType === 'COPY'}
onChange={() => setImportType('COPY')}
/>
<label htmlFor="copy">Copy</label>
</div>
</div>
</div>
<div className="flex gap-4">
<Button type="submit" className="px-10 py-2 text-base">
Import Directory
</Button>
<Button
type="button"
variant="secondary"
onClick={handleCancel}
className="px-10 py-2 text-base"
>
Cancel
</Button>
</div>
</form>
</div>
</div>
</div>
);
}
return ( return (
<div> <div className="overflow-x-auto">
<h1 className="text-xl font-bold dark:text-white">Admin - Import</h1> <div className="inline-block min-w-full overflow-hidden rounded shadow">
<p className="text-gray-500 dark:text-gray-400">Document import page</p> <table
className="min-w-full leading-normal bg-white dark:bg-gray-700 text-sm"
>
<thead className="text-gray-800 dark:text-gray-400">
<tr>
<th
className="p-3 font-normal text-left border-b border-gray-200 dark:border-gray-800 w-12"
></th>
<th
className="p-3 font-normal text-left border-b border-gray-200 dark:border-gray-800 break-all"
>
{currentPath}
</th>
</tr>
</thead>
<tbody className="text-black dark:text-white">
{currentPath !== '/' && (
<tr>
<td
className="p-3 border-b border-gray-200 text-gray-800 dark:text-gray-400"
></td>
<td className="p-3 border-b border-gray-200">
<button onClick={handleNavigateUp}>
<p>../</p>
</button>
</td>
</tr>
)}
{directories.length === 0 ? (
<tr>
<td className="text-center p-3" colSpan={2}>No Folders</td>
</tr>
) : (
directories.map((item) => (
<tr key={item.name}>
<td
className="p-3 border-b border-gray-200 text-gray-800 dark:text-gray-400"
>
<button onClick={() => item.name && handleSelectDirectory(item.name)}>
<FolderOpen size={20} />
</button>
</td>
<td className="p-3 border-b border-gray-200">
<button onClick={() => item.name && handleSelectDirectory(item.name)}>
<p>{item.name ?? ''}</p>
</button>
</td>
</tr>
))
)}
</tbody>
</table>
</div>
</div> </div>
); );
} }

View File

@@ -0,0 +1,73 @@
import { useGetImportResults } from '../generated/anthoLumeAPIV1';
import type { ImportResult } from '../generated/model/importResult';
import { Link } from 'react-router-dom';
export default function AdminImportResultsPage() {
const { data: resultsData, isLoading } = useGetImportResults();
const results = resultsData?.data?.results || [];
if (isLoading) {
return <div className="text-gray-500 dark:text-white">Loading...</div>;
}
return (
<div className="overflow-x-auto">
<div className="inline-block min-w-full overflow-hidden rounded shadow">
<table
className="min-w-full leading-normal bg-white dark:bg-gray-700 text-sm"
>
<thead className="text-gray-800 dark:text-gray-400">
<tr>
<th
className="p-3 font-normal text-left uppercase border-b border-gray-200 dark:border-gray-800"
>
Document
</th>
<th
className="p-3 font-normal text-left uppercase border-b border-gray-200 dark:border-gray-800"
>
Status
</th>
<th
className="p-3 font-normal text-left uppercase border-b border-gray-200 dark:border-gray-800"
>
Error
</th>
</tr>
</thead>
<tbody className="text-black dark:text-white">
{results.length === 0 ? (
<tr>
<td className="text-center p-3" colSpan={3}>No Results</td>
</tr>
) : (
results.map((result: ImportResult, index: number) => (
<tr key={index}>
<td
className="p-3 border-b border-gray-200 grid"
style={{ gridTemplateColumns: '4rem auto' }}
>
<span className="text-gray-800 dark:text-gray-400">Name:</span>
{result.id ? (
<Link to={`/documents/${result.id}`}>{result.name}</Link>
) : (
<span>N/A</span>
)}
<span className="text-gray-800 dark:text-gray-400">File:</span>
<span>{result.path}</span>
</td>
<td className="p-3 border-b border-gray-200">
<p>{result.status}</p>
</td>
<td className="p-3 border-b border-gray-200">
<p>{result.error || ''}</p>
</td>
</tr>
))
)}
</tbody>
</table>
</div>
</div>
);
}

View File

@@ -1,8 +1,66 @@
import { useState, FormEvent } from 'react';
import { useGetLogs } from '../generated/anthoLumeAPIV1';
import { Button } from '../components/Button';
import { Search } from 'lucide-react';
export default function AdminLogsPage() { export default function AdminLogsPage() {
const [filter, setFilter] = useState('');
const { data: logsData, isLoading, refetch } = useGetLogs(
filter ? { filter } : {}
);
const logs = logsData?.data?.logs || [];
const handleFilterSubmit = (e: FormEvent) => {
e.preventDefault();
refetch();
};
if (isLoading) {
return <div className="text-gray-500 dark:text-white">Loading...</div>;
}
return ( return (
<div> <div>
<h1 className="text-xl font-bold dark:text-white">Admin - Logs</h1> {/* Filter Form */}
<p className="text-gray-500 dark:text-gray-400">System logs page</p> <div
className="flex flex-col gap-2 grow p-4 mb-4 rounded shadow-lg bg-white dark:bg-gray-700 text-gray-500 dark:text-white"
>
<form className="flex gap-4 flex-col lg:flex-row" onSubmit={handleFilterSubmit}>
<div className="flex flex-col w-full grow">
<div className="flex relative">
<span
className="inline-flex items-center px-3 border-t bg-white border-l border-b border-gray-300 text-gray-500 shadow-sm text-sm"
>
<Search size={15} />
</span>
<input
type="text"
value={filter}
onChange={(e) => setFilter(e.target.value)}
className="flex-1 appearance-none rounded-none border border-gray-300 w-full py-2 px-2 bg-white text-gray-700 placeholder-gray-400 shadow-sm text-base focus:outline-none focus:ring-2 focus:ring-purple-600 focus:border-transparent"
placeholder="JQ Filter"
/>
</div>
</div>
<div className="lg:w-60">
<Button variant="secondary" type="submit">Filter</Button>
</div>
</form>
</div>
{/* Log Display */}
<div
className="flex flex-col-reverse text-black dark:text-white w-full overflow-scroll"
style={{ fontFamily: 'monospace' }}
>
{logs.map((log: string, index: number) => (
<span key={index} className="whitespace-nowrap hover:whitespace-pre">
{log}
</span>
))}
</div>
</div> </div>
); );
} }

View File

@@ -1,8 +1,218 @@
import { useState, FormEvent } from 'react';
import { useGetAdmin, usePostAdminAction } from '../generated/anthoLumeAPIV1';
import { Button } from '../components/Button';
interface BackupTypes {
covers: boolean;
documents: boolean;
}
export default function AdminPage() { export default function AdminPage() {
const { isLoading } = useGetAdmin();
const postAdminAction = usePostAdminAction();
const [backupTypes, setBackupTypes] = useState<BackupTypes>({
covers: false,
documents: false,
});
const [restoreFile, setRestoreFile] = useState<File | null>(null);
const [message, setMessage] = useState<string | null>(null);
const [errorMessage, setErrorMessage] = useState<string | null>(null);
const handleBackupSubmit = (e: FormEvent) => {
e.preventDefault();
const backupTypesList: string[] = [];
if (backupTypes.covers) backupTypesList.push('COVERS');
if (backupTypes.documents) backupTypesList.push('DOCUMENTS');
postAdminAction.mutate(
{
data: {
action: 'BACKUP',
backup_types: backupTypesList as any,
},
},
{
onSuccess: (response) => {
// Handle file download
const url = window.URL.createObjectURL(new Blob([response.data]));
const link = document.createElement('a');
link.href = url;
link.setAttribute('download', `AnthoLumeBackup_${new Date().toISOString().replace(/[:.]/g, '')}.zip`);
document.body.appendChild(link);
link.click();
link.remove();
setMessage('Backup completed successfully');
setErrorMessage(null);
},
onError: (error) => {
setErrorMessage('Backup failed: ' + (error as any).message);
setMessage(null);
},
}
);
};
const handleRestoreSubmit = (e: FormEvent) => {
e.preventDefault();
if (!restoreFile) return;
const formData = new FormData();
formData.append('restore_file', restoreFile);
formData.append('action', 'RESTORE');
postAdminAction.mutate(
{
data: formData as any,
},
{
onSuccess: () => {
setMessage('Restore completed successfully');
setErrorMessage(null);
},
onError: (error) => {
setErrorMessage('Restore failed: ' + (error as any).message);
setMessage(null);
},
}
);
};
const handleMetadataMatch = () => {
postAdminAction.mutate(
{
data: {
action: 'METADATA_MATCH',
},
},
{
onSuccess: () => {
setMessage('Metadata matching started');
setErrorMessage(null);
},
onError: (error) => {
setErrorMessage('Metadata matching failed: ' + (error as any).message);
setMessage(null);
},
}
);
};
const handleCacheTables = () => {
postAdminAction.mutate(
{
data: {
action: 'CACHE_TABLES',
},
},
{
onSuccess: () => {
setMessage('Cache tables started');
setErrorMessage(null);
},
onError: (error) => {
setErrorMessage('Cache tables failed: ' + (error as any).message);
setMessage(null);
},
}
);
};
if (isLoading) {
return <div className="text-gray-500 dark:text-white">Loading...</div>;
}
return ( return (
<div> <div className="w-full flex flex-col gap-4 grow">
<h1 className="text-xl font-bold dark:text-white">Admin - General</h1> {/* Backup & Restore Card */}
<p className="text-gray-500 dark:text-gray-400">Admin general settings page</p> <div
className="flex flex-col gap-2 grow p-4 rounded shadow-lg bg-white dark:bg-gray-700 text-gray-500 dark:text-white"
>
<p className="text-lg font-semibold mb-2">Backup & Restore</p>
<div className="flex flex-col gap-4">
{/* Backup Form */}
<form className="flex justify-between" onSubmit={handleBackupSubmit}>
<div className="flex gap-8 items-center">
<div>
<input
type="checkbox"
id="backup_covers"
checked={backupTypes.covers}
onChange={(e) => setBackupTypes({ ...backupTypes, covers: e.target.checked })}
/>
<label htmlFor="backup_covers">Covers</label>
</div>
<div>
<input
type="checkbox"
id="backup_documents"
checked={backupTypes.documents}
onChange={(e) => setBackupTypes({ ...backupTypes, documents: e.target.checked })}
/>
<label htmlFor="backup_documents">Documents</label>
</div>
</div>
<div className="w-40 h-10">
<Button variant="secondary" type="submit">Backup</Button>
</div>
</form>
{/* Restore Form */}
<form
onSubmit={handleRestoreSubmit}
className="flex justify-between grow"
>
<div className="flex items-center w-1/2">
<input
type="file"
accept=".zip"
onChange={(e) => setRestoreFile(e.target.files?.[0] || null)}
className="w-full"
/>
</div>
<div className="w-40 h-10">
<Button variant="secondary" type="submit">Restore</Button>
</div>
</form>
</div>
{errorMessage && (
<span className="text-red-400 text-xs">{errorMessage}</span>
)}
{message && (
<span className="text-green-400 text-xs">{message}</span>
)}
</div>
{/* Tasks Card */}
<div
className="flex flex-col grow p-4 rounded shadow-lg bg-white dark:bg-gray-700 text-gray-500 dark:text-white"
>
<p className="text-lg font-semibold">Tasks</p>
<table className="min-w-full bg-white dark:bg-gray-700 text-sm">
<tbody className="text-black dark:text-white">
<tr>
<td className="pl-0">
<p>Metadata Matching</p>
</td>
<td className="py-2 float-right">
<div className="w-40 h-10 text-base">
<Button variant="secondary" onClick={handleMetadataMatch}>Run</Button>
</div>
</td>
</tr>
<tr>
<td>
<p>Cache Tables</p>
</td>
<td className="py-2 float-right">
<div className="w-40 h-10 text-base">
<Button variant="secondary" onClick={handleCacheTables}>Run</Button>
</div>
</td>
</tr>
</tbody>
</table>
</div>
</div> </div>
); );
} }

View File

@@ -1,8 +1,234 @@
import { useState, FormEvent } from 'react';
import { useGetUsers, useUpdateUser } from '../generated/anthoLumeAPIV1';
import { Plus, Trash2 } from 'lucide-react';
export default function AdminUsersPage() { export default function AdminUsersPage() {
const { data: usersData, isLoading, refetch } = useGetUsers({});
const updateUser = useUpdateUser();
const [showAddForm, setShowAddForm] = useState(false);
const [newUsername, setNewUsername] = useState('');
const [newPassword, setNewPassword] = useState('');
const [newIsAdmin, setNewIsAdmin] = useState(false);
const users = usersData?.data?.users || [];
const handleCreateUser = (e: FormEvent) => {
e.preventDefault();
if (!newUsername || !newPassword) return;
updateUser.mutate(
{
data: {
operation: 'CREATE',
user: newUsername,
password: newPassword,
is_admin: newIsAdmin,
},
},
{
onSuccess: () => {
setShowAddForm(false);
setNewUsername('');
setNewPassword('');
setNewIsAdmin(false);
refetch();
},
onError: (error: any) => {
alert('Failed to create user: ' + error.message);
},
}
);
};
const handleDeleteUser = (userId: string) => {
updateUser.mutate(
{
data: {
operation: 'DELETE',
user: userId,
},
},
{
onSuccess: () => {
refetch();
},
onError: (error: any) => {
alert('Failed to delete user: ' + error.message);
},
}
);
};
const handleUpdatePassword = (userId: string, password: string) => {
if (!password) return;
updateUser.mutate(
{
data: {
operation: 'UPDATE',
user: userId,
password: password,
},
},
{
onSuccess: () => {
refetch();
},
onError: (error: any) => {
alert('Failed to update password: ' + error.message);
},
}
);
};
const handleToggleAdmin = (userId: string, isAdmin: boolean) => {
updateUser.mutate(
{
data: {
operation: 'UPDATE',
user: userId,
is_admin: isAdmin,
},
},
{
onSuccess: () => {
refetch();
},
onError: (error: any) => {
alert('Failed to update admin status: ' + error.message);
},
}
);
};
if (isLoading) {
return <div className="text-gray-500 dark:text-white">Loading...</div>;
}
return ( return (
<div> <div className="relative h-full overflow-x-auto">
<h1 className="text-xl font-bold dark:text-white">Admin - Users</h1> {/* Add User Form */}
<p className="text-gray-500 dark:text-gray-400">User management page</p> {showAddForm && (
<div className="absolute top-10 left-10 p-3 transition-all duration-200 bg-gray-200 rounded shadow-lg shadow-gray-500 dark:shadow-gray-900 dark:bg-gray-600">
<form onSubmit={handleCreateUser}
className="flex flex-col gap-2 text-black dark:text-white text-sm">
<input
type="text"
value={newUsername}
onChange={(e) => setNewUsername(e.target.value)}
placeholder="Username"
className="p-2 bg-gray-300 text-black dark:bg-gray-700 dark:text-white"
/>
<input
type="password"
value={newPassword}
onChange={(e) => setNewPassword(e.target.value)}
placeholder="Password"
className="p-2 bg-gray-300 text-black dark:bg-gray-700 dark:text-white"
/>
<div className="flex items-center gap-2">
<input
type="checkbox"
id="new_is_admin"
checked={newIsAdmin}
onChange={(e) => setNewIsAdmin(e.target.checked)}
/>
<label htmlFor="new_is_admin">Admin</label>
</div>
<button
className="font-medium px-2 py-1 text-white bg-gray-500 dark:text-gray-800 hover:bg-gray-800 dark:hover:bg-gray-100"
type="submit"
>
Create
</button>
</form>
</div>
)}
{/* Users Table */}
<div className="min-w-full overflow-scroll rounded shadow">
<table className="min-w-full leading-normal bg-white dark:bg-gray-700 text-sm">
<thead className="text-gray-800 dark:text-gray-400">
<tr>
<th className="p-3 font-normal text-left uppercase border-b border-gray-200 dark:border-gray-800 w-12">
<button onClick={() => setShowAddForm(!showAddForm)}>
<Plus size={20} />
</button>
</th>
<th className="p-3 font-normal text-left uppercase border-b border-gray-200 dark:border-gray-800">User</th>
<th className="p-3 font-normal text-left uppercase border-b border-gray-200 dark:border-gray-800">Password</th>
<th className="p-3 font-normal text-left uppercase border-b border-gray-200 dark:border-gray-800 text-center">
Permissions
</th>
<th className="p-3 font-normal text-left uppercase border-b border-gray-200 dark:border-gray-800 w-48">Created</th>
</tr>
</thead>
<tbody className="text-black dark:text-white">
{users.length === 0 ? (
<tr>
<td className="text-center p-3" colSpan={5}>No Results</td>
</tr>
) : (
users.map((user) => (
<tr key={user.id}>
{/* Delete Button */}
<td className="p-3 border-b border-gray-200 text-gray-800 dark:text-gray-400 cursor-pointer relative">
<button onClick={() => handleDeleteUser(user.id)}>
<Trash2 size={20} />
</button>
</td>
{/* User ID */}
<td className="p-3 border-b border-gray-200">
<p>{user.id}</p>
</td>
{/* Password Reset */}
<td className="border-b border-gray-200 px-3">
<button
onClick={() => {
const password = prompt(`Enter new password for ${user.id}`);
if (password) handleUpdatePassword(user.id, password);
}}
className="font-medium px-2 py-1 text-white bg-gray-500 dark:text-gray-800 hover:bg-gray-800 dark:hover:bg-gray-100"
>
Reset
</button>
</td>
{/* Admin Toggle */}
<td className="flex gap-2 justify-center p-3 border-b border-gray-200 text-center min-w-40">
<button
onClick={() => handleToggleAdmin(user.id, true)}
disabled={user.admin}
className={`px-2 py-1 rounded-md text-white dark:text-black ${
user.admin
? 'bg-gray-800 dark:bg-gray-100 cursor-default'
: 'bg-gray-400 dark:bg-gray-600 cursor-pointer'
}`}
>
admin
</button>
<button
onClick={() => handleToggleAdmin(user.id, false)}
disabled={!user.admin}
className={`px-2 py-1 rounded-md text-white dark:text-black ${
!user.admin
? 'bg-gray-800 dark:bg-gray-100 cursor-default'
: 'bg-gray-400 dark:bg-gray-600 cursor-pointer'
}`}
>
user
</button>
</td>
{/* Created Date */}
<td className="p-3 border-b border-gray-200">
<p>{user.created_at}</p>
</td>
</tr>
))
)}
</tbody>
</table>
</div>
</div> </div>
); );
} }

View File

@@ -1,51 +1,40 @@
import { Link } from 'react-router-dom'; import { Link } from 'react-router-dom';
import { useGetProgressList } from '../generated/anthoLumeAPIV1'; import { useGetProgressList } from '../generated/anthoLumeAPIV1';
import { Table } from '../components/Table';
export default function ProgressPage() { export default function ProgressPage() {
const { data, isLoading } = useGetProgressList({ page: 1, limit: 15 }); const { data, isLoading } = useGetProgressList({ page: 1, limit: 15 });
const progress = data?.data?.progress; const progress = data?.data?.progress;
if (isLoading) { const columns = [
return <div className="text-gray-500 dark:text-white">Loading...</div>; {
} key: 'document_id' as const,
header: 'Document',
render: (_: any, row: any) => (
<Link
to={`/documents/${row.document_id}`}
className="text-blue-600 dark:text-blue-400 hover:underline"
>
{row.author || 'Unknown'} - {row.title || 'Unknown'}
</Link>
),
},
{
key: 'device_name' as const,
header: 'Device Name',
render: (value: any) => value || 'Unknown',
},
{
key: 'percentage' as const,
header: 'Percentage',
render: (value: any) => (value ? `${Math.round(value)}%` : '0%'),
},
{
key: 'created_at' as const,
header: 'Created At',
render: (value: any) => (value ? new Date(value).toLocaleDateString() : 'N/A'),
},
];
return ( return <Table columns={columns} data={progress || []} loading={isLoading} />;
<div className="overflow-x-auto">
<div className="inline-block min-w-full overflow-hidden rounded shadow">
<table className="min-w-full bg-white dark:bg-gray-700">
<thead>
<tr className="border-b dark:border-gray-600">
<th className="text-left p-3 text-gray-500 dark:text-white">Document</th>
<th className="text-left p-3 text-gray-500 dark:text-white">Device Name</th>
<th className="text-left p-3 text-gray-500 dark:text-white">Percentage</th>
<th className="text-left p-3 text-gray-500 dark:text-white">Created At</th>
</tr>
</thead>
<tbody>
{progress?.map((row: any) => (
<tr key={row.document_id} className="border-b dark:border-gray-600">
<td className="p-3">
<Link
to={`/documents/${row.document_id}`}
className="text-blue-600 dark:text-blue-400 hover:underline"
>
{row.author || 'Unknown'} - {row.title || 'Unknown'}
</Link>
</td>
<td className="p-3 text-gray-700 dark:text-gray-300">
{row.device_name || 'Unknown'}
</td>
<td className="p-3 text-gray-700 dark:text-gray-300">
{row.percentage ? Math.round(row.percentage) : 0}%
</td>
<td className="p-3 text-gray-700 dark:text-gray-300">
{row.created_at ? new Date(row.created_at).toLocaleDateString() : 'N/A'}
</td>
</tr>
))}
</tbody>
</table>
</div>
</div>
);
} }