203 lines
8.8 KiB
TypeScript
203 lines
8.8 KiB
TypeScript
"use client";
|
|
|
|
import { useCallback, useMemo, useState } from "react";
|
|
import { CalendarClock, Camera } from "lucide-react";
|
|
import { BrapiEntityPage, type BrapiFormField } from "@/components/brapi/BrapiEntityPage";
|
|
import { Tabs, TabsContent, TabsList, TabsTrigger } from "@/components/ui/tabs";
|
|
import {
|
|
createEventRow,
|
|
createImageRow,
|
|
deleteEventRow,
|
|
deleteImageRow,
|
|
fetchEventImageOptions,
|
|
fetchEventRows,
|
|
fetchImageRows,
|
|
updateEventRow,
|
|
updateImageRow,
|
|
} from "./api";
|
|
import { ImageFormUpload } from "./components/ImageFormUpload";
|
|
import { NONE_SELECT_VALUE, type SelectOption } from "./types";
|
|
|
|
const eventTypeOptions: SelectOption[] = [
|
|
{ value: "observation", label: "observation / 观察测量" },
|
|
{ value: "planting", label: "planting / 播种" },
|
|
{ value: "fertilizer", label: "fertilizer / 施肥" },
|
|
{ value: "irrigation", label: "irrigation / 灌溉" },
|
|
{ value: "tillage", label: "tillage / 耕作整地" },
|
|
{ value: "chemicals", label: "chemicals / 药剂处理" },
|
|
{ value: "weeding", label: "weeding / 除草" },
|
|
{ value: "harvest", label: "harvest / 收获" },
|
|
{ value: "other", label: "other / 其他" },
|
|
];
|
|
|
|
const mimeTypeOptions: SelectOption[] = [
|
|
{ value: "image/jpeg", label: "image/jpeg" },
|
|
{ value: "image/jpg", label: "image/jpg" },
|
|
{ value: "image/png", label: "image/png" },
|
|
{ value: "image/gif", label: "image/gif" },
|
|
{ value: "image/webp", label: "image/webp" },
|
|
{ value: "image/bmp", label: "image/bmp" },
|
|
];
|
|
|
|
const formatDateRange = (start: unknown, end: unknown) => {
|
|
const startText = String(start ?? "").trim();
|
|
const endText = String(end ?? "").trim();
|
|
if (!startText && !endText) return "N/A";
|
|
if (startText && endText) return `${startText} ~ ${endText}`;
|
|
return startText || endText;
|
|
};
|
|
|
|
const eventTypeLabel = (value: unknown) => {
|
|
const text = String(value ?? "").trim();
|
|
const match = eventTypeOptions.find((option) => option.value === text);
|
|
return match?.label || text || "N/A";
|
|
};
|
|
|
|
export default function EventImagePage() {
|
|
const [studyOptions, setStudyOptions] = useState<SelectOption[]>([]);
|
|
const [observationUnitOptions, setObservationUnitOptions] = useState<SelectOption[]>([]);
|
|
const [observationOptions, setObservationOptions] = useState<SelectOption[]>([]);
|
|
|
|
const loadOptions = useCallback(async () => {
|
|
const options = await fetchEventImageOptions();
|
|
setStudyOptions(options.studies);
|
|
setObservationUnitOptions(options.observationUnits);
|
|
setObservationOptions(options.observations);
|
|
}, []);
|
|
|
|
const loadEvents = useCallback(async () => {
|
|
const [, rows] = await Promise.all([loadOptions(), fetchEventRows()]);
|
|
return rows as unknown as Record<string, unknown>[];
|
|
}, [loadOptions]);
|
|
|
|
const loadImages = useCallback(async () => {
|
|
const [, rows] = await Promise.all([loadOptions(), fetchImageRows()]);
|
|
return rows as unknown as Record<string, unknown>[];
|
|
}, [loadOptions]);
|
|
|
|
const eventFields = useMemo<BrapiFormField[]>(() => [
|
|
{ key: "id", label: "Event ID", type: "text", required: true, placeholder: "event-001" },
|
|
{ key: "event_type", label: "Event Type", type: "select", required: true, options: eventTypeOptions },
|
|
{ key: "event_type_db_id", label: "Event Type DbId", type: "text", placeholder: "CO:0000000" },
|
|
{
|
|
key: "study_db_id",
|
|
label: "Study",
|
|
type: "select",
|
|
options: [{ value: NONE_SELECT_VALUE, label: "No study" }, ...studyOptions],
|
|
},
|
|
{ key: "start_date", label: "Start Date", type: "date" },
|
|
{ key: "end_date", label: "End Date", type: "date" },
|
|
{
|
|
key: "observation_unit_db_ids",
|
|
label: "ObservationUnit IDs",
|
|
type: "text",
|
|
placeholder: "ou-plot-001, ou-plot-002",
|
|
colSpan: 2,
|
|
},
|
|
{ key: "event_description", label: "Description", type: "textarea", colSpan: 2 },
|
|
], [studyOptions]);
|
|
|
|
const imageFields = useMemo<BrapiFormField[]>(() => [
|
|
{ key: "id", label: "Image ID", type: "text", required: true, readOnly: true, placeholder: "上传图片后自动生成" },
|
|
{ key: "image_name", label: "Image Name", type: "text", required: true, readOnly: true, placeholder: "上传图片后自动填写" },
|
|
{ key: "image_file_name", label: "File Name", type: "text", readOnly: true, placeholder: "上传图片后自动填写" },
|
|
{ key: "image_url", label: "Image URL", type: "text", required: true, readOnly: true, placeholder: "上传图片后自动填写", colSpan: 2 },
|
|
{ key: "mime_type", label: "MIME Type", type: "select", readOnly: true, options: mimeTypeOptions },
|
|
{ key: "image_time_stamp", label: "Image Date", type: "date", readOnly: true },
|
|
{
|
|
key: "study_db_id",
|
|
label: "Study",
|
|
type: "select",
|
|
options: [{ value: NONE_SELECT_VALUE, label: "No study" }, ...studyOptions],
|
|
},
|
|
{
|
|
key: "observation_unit_db_id",
|
|
label: "ObservationUnit",
|
|
type: "select",
|
|
options: [{ value: NONE_SELECT_VALUE, label: "No observation unit" }, ...observationUnitOptions],
|
|
},
|
|
{
|
|
key: "observation_db_ids",
|
|
label: "Observation IDs",
|
|
type: "text",
|
|
placeholder: observationOptions.slice(0, 3).map((option) => option.value).join(", ") || "obs-001, obs-002",
|
|
colSpan: 2,
|
|
},
|
|
{ key: "description", label: "Description", type: "textarea", colSpan: 2 },
|
|
], [observationOptions, observationUnitOptions, studyOptions]);
|
|
|
|
const validateImageForm = useCallback((payload: Record<string, unknown>) => {
|
|
const imageUrl = String(payload.image_url ?? "").trim();
|
|
if (!imageUrl) {
|
|
return "请先上传图片,上传成功后系统会自动填充带 * 的图片字段。";
|
|
}
|
|
return null;
|
|
}, []);
|
|
|
|
return (
|
|
<Tabs defaultValue="events" className="flex min-h-full flex-col gap-4">
|
|
<TabsList className="w-full justify-start overflow-x-auto rounded-lg border bg-white p-1 dark:border-slate-800 dark:bg-slate-950 sm:w-fit">
|
|
<TabsTrigger value="events" className="gap-2"><CalendarClock className="h-4 w-4" />Events</TabsTrigger>
|
|
<TabsTrigger value="images" className="gap-2"><Camera className="h-4 w-4" />Images</TabsTrigger>
|
|
</TabsList>
|
|
|
|
<TabsContent value="events" className="mt-0 min-h-0 flex-1">
|
|
<BrapiEntityPage
|
|
icon={CalendarClock}
|
|
iconBg="bg-gradient-to-br from-fuchsia-500 to-pink-600"
|
|
title="Event Management"
|
|
description="Manage phenotyping and field operation events through /brapi/v2/events."
|
|
addLabel="New Event"
|
|
columns={[
|
|
{ key: "eventDbId", label: "Event ID" },
|
|
{ key: "event_type", label: "Event Type", render: eventTypeLabel },
|
|
{ key: "study_db_id", label: "Study" },
|
|
{
|
|
key: "start_date",
|
|
label: "Date Range",
|
|
render: (_value, row) => formatDateRange(row.start_date, row.end_date),
|
|
},
|
|
{ key: "event_description", label: "Description" },
|
|
{ key: "observation_unit_db_ids", label: "Observation Units" },
|
|
]}
|
|
fields={eventFields}
|
|
data={[]}
|
|
stats={[{ label: "/brapi/v2/events", value: "BrAPI", className: "bg-fuchsia-50 text-fuchsia-700 dark:bg-fuchsia-400/10 dark:text-fuchsia-200" }]}
|
|
loadData={loadEvents}
|
|
createRecord={(payload) => createEventRow(payload) as unknown as Promise<Record<string, unknown>>}
|
|
updateRecord={(id, payload) => updateEventRow(id, payload) as unknown as Promise<Record<string, unknown>>}
|
|
deleteRecord={deleteEventRow}
|
|
/>
|
|
</TabsContent>
|
|
|
|
<TabsContent value="images" className="mt-0 min-h-0 flex-1">
|
|
<BrapiEntityPage
|
|
icon={Camera}
|
|
iconBg="bg-gradient-to-br from-purple-500 to-indigo-600"
|
|
title="Image Management"
|
|
description="Manage field image metadata and links through /brapi/v2/images."
|
|
addLabel="New Image"
|
|
columns={[
|
|
{ key: "imageDbId", label: "Image ID" },
|
|
{ key: "image_name", label: "Image Name" },
|
|
{ key: "study_db_id", label: "Study" },
|
|
{ key: "observation_unit_db_id", label: "Observation Unit" },
|
|
{ key: "mime_type", label: "MIME Type" },
|
|
{ key: "image_time_stamp", label: "Image Date" },
|
|
{ key: "image_url", label: "Image URL" },
|
|
]}
|
|
fields={imageFields}
|
|
data={[]}
|
|
stats={[{ label: "/brapi/v2/images", value: "BrAPI", className: "bg-purple-50 text-purple-700 dark:bg-purple-400/10 dark:text-purple-200" }]}
|
|
loadData={loadImages}
|
|
renderFormExtra={(props) => <ImageFormUpload {...props} />}
|
|
validateForm={validateImageForm}
|
|
createRecord={(payload) => createImageRow(payload) as unknown as Promise<Record<string, unknown>>}
|
|
updateRecord={(id, payload) => updateImageRow(id, payload) as unknown as Promise<Record<string, unknown>>}
|
|
deleteRecord={deleteImageRow}
|
|
/>
|
|
</TabsContent>
|
|
</Tabs>
|
|
);
|
|
}
|