diff --git a/src/app/wiki/[slug]/wiki-by-slug-client.tsx b/src/app/wiki/[slug]/wiki-by-slug-client.tsx index a551f6e..e0f638c 100644 --- a/src/app/wiki/[slug]/wiki-by-slug-client.tsx +++ b/src/app/wiki/[slug]/wiki-by-slug-client.tsx @@ -497,9 +497,11 @@ export default function WikiBySlugClient({ slug }: { slug: string }) { createdAt: sample?.created_at || 'Unknown date', title: `Phiên bản lúc ${formatDate(sample?.created_at)}` }; - if (sample?.isCurrent) { - return { ...versionInfo, content: sample.content || '' }; + if (sample && "content" in sample && (sample as any).isCurrent) { + return { ...versionInfo, content: (sample as any).content || "" }; } + + const contentResp = await getContentByVersionWikiId(versionId); return { ...versionInfo, content: contentResp?.data?.content || "" }; }); diff --git a/src/components/calendar/Calendar.tsx b/src/components/calendar/Calendar.tsx index 8c9e3f0..b3bb58c 100644 --- a/src/components/calendar/Calendar.tsx +++ b/src/components/calendar/Calendar.tsx @@ -73,7 +73,16 @@ const Calendar: React.FC = () => { const handleEventClick = (clickInfo: EventClickArg) => { const event = clickInfo.event; - setSelectedEvent(event as unknown as CalendarEvent); + setSelectedEvent({ + id: event.id, + title: event.title, + start: event.startStr, + end: event.endStr, + extendedProps: { + calendar: event.extendedProps.calendar, + }, + } as CalendarEvent); + setEventTitle(event.title); setEventStartDate(event.start?.toISOString().split("T")[0] || ""); setEventEndDate(event.end?.toISOString().split("T")[0] || ""); diff --git a/src/components/form/role-upgrade/Editor.tsx b/src/components/form/role-upgrade/Editor.tsx index bd7afd8..70a7573 100644 --- a/src/components/form/role-upgrade/Editor.tsx +++ b/src/components/form/role-upgrade/Editor.tsx @@ -7,9 +7,10 @@ import "react-quill-new/dist/quill.snow.css"; const ReactQuillEditor = dynamic( async () => { const { default: RQ } = await import("react-quill-new"); - return ({ forwardedRef, ...props }: any) => ( + return ({ forwardedRef, ...props }: { forwardedRef: React.Ref; [key: string]: any }) => ( ); + }, { ssr: false, diff --git a/src/components/tables/ApplicationDetailModal.tsx b/src/components/tables/ApplicationDetailModal.tsx index c63d277..61231d8 100644 --- a/src/components/tables/ApplicationDetailModal.tsx +++ b/src/components/tables/ApplicationDetailModal.tsx @@ -15,10 +15,13 @@ import { IsolatedContent } from "@/components/ui/IsolatedContent"; import { apiDeleteHistorianCV } from "@/service/historianService"; import { statusConfig } from "@/service/handler"; +import { Application } from "@/interface/historian"; +import { MediaItem } from "@/components/tables/MediaTable"; + interface Props { isOpen: boolean; onClose: () => void; - application: any; + application: Application | null; onRefresh: () => void; } @@ -42,7 +45,7 @@ export default function ApplicationDetailModal({ } }, [isOpen, application]); - const isImageFile = (file: any) => { + const isImageFile = (file: MediaItem) => { const isImageMime = file.mime_type?.startsWith("image/"); const isImageExt = /\.(jpg|jpeg|png|webp|gif)$/i.test(file.storage_key); return isImageMime || isImageExt; @@ -51,17 +54,17 @@ export default function ApplicationDetailModal({ const mediaList = application?.media || []; const imageMediaOnly = mediaList.filter(isImageFile); - const imageSlides = imageMediaOnly.map((item: any) => ({ + const imageSlides = imageMediaOnly.map((item: MediaItem) => ({ src: `${URL_MEDIA}${item.storage_key}`, title: item.original_name, description: `Dung lượng: ${(item.size / 1024).toFixed(2)} KB`, })); - const handleMediaClick = (item: any) => { + const handleMediaClick = (item: MediaItem) => { const fileUrl = `${URL_MEDIA}${item.storage_key}`; if (isImageFile(item)) { const photoIndex = imageMediaOnly.findIndex( - (img: any) => img.id === item.id, + (img: MediaItem) => img.id === item.id, ); setIndex(photoIndex); } else { @@ -93,7 +96,9 @@ export default function ApplicationDetailModal({ }; const handleDeleteApplication = async () => { + if (!application) return; await apiDeleteHistorianCV(application.id); + Swal.fire("Thành công!", "Hồ sơ đã được xóa.", "success"); onRefresh(); onClose(); @@ -186,7 +191,7 @@ export default function ApplicationDetailModal({ {mediaList.length > 0 ? (
- {mediaList.map((media: any, idx: number) => { + {mediaList.map((media: MediaItem, idx: number) => { const isImg = isImageFile(media); return (
| null; + created_at: string; updated_at: string; } diff --git a/src/components/tables/UserDetailModal.tsx b/src/components/tables/UserDetailModal.tsx index 1c3600f..380f695 100644 --- a/src/components/tables/UserDetailModal.tsx +++ b/src/components/tables/UserDetailModal.tsx @@ -2,6 +2,8 @@ import { Modal } from "../ui/modal"; import UserMetaCard from "@/components/user-profile/UserMetaCard"; import UserInfoCard from "@/components/user-profile/UserInfoCard"; +import { UserMetaCardProps } from "@/interface/user"; + import { fullDataUser } from "@/interface/admin"; import { useEffect, useState } from "react"; import { MediaDto } from "@/interface/media"; @@ -28,7 +30,17 @@ export default function UserDetailModal({ const [mediaData, setMediaData] = useState(null); const [loading, setLoading] = useState(true); - const formattedData = { data: user }; + const formattedData: UserMetaCardProps = { + data: user + ? { + id: user.id, + email: user.email, + profile: user.profile, + roles: user.roles as any, // UserRole and Role are slightly different, need a better fix or small cast + } + : undefined, + }; + useEffect(() => { if (user?.id && isOpen) { @@ -68,8 +80,9 @@ export default function UserDetailModal({
- - + + +
{loading ? ( diff --git a/src/components/ui/chat/ChatbotWidget.tsx b/src/components/ui/chat/ChatbotWidget.tsx index dbcf402..a86f171 100644 --- a/src/components/ui/chat/ChatbotWidget.tsx +++ b/src/components/ui/chat/ChatbotWidget.tsx @@ -3,6 +3,8 @@ import React, { useState, useRef, useEffect } from "react"; import { ChatbotPayload } from "@/interface/chatbot"; import { apiChatbot } from "@/service/chatbotService"; +import { AxiosError } from "axios"; + type Message = { id: string; @@ -67,16 +69,18 @@ export default function ChatbotWidget({ }; setMessages((prev) => [...prev, botMessage]); - } catch (error: any) { + } catch (error) { + const axiosError = error as AxiosError<{ message: string }>; const errorMessage: Message = { id: (Date.now() + 1).toString(), sender: "bot", text: - error?.response?.data?.message || + axiosError.response?.data?.message || "Có lỗi xảy ra khi kết nối. Vui lòng thử lại sau.", }; setMessages((prev) => [...prev, errorMessage]); } finally { + setIsLoading(false); } }; diff --git a/src/components/user-profile/ApplicationList.tsx b/src/components/user-profile/ApplicationList.tsx index 8bf39fb..4c857e0 100644 --- a/src/components/user-profile/ApplicationList.tsx +++ b/src/components/user-profile/ApplicationList.tsx @@ -20,7 +20,10 @@ const formatFullDateTime = (dateString: string) => { return `${time} ${day}`; }; -const processMedia = (mediaArray: any[]) => { +import { Application } from "@/interface/historian"; +import { MediaItem } from "../tables/MediaTable"; + +const processMedia = (mediaArray: MediaItem[]) => { if (!mediaArray || mediaArray.length === 0) return { type: "empty" }; const imageFiles = mediaArray.filter((file) => { const isImageMime = file.mime_type?.startsWith("image/"); @@ -49,15 +52,16 @@ const processMedia = (mediaArray: any[]) => { export default function ApplicationList({ applications, }: { - applications: any[]; + applications: Application[]; }) { const router = useRouter(); const dispatch = useDispatch(); - const handleViewDetail = (app: any) => { + const handleViewDetail = (app: Application) => { dispatch(setSelectedApplication(app)); router.push(`/user/account/applications`); }; + const StatusIcons: Record = { APPROVED: ( diff --git a/src/components/user-profile/Media.tsx b/src/components/user-profile/Media.tsx index 2f4ea57..becb3b3 100644 --- a/src/components/user-profile/Media.tsx +++ b/src/components/user-profile/Media.tsx @@ -11,6 +11,8 @@ import { URL_MEDIA } from "../../../api"; import { deleteMedia } from "@/service/mediaService"; import { INITIAL_LIMIT } from "../../../constant"; +import { MediaItem } from "../tables/MediaTable"; + export default function MediaLibrary({ data, @@ -32,7 +34,7 @@ export default function MediaLibrary({ setLocalMedia(data?.data || []); }, [data]); - const isImageFile = (file: any) => { + const isImageFile = (file: MediaItem) => { const isImageMime = file.mime_type?.startsWith("image/"); const isImageExt = /\.(jpg|jpeg|png|webp|gif)$/i.test(file.storage_key); return isImageMime || isImageExt; @@ -77,7 +79,7 @@ export default function MediaLibrary({ } }; - const handleItemClick = (item: any, idx: number, isImage: boolean) => { + const handleItemClick = (item: MediaItem, idx: number, isImage: boolean) => { if (isSelectionMode) { toggleItemSelection(item.id); } else { @@ -166,7 +168,7 @@ export default function MediaLibrary({ } }; - const renderItemCard = (item: any, isImage: boolean, idx: number) => { + const renderItemCard = (item: MediaItem, isImage: boolean, idx: number) => { const isSelected = selectedIds.includes(item.id); return ( @@ -254,6 +256,7 @@ export default function MediaLibrary({ ); }; + return (
diff --git a/src/components/user-profile/UserInfoCard.tsx b/src/components/user-profile/UserInfoCard.tsx index 8a6ec0a..2bac423 100644 --- a/src/components/user-profile/UserInfoCard.tsx +++ b/src/components/user-profile/UserInfoCard.tsx @@ -10,6 +10,8 @@ import { Profile, UserMetaCardProps } from "@/interface/user"; import { apiUpdateUser } from "@/service/userService"; import { toast } from "sonner"; import Link from "next/link"; +import { AxiosError } from "axios"; + export default function UserInfoCard({ data }: { data: UserMetaCardProps }) { const router = useRouter(); @@ -62,8 +64,9 @@ export default function UserInfoCard({ data }: { data: UserMetaCardProps }) { setTimeout(() => { window.location.reload(); }, 1000); - } catch (error: any) { - const serverResponse = error.response?.data; + } catch (error) { + const axiosError = error as AxiosError<{ status: boolean; message: string }>; + const serverResponse = axiosError.response?.data; if (serverResponse && serverResponse.status === false) { const msg = serverResponse.message || ""; @@ -77,8 +80,9 @@ export default function UserInfoCard({ data }: { data: UserMetaCardProps }) { toast.error("Không thể kết nối đến máy chủ hoặc lỗi hệ thống."); } - console.error("Lỗi chi tiết:", error); + console.error("Lỗi chi tiết:", axiosError); } + }; return ( diff --git a/src/service/adminService.ts b/src/service/adminService.ts index 5508a5c..ad69c91 100644 --- a/src/service/adminService.ts +++ b/src/service/adminService.ts @@ -9,7 +9,17 @@ export const apiGetListUser = async (payload: getUserDto) => { return response?.data; }; -export const apiChangeRole = async (id: string, payload: any) => { +export interface ChangeRolePayload { + role_ids: string[]; + user_id: string; +} + +export interface UpdateApplicationStatusPayload { + status: "APPROVED" | "REJECTED"; + review_note: string; +} + +export const apiChangeRole = async (id: string, payload: ChangeRolePayload) => { const response = await api.patch(API.Admin.CHANGE_ROLE(id), payload); return response?.data; }; @@ -32,11 +42,12 @@ export const apiGetUserMedia = async (id: string) => { return response?.data; }; -export const apiUpdateApplicationStatus = async (id: string, payload: any) => { +export const apiUpdateApplicationStatus = async (id: string, payload: UpdateApplicationStatusPayload) => { const response = await api.put(API.Admin.UPDATE_APPLICATION_STATUS(id), payload); return response?.data; }; + export const apiGetUserById = async (userId: string) => { const response = await api.get(API.Admin.GET_USER_BY_ID(userId)); return response?.data; diff --git a/src/service/auth.ts b/src/service/auth.ts index 87161ef..7223255 100644 --- a/src/service/auth.ts +++ b/src/service/auth.ts @@ -2,6 +2,29 @@ import api from "@/config/config"; import { API } from "../../api"; import { clearStoredTokens, extractTokensFromResponsePayload, setStoredTokens } from "@/auth/tokenStore"; +export interface SignUpPayload { + display_name: string; + email: string; + password: string; + token_id: string; +} + +export interface SignInPayload { + email: string; + password: string; +} + +export interface ResetPasswordPayload { + email: string; + new_password: string; + token_id: string; +} + +export interface ChangePasswordPayload { + old_password: string; + new_password: string; +} + export const apiCreateOTP = async (email: string, token_type: number = 2) => { const response = await api.post(API.Auth.CREATEOTP, { email, @@ -16,7 +39,7 @@ export const apiVerifyOTP = async (email: string, token: string, token_type: num return response.data; }; -export const apiSignUp = async (payload: any) => { +export const apiSignUp = async (payload: SignUpPayload) => { const response = await api.post(API.Auth.SIGNUP, payload); return response.data; }; @@ -27,14 +50,14 @@ export const apiLogout = async () => { return response.data; }; -export const apiSignIn = async (payload: any) => { +export const apiSignIn = async (payload: SignInPayload) => { const response = await api.post(API.Auth.SIGNIN, payload); const tokens = extractTokensFromResponsePayload(response?.data); if (tokens) setStoredTokens(tokens); return response.data; }; -export const apiResetPassword = async (payload: any) => { +export const apiResetPassword = async (payload: ResetPasswordPayload) => { const response = await api.post(API.Auth.FORGOT_PASSWORD, payload); return response.data; }; @@ -44,7 +67,8 @@ export const apiGetCurrentUser = async () => { return response?.data; }; -export const apiChangePassword = async (payload: any) => { +export const apiChangePassword = async (payload: ChangePasswordPayload) => { const response = await api.patch(API.User.CHANGE_PASSWORD, payload); return response?.data; }; + diff --git a/src/service/historianService.ts b/src/service/historianService.ts index 49e50bb..03cb303 100644 --- a/src/service/historianService.ts +++ b/src/service/historianService.ts @@ -1,16 +1,30 @@ import api from "@/config/config"; import { API } from "../../api"; -export const createHistorianCV = async (payload: any) => { +export interface CreateHistorianCVPayload { + content: string; + media_ids: (string | number)[]; + verify_type: string; +} + +export interface GetUserApplicationsPayload { + page?: number; + limit?: number; + status?: string; + user_id?: string; +} + +export const createHistorianCV = async (payload: CreateHistorianCVPayload) => { const response = await api.post(API.Historian.CREATE_CV, payload); return response?.data; }; -export const apiGetUserApplications = async (payload :any) => { +export const apiGetUserApplications = async (payload : GetUserApplicationsPayload) => { const response = await api.get(API.Historian.APPLICATION, { params: payload }); return response?.data; }; + export const apiDeleteHistorianCV = async (id: number | string) => { const response = await api.delete(API.Historian.DELETE_CV(id)); return response?.data; diff --git a/src/service/mediaService.ts b/src/service/mediaService.ts index 2a42f9f..dac9dcf 100644 --- a/src/service/mediaService.ts +++ b/src/service/mediaService.ts @@ -121,7 +121,16 @@ export const deleteMediaById = async (mediaId: string) => { return response?.data; } -export const getMedia = async (payload: any) => { +export interface GetMediaPayload { + page?: number; + limit?: number; + search?: string; + sortBy?: string; + sortOrder?: "asc" | "desc"; + user_id?: string; +} + +export const getMedia = async (payload: GetMediaPayload) => { const response = await api.get(API.Media.GET_MEDIA, { params: payload, }); diff --git a/src/uhm/api/geometries.ts b/src/uhm/api/geometries.ts index cdca56f..639b8a2 100644 --- a/src/uhm/api/geometries.ts +++ b/src/uhm/api/geometries.ts @@ -9,8 +9,9 @@ export type { GeometriesBBoxQuery } from "@/uhm/types/api"; export type EntityGeometrySearchGeo = { id: string; type: string | null; - draw_geometry: unknown; - binding?: unknown; + draw_geometry: Geometry; + binding?: string[]; + time_start?: number | null; time_end?: number | null; }; @@ -106,8 +107,9 @@ export async function searchGeometriesByEntityName( type GeometryRow = { id: string; geo_type: number; - draw_geometry: unknown; - binding?: unknown; + draw_geometry: Geometry; + binding?: string[]; + time_start?: number; time_end?: number; bbox?: { diff --git a/src/uhm/api/wikis.ts b/src/uhm/api/wikis.ts index fc3f277..8cef7da 100644 --- a/src/uhm/api/wikis.ts +++ b/src/uhm/api/wikis.ts @@ -4,20 +4,21 @@ import { ApiError, requestJson } from "@/uhm/api/http"; export type Wiki = { id: string; - project_id?: string; + project_id: string; title?: string; slug?: string | null; content?: string; is_deleted?: boolean; created_at?: string; updated_at?: string; - content_sample?:{ - created_at?: string; - content?: string; - id?: string; + content_sample?: { + id: string; + title: string; + created_at: string; }[]; }; + export async function searchWikisByTitle(title: string, options?: { limit?: number; cursor?: string; entityId?: string }): Promise { const keyword = title.trim(); const params = new URLSearchParams({ title: keyword }); diff --git a/src/uhm/lib/editor/background/backgroundVisibilityStorage.ts b/src/uhm/lib/editor/background/backgroundVisibilityStorage.ts index 26204bf..d949ecb 100644 --- a/src/uhm/lib/editor/background/backgroundVisibilityStorage.ts +++ b/src/uhm/lib/editor/background/backgroundVisibilityStorage.ts @@ -41,7 +41,9 @@ export function persistBackgroundLayerVisibility(visibility: BackgroundLayerVisi function normalizeBackgroundLayerVisibility(raw: unknown): BackgroundLayerVisibility | null { if (!raw || typeof raw !== "object") return null; - const source = raw as Record; + const source = raw as Partial>; + + const next: BackgroundLayerVisibility = { ...DEFAULT_BACKGROUND_LAYER_VISIBILITY, }; diff --git a/src/uhm/lib/editor/snapshot/editorSnapshot.ts b/src/uhm/lib/editor/snapshot/editorSnapshot.ts index d142388..430e617 100644 --- a/src/uhm/lib/editor/snapshot/editorSnapshot.ts +++ b/src/uhm/lib/editor/snapshot/editorSnapshot.ts @@ -3,7 +3,8 @@ import { normalizeGeoTypeKey, typeKeyToGeoTypeCode } from "@/uhm/lib/map/geo/geo import type { Change } from "@/uhm/lib/editor/draft/editorTypes"; import type { EntitySnapshot } from "@/uhm/types/entities"; import type { EntitySnapshotOperation } from "@/uhm/types/entities"; -import type { Feature, FeatureCollection, GeometryEntitySnapshot, GeometrySnapshot } from "@/uhm/types/geo"; +import type { Feature, FeatureCollection, Geometry, GeometryEntitySnapshot, GeometrySnapshot } from "@/uhm/types/geo"; + import type { EditorSnapshot, Project } from "@/uhm/types/projects"; import type { WikiSnapshot } from "@/uhm/types/wiki"; import type { EntityWikiLinkSnapshot } from "@/uhm/types/projects"; @@ -14,6 +15,56 @@ function isRecord(value: unknown): value is UnknownRecord { return !!value && typeof value === "object" && !Array.isArray(value); } +interface RawEntityRow extends UnknownRecord { + id?: string | number; + operation?: string; + source?: string; + ref?: { id?: string }; + name?: string; + description?: string; + status?: number; +} + +interface RawGeometryRow extends UnknownRecord { + id?: string | number; + operation?: string; + source?: string; + ref?: { id?: string }; + type?: string | number; + geo_type?: string | number; + draw_geometry?: Geometry; + geometry?: Geometry; + binding?: string[]; + time_start?: number; + time_end?: number; +} + +interface RawWikiRow extends UnknownRecord { + id?: string; + operation?: string; + source?: string; + ref?: { id?: string }; + title?: string; + slug?: string; + doc?: string; + updated_at?: string | number; +} + +interface RawGeometryEntityRow extends UnknownRecord { + geometry_id?: string | number; + entity_id?: string | number; + operation?: string; + base_links_hash?: string; +} + +interface RawEntityWikiRow extends UnknownRecord { + entity_id?: string; + wiki_id?: string; + operation?: string; + is_deleted?: boolean | number; +} + + function sanitizeEntitySnapshotOperation(op: unknown): EntitySnapshotOperation { if (typeof op !== "string") return "reference"; const v = op.trim(); @@ -132,10 +183,11 @@ export function normalizeEditorSnapshot(raw: unknown): EditorSnapshot | null { ? geometryEntityRaw .filter(isRecord) .map((r) => { - const geometry_id = getStringId(r.geometry_id); - const entity_id = typeof r.entity_id === "string" ? r.entity_id : ""; + const row = r as RawGeometryEntityRow; + const geometry_id = getStringId(row.geometry_id); + const entity_id = typeof row.entity_id === "string" ? row.entity_id : ""; return { - ...(r as unknown as Omit), + ...(row as unknown as Omit), geometry_id, entity_id, }; @@ -191,16 +243,17 @@ export function normalizeEditorSnapshot(raw: unknown): EditorSnapshot | null { const links = geometryEntity || migratedGeometryEntity || []; const byGeom = new Map(); for (const row of links) { - if ((row as any)?.operation === "delete") continue; + if ((row as RawGeometryEntityRow).operation === "delete") continue; const list = byGeom.get(row.geometry_id) || []; list.push(row.entity_id); byGeom.set(row.geometry_id, list); } const entityNameById = new Map(); - for (const row of entities || []) { + for (const r of entities || []) { + const row = r as RawEntityRow; const id = typeof row?.id === "string" ? row.id : ""; if (!id) continue; - const name = typeof (row as any)?.name === "string" ? String((row as any).name).trim() : ""; + const name = typeof row.name === "string" ? String(row.name).trim() : ""; if (name) entityNameById.set(id, name); } const geometryById = new Map(); @@ -327,7 +380,7 @@ export function buildEditorSnapshot(options: { ? cloned.name.trim() : id; const source: "inline" | "ref" = cloned.source === "inline" ? "inline" : "ref"; - const opRaw = sanitizeEntitySnapshotOperation((cloned as any).operation); + const opRaw = sanitizeEntitySnapshotOperation((cloned as RawEntityRow).operation); // Editor state should delete objects by removing them from the list. // Keep this defensive guard to avoid emitting delete markers unexpectedly. if (opRaw === "delete") continue; @@ -417,13 +470,14 @@ export function buildEditorSnapshot(options: { } const baselineGeometryEntity = new globalThis.Map(); - for (const row of options.previousSnapshot?.geometry_entity || []) { + for (const r of options.previousSnapshot?.geometry_entity || []) { + const row = r as RawGeometryEntityRow; if (!row) continue; - if ((row as any).operation === "delete") continue; + if (row.operation === "delete") continue; const geometry_id = typeof row.geometry_id === "string" || typeof row.geometry_id === "number" ? String(row.geometry_id).trim() : ""; const entity_id = typeof row.entity_id === "string" || typeof row.entity_id === "number" ? String(row.entity_id).trim() : ""; if (!geometry_id || !entity_id) continue; - baselineGeometryEntity.set(`${geometry_id}::${entity_id}`, (row as any).base_links_hash); + baselineGeometryEntity.set(`${geometry_id}::${entity_id}`, row.base_links_hash); } const currentGeometryEntityRows: GeometryEntitySnapshot[] = []; @@ -474,7 +528,7 @@ export function buildEditorSnapshot(options: { const previousWikis = new globalThis.Map(); for (const item of options.previousSnapshot?.wikis || []) { if (!item || typeof item !== "object") continue; - if ((item as any).operation === "delete") continue; + if ((item as RawWikiRow).operation === "delete") continue; const id = (item as WikiSnapshot).id; if (typeof id === "string" && id.length > 0) previousWikis.set(id, item as WikiSnapshot); } @@ -537,24 +591,26 @@ export function buildEditorSnapshot(options: { for (const prev of previousWikis.values()) { if (!prev?.id) continue; if (currentWikiIds.has(prev.id)) continue; + const row = prev as RawWikiRow; deletedWikis.push({ id: prev.id, source: prev.source === "inline" ? "inline" : "ref", operation: "delete", title: typeof prev.title === "string" ? prev.title : "Untitled wiki", - slug: (prev as any).slug ?? null, - doc: (prev as any).doc ?? null, - updated_at: (prev as any).updated_at ?? undefined, + slug: row.slug ?? null, + doc: row.doc ?? null, + updated_at: row.updated_at ?? undefined, } as WikiSnapshot); } const wikis = [...wikisCurrent, ...deletedWikis]; const baselineEntityWiki = new Set(); - for (const row of options.previousSnapshot?.entity_wiki || []) { - if (!row || typeof (row as any).entity_id !== "string" || typeof (row as any).wiki_id !== "string") continue; - if ((row as any).operation === "delete") continue; - const entity_id = (row as any).entity_id.trim(); - const wiki_id = (row as any).wiki_id.trim(); + for (const r of options.previousSnapshot?.entity_wiki || []) { + const row = r as RawEntityWikiRow; + if (!row || typeof row.entity_id !== "string" || typeof row.wiki_id !== "string") continue; + if (row.operation === "delete") continue; + const entity_id = row.entity_id.trim(); + const wiki_id = row.wiki_id.trim(); if (!entity_id || !wiki_id) continue; baselineEntityWiki.add(`${entity_id}::${wiki_id}`); } @@ -591,7 +647,7 @@ export function buildEditorSnapshot(options: { source: e.source, operation: e.operation, name: typeof e.name === "string" ? e.name : undefined, - description: typeof (e as any).description === "string" ? (e as any).description : (e as any).description ?? null, + description: typeof (e as RawEntityRow).description === "string" ? (e as RawEntityRow).description : (e as RawEntityRow).description ?? null, })) .sort((a, b) => String(a.id).localeCompare(String(b.id))), geometries: geometries.slice().sort((a, b) => String(a.id).localeCompare(String(b.id))), @@ -602,8 +658,8 @@ export function buildEditorSnapshot(options: { source: w.source, operation: w.operation, title: w.title, - slug: (w as any).slug ?? null, - doc: (w as any).doc ?? null, + slug: (w as RawWikiRow).slug ?? null, + doc: (w as RawWikiRow).doc ?? null, })) .sort((a, b) => a.id.localeCompare(b.id)), entity_wiki: entityWikis, @@ -640,7 +696,7 @@ function dedupeAndSortGeometryEntity(rows: GeometryEntitySnapshot[]): GeometryEn const geometry_id = typeof row.geometry_id === "string" ? row.geometry_id : ""; const entity_id = typeof row.entity_id === "string" ? row.entity_id : ""; if (!geometry_id || !entity_id) continue; - const opRaw = (row as any).operation; + const opRaw = (row as RawGeometryEntityRow).operation; const operation: GeometryEntitySnapshot["operation"] = opRaw === "delete" ? "delete" diff --git a/src/uhm/types/api.ts b/src/uhm/types/api.ts index 7fea91a..45c8ba4 100644 --- a/src/uhm/types/api.ts +++ b/src/uhm/types/api.ts @@ -4,8 +4,14 @@ export type ApiEnvelope = { status: boolean | "success" | "error" | string; data?: T; message?: string; - errors?: unknown; - pagination?: unknown; + errors?: string | Record | null; + pagination?: { + current_page: number; + page_size: number; + total_records: number; + total_pages: number; + } | null; + }; export type GeometriesBBoxQuery = { diff --git a/src/uhm/types/wiki.ts b/src/uhm/types/wiki.ts index 2ab0aaf..29bd6b2 100644 --- a/src/uhm/types/wiki.ts +++ b/src/uhm/types/wiki.ts @@ -2,15 +2,22 @@ // FE stores Tiptap JSON as a JSON-stringified payload. export type WikiDoc = string | null; +export type WikiContentSample = { + id: string; + title: string; + created_at: string; +}; + export type WikiSnapshotOperation = "create" | "update" | "delete" | "reference"; export type WikiSnapshot = { id: string; source: "inline" | "ref"; - // Optional for backwards-compat with older commits. New commits should include it. operation?: WikiSnapshotOperation; title: string; slug?: string | null; doc: WikiDoc; + content_sample?: WikiContentSample[]; updated_at?: string; }; +