mirror of
https://github.com/Tria-plc/edr-platform.git
synced 2026-08-27 00:52:50 +00:00
improve endpoints
This commit is contained in:
@@ -1 +0,0 @@
|
||||
VITE_API_URL=http://localhost:3001
|
||||
@@ -0,0 +1,3 @@
|
||||
export const FILE_SETTINGS = {
|
||||
CUSTOMER_REGISTRATION: "customer_registration"
|
||||
}
|
||||
@@ -0,0 +1,8 @@
|
||||
export const QUERY_KEYS = {
|
||||
USERS: "users",
|
||||
CUSTOMER: "Customers",
|
||||
FILES: {
|
||||
FILE_UPLOAD_SETTINGS: "file-upload-settings",
|
||||
BY_CODE: "by-code"
|
||||
}
|
||||
}
|
||||
@@ -36,6 +36,7 @@ export const URL_CONSTANTS = {
|
||||
BASE: "/files",
|
||||
UPLOAD: "/files/upload",
|
||||
FILE_UPLOAD_SETTINGS: "/files/upload",
|
||||
FILE_UPLOAD_SETTINGS_BY_CODE: "/api/file-upload-settings/by-code",
|
||||
DOWNLOAD: (id: string | number) => `/files/${id}/download`,
|
||||
DELETE: (id: string | number) => `/files/${id}`,
|
||||
BY_ID: (id: string | number) => `/files/${id}`,
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import type { ReactNode } from "react";
|
||||
import { IFileUploadSetting } from "@edr/types/freight";
|
||||
import { useState, type ReactNode } from "react";
|
||||
|
||||
import {
|
||||
Dialog,
|
||||
@@ -11,6 +12,7 @@ import {
|
||||
Label,
|
||||
Button,
|
||||
Textarea,
|
||||
SmartFileInput,
|
||||
} from "@edr/ui-common";
|
||||
|
||||
import {
|
||||
@@ -22,6 +24,9 @@ import {
|
||||
MapPin,
|
||||
FileText,
|
||||
} from "lucide-react";
|
||||
import { getFileUploadSettingByCode } from "@/services/fileUploadSettings.service";
|
||||
import { FILE_SETTINGS } from "@/constants/FILE_SETTINGS";
|
||||
import { useQuery } from "@tanstack/react-query";
|
||||
|
||||
export interface CustomerFormData {
|
||||
companyName?: string;
|
||||
@@ -46,7 +51,78 @@ export default function NewCustomerPage({
|
||||
mode = "create",
|
||||
customer,
|
||||
children,
|
||||
}: NewCustomerPageProps = {}) {
|
||||
}: NewCustomerPageProps = {}) { //getFileUploadSettingByCode
|
||||
const [files, setFiles] = useState<
|
||||
Record<string, File | File[] | null>
|
||||
>({});
|
||||
|
||||
const { data: customerRegistrationFiles, isLoading, isError, error } = useQuery(
|
||||
getFileUploadSettingByCode.queryOptions({
|
||||
input: FILE_SETTINGS.CUSTOMER_REGISTRATION,
|
||||
})
|
||||
);
|
||||
console.log("customerRegistrationFiles", customerRegistrationFiles)
|
||||
// const shipmentFileUploadSetting: IFileUploadSetting = {
|
||||
// id: "setting_1",
|
||||
// code: "shipment_documents",
|
||||
// label: "Shipment Documents",
|
||||
// description: "Upload all required shipment-related documents",
|
||||
|
||||
// entity: "shipment", // depends on your FileUploadEntity enum/type
|
||||
|
||||
// fields: [
|
||||
// {
|
||||
// id: "field_1",
|
||||
// fileKey: "invoice",
|
||||
// fileLabel: "Invoice",
|
||||
// order: 1,
|
||||
// isRequired: true,
|
||||
// isMultiple: false,
|
||||
// maxFiles: 1,
|
||||
// maxSizeMb: 5,
|
||||
// allowedExtensions: ["pdf", "jpg", "png"],
|
||||
// helpText: "Upload commercial invoice",
|
||||
// settingId: "",
|
||||
// createdAt: "",
|
||||
// updatedAt: ""
|
||||
// },
|
||||
|
||||
// {
|
||||
// id: "field_2",
|
||||
// fileKey: "packing_list",
|
||||
// fileLabel: "Packing List",
|
||||
// order: 2,
|
||||
// isRequired: false,
|
||||
// isMultiple: true,
|
||||
// maxFiles: 3,
|
||||
// maxSizeMb: 10,
|
||||
// allowedExtensions: ["pdf", "xlsx"],
|
||||
// helpText: "Optional packing list documents",
|
||||
// settingId: "",
|
||||
// createdAt: "",
|
||||
// updatedAt: ""
|
||||
// },
|
||||
|
||||
// {
|
||||
// id: "field_3",
|
||||
// fileKey: "cargo_images",
|
||||
// fileLabel: "Cargo Images",
|
||||
// order: 3,
|
||||
// isRequired: false,
|
||||
// isMultiple: true,
|
||||
// maxFiles: 5,
|
||||
// maxSizeMb: 2,
|
||||
// allowedExtensions: ["jpg", "jpeg", "png"],
|
||||
// helpText: "Photos of cargo condition",
|
||||
// settingId: "",
|
||||
// createdAt: "",
|
||||
// updatedAt: ""
|
||||
// },
|
||||
// ],
|
||||
// createdAt: "",
|
||||
// updatedAt: ""
|
||||
// };
|
||||
|
||||
const isEdit = mode === "edit";
|
||||
const title = isEdit ? "Edit Customer" : "New Customer";
|
||||
const description = isEdit
|
||||
@@ -208,7 +284,16 @@ export default function NewCustomerPage({
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
{
|
||||
customerRegistrationFiles &&
|
||||
<SmartFileInput
|
||||
file={customerRegistrationFiles}
|
||||
value={files}
|
||||
onChange={setFiles}
|
||||
/>
|
||||
}
|
||||
</div>
|
||||
<div className="flex justify-end gap-3">
|
||||
<Button variant="outline">Cancel</Button>
|
||||
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import { api } from "./crud";
|
||||
import { endpoint, unwrap } from "@/utils/endpoint";
|
||||
import { client } from "@/utils/api";
|
||||
|
||||
import type {
|
||||
CreateFileUploadFieldDto,
|
||||
@@ -8,6 +9,9 @@ import type {
|
||||
UpdateFileUploadFieldDto,
|
||||
UpdateFileUploadSettingDto,
|
||||
} from "@/types/fileUploadSettings";
|
||||
import { URL_CONSTANTS } from "@/constants/URLS";
|
||||
import { QUERY_KEYS } from "@/constants/TANSTACK_QUEY_KEY";
|
||||
import { ApiResponse } from "@/types/apiResponse";
|
||||
|
||||
const BASE = "/api/file-upload-settings";
|
||||
|
||||
@@ -18,25 +22,24 @@ const BASE = "/api/file-upload-settings";
|
||||
*/
|
||||
type Envelope<T> = { data: T } | T;
|
||||
|
||||
function unwrap<T>(payload: Envelope<T>): T {
|
||||
if (
|
||||
payload &&
|
||||
typeof payload === "object" &&
|
||||
"data" in (payload as object)
|
||||
) {
|
||||
return (payload as { data: T }).data;
|
||||
}
|
||||
// function unwrap<T>(payload: Envelope<T>): T {
|
||||
// if (
|
||||
// payload &&
|
||||
// typeof payload === "object" &&
|
||||
// "data" in (payload as object)
|
||||
// ) {
|
||||
// return (payload as { data: T }).data;
|
||||
// }
|
||||
|
||||
return payload as T;
|
||||
}
|
||||
// return payload as T;
|
||||
// }
|
||||
|
||||
export const fileUploadSettingsService = {
|
||||
// GET /file-upload-settings
|
||||
list: async (): Promise<FileUploadSetting[]> => {
|
||||
const response =
|
||||
await api.get<Envelope<FileUploadSetting[]>>(BASE);
|
||||
|
||||
return unwrap(response);
|
||||
await client.get<ApiResponse<FileUploadSetting[]>>(BASE);
|
||||
return unwrap(response.data);
|
||||
},
|
||||
|
||||
// GET /file-upload-settings/:id
|
||||
@@ -44,11 +47,11 @@ export const fileUploadSettingsService = {
|
||||
id: string
|
||||
): Promise<FileUploadSetting> => {
|
||||
const response =
|
||||
await api.get<Envelope<FileUploadSetting>>(
|
||||
await client.get<ApiResponse<FileUploadSetting>>(
|
||||
`${BASE}/${id}`
|
||||
);
|
||||
|
||||
return unwrap(response);
|
||||
return unwrap(response.data);
|
||||
},
|
||||
|
||||
// GET /file-upload-settings/by-code/:code
|
||||
@@ -56,11 +59,11 @@ export const fileUploadSettingsService = {
|
||||
code: string
|
||||
): Promise<FileUploadSetting> => {
|
||||
const response =
|
||||
await api.get<Envelope<FileUploadSetting>>(
|
||||
await client.get<ApiResponse<FileUploadSetting>>(
|
||||
`${BASE}/by-code/${encodeURIComponent(code)}`
|
||||
);
|
||||
|
||||
return unwrap(response);
|
||||
return unwrap(response.data);
|
||||
},
|
||||
|
||||
// POST /file-upload-settings
|
||||
@@ -68,12 +71,12 @@ export const fileUploadSettingsService = {
|
||||
payload: CreateFileUploadSettingDto
|
||||
): Promise<FileUploadSetting> => {
|
||||
const response =
|
||||
await api.post<Envelope<FileUploadSetting>>(
|
||||
await client.post<ApiResponse<FileUploadSetting>>(
|
||||
BASE,
|
||||
payload
|
||||
);
|
||||
|
||||
return unwrap(response);
|
||||
return unwrap(response.data);
|
||||
},
|
||||
|
||||
// PATCH /file-upload-settings/:id
|
||||
@@ -82,17 +85,17 @@ export const fileUploadSettingsService = {
|
||||
payload: UpdateFileUploadSettingDto
|
||||
): Promise<FileUploadSetting> => {
|
||||
const response =
|
||||
await api.patch<Envelope<FileUploadSetting>>(
|
||||
await client.patch<ApiResponse<FileUploadSetting>>(
|
||||
`${BASE}/${id}`,
|
||||
payload
|
||||
);
|
||||
|
||||
return unwrap(response);
|
||||
return unwrap(response.data);
|
||||
},
|
||||
|
||||
// DELETE /file-upload-settings/:id
|
||||
remove: async (id: string): Promise<void> => {
|
||||
await api.delete(`${BASE}/${id}`);
|
||||
await client.delete(`${BASE}/${id}`);
|
||||
},
|
||||
|
||||
// PUT /file-upload-settings/:id/fields
|
||||
@@ -101,12 +104,11 @@ export const fileUploadSettingsService = {
|
||||
fields: CreateFileUploadFieldDto[]
|
||||
): Promise<FileUploadField[]> => {
|
||||
const response =
|
||||
await api.put<Envelope<FileUploadField[]>>(
|
||||
await client.put<ApiResponse<FileUploadField[]>>(
|
||||
`${BASE}/${id}/fields`,
|
||||
fields
|
||||
);
|
||||
|
||||
return unwrap(response);
|
||||
return unwrap(response.data);
|
||||
},
|
||||
|
||||
// POST /file-upload-settings/:id/fields
|
||||
@@ -115,12 +117,12 @@ export const fileUploadSettingsService = {
|
||||
payload: CreateFileUploadFieldDto
|
||||
): Promise<FileUploadField> => {
|
||||
const response =
|
||||
await api.post<Envelope<FileUploadField>>(
|
||||
await client.post<ApiResponse<FileUploadField>>(
|
||||
`${BASE}/${id}/fields`,
|
||||
payload
|
||||
);
|
||||
|
||||
return unwrap(response);
|
||||
return unwrap(response.data);
|
||||
},
|
||||
|
||||
// PATCH /file-upload-settings/fields/:fieldId
|
||||
@@ -129,20 +131,29 @@ export const fileUploadSettingsService = {
|
||||
payload: UpdateFileUploadFieldDto
|
||||
): Promise<FileUploadField> => {
|
||||
const response =
|
||||
await api.patch<Envelope<FileUploadField>>(
|
||||
await client.patch<ApiResponse<FileUploadField>>(
|
||||
`${BASE}/fields/${fieldId}`,
|
||||
payload
|
||||
);
|
||||
|
||||
return unwrap(response);
|
||||
return unwrap(response.data);
|
||||
},
|
||||
|
||||
// DELETE /file-upload-settings/fields/:fieldId
|
||||
removeField: async (
|
||||
fieldId: string
|
||||
): Promise<void> => {
|
||||
await api.delete(
|
||||
await client.delete(
|
||||
`${BASE}/fields/${fieldId}`
|
||||
);
|
||||
},
|
||||
};
|
||||
};
|
||||
|
||||
|
||||
export const getFileUploadSettingByCode = endpoint<string, FileUploadSetting>(
|
||||
QUERY_KEYS.FILES.FILE_UPLOAD_SETTINGS,
|
||||
QUERY_KEYS.FILES.BY_CODE,
|
||||
(code) =>
|
||||
client
|
||||
.get<ApiResponse<FileUploadSetting>>(`${URL_CONSTANTS.FILES.FILE_UPLOAD_SETTINGS_BY_CODE}/${code}`)
|
||||
.then(res => res.data.data)
|
||||
);
|
||||
5
apps/edr-freight-web/portal/src/types/apiResponse.ts
Normal file
5
apps/edr-freight-web/portal/src/types/apiResponse.ts
Normal file
@@ -0,0 +1,5 @@
|
||||
export type ApiResponse<T> = {
|
||||
success: boolean;
|
||||
data: T;
|
||||
timestamp: string;
|
||||
};
|
||||
@@ -1,11 +1,20 @@
|
||||
import { QueryObserverOptions, UseQueryOptions } from "@tanstack/react-query";
|
||||
import {
|
||||
UseQueryOptions,
|
||||
QueryObserverOptions,
|
||||
} from "@tanstack/react-query";
|
||||
import axios from "axios";
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Axios client
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
export const client = axios.create({
|
||||
baseURL: import.meta.env.VITE_API_URL,
|
||||
});
|
||||
|
||||
// Attach auth token to every request
|
||||
client.interceptors.request.use((config) => {
|
||||
// TODO: replace with secure storage (cookie/localStorage/auth provider)
|
||||
const token = document.cookie
|
||||
.split("; ")
|
||||
.find((row) => row.startsWith("auth-token="))
|
||||
@@ -13,9 +22,11 @@ client.interceptors.request.use((config) => {
|
||||
if (token) {
|
||||
config.headers.Authorization = `Bearer ${token}`;
|
||||
}
|
||||
|
||||
return config;
|
||||
});
|
||||
|
||||
// Handle auth errors globally
|
||||
client.interceptors.response.use(
|
||||
(response) => response,
|
||||
(error) => {
|
||||
@@ -23,90 +34,5 @@ client.interceptors.response.use(
|
||||
window.location.href = "/auth";
|
||||
}
|
||||
return Promise.reject(error);
|
||||
},
|
||||
);
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Types
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
export type QueryConfig<T> = Omit<
|
||||
UseQueryOptions<T, Error, T, readonly unknown[]>,
|
||||
"queryKey" | "queryFn"
|
||||
>;
|
||||
|
||||
export interface EndpointWithInput<TInput, TResponse> {
|
||||
call(input: TInput): Promise<TResponse>;
|
||||
queryKey(): readonly unknown[];
|
||||
queryKey(input: TInput): readonly unknown[];
|
||||
queryOptions(
|
||||
config: { input: TInput } & QueryConfig<TResponse>,
|
||||
): UseQueryOptions<TResponse, Error, TResponse, readonly unknown[]>;
|
||||
}
|
||||
|
||||
export interface EndpointWithoutInput<TResponse> {
|
||||
call(): Promise<TResponse>;
|
||||
queryKey(): readonly unknown[];
|
||||
queryOptions(
|
||||
config?: QueryConfig<TResponse>,
|
||||
): UseQueryOptions<TResponse, Error, TResponse, readonly unknown[]>;
|
||||
}
|
||||
|
||||
export type Endpoint<TInput, TResponse> = TInput extends void
|
||||
? EndpointWithoutInput<TResponse>
|
||||
: EndpointWithInput<TInput, TResponse>;
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Builder
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
export function endpoint<TInput, TResponse>(
|
||||
service: string,
|
||||
action: string,
|
||||
execute: (input: TInput) => Promise<TResponse>,
|
||||
): Endpoint<TInput, TResponse> {
|
||||
const call = (input: TInput) => execute(input);
|
||||
|
||||
const queryKey = (input?: TInput): readonly unknown[] => {
|
||||
if (input === undefined) return [service, action] as const;
|
||||
return [service, action, input] as const;
|
||||
};
|
||||
|
||||
const queryOptions = (
|
||||
config?: Record<string, unknown>,
|
||||
): QueryObserverOptions<
|
||||
TResponse,
|
||||
Error,
|
||||
TResponse,
|
||||
TResponse,
|
||||
readonly unknown[]
|
||||
> => {
|
||||
const input = config?.input as TInput | undefined;
|
||||
const { input: _, ...rest } = config ?? {};
|
||||
const key: readonly unknown[] =
|
||||
input !== undefined ? [service, action, input] : [service, action];
|
||||
return {
|
||||
...rest,
|
||||
queryKey: key,
|
||||
queryFn: () => execute(input as TInput),
|
||||
} as ReturnType<typeof queryOptions>;
|
||||
};
|
||||
|
||||
return { call, queryKey, queryOptions } as Endpoint<TInput, TResponse>;
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Helpers
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
/** Some API endpoints return `{ data: T }`, others return `T` directly. */
|
||||
export function unwrap<T>(response: { data: T } | T): T {
|
||||
if (
|
||||
response &&
|
||||
typeof response === "object" &&
|
||||
"data" in (response as object)
|
||||
) {
|
||||
return (response as { data: T }).data;
|
||||
}
|
||||
return response as T;
|
||||
}
|
||||
);
|
||||
|
||||
94
apps/edr-freight-web/portal/src/utils/endpoint.ts
Normal file
94
apps/edr-freight-web/portal/src/utils/endpoint.ts
Normal file
@@ -0,0 +1,94 @@
|
||||
import {
|
||||
UseQueryOptions,
|
||||
} from "@tanstack/react-query";
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// React Query shared types
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
export type QueryConfig<T> = Omit<
|
||||
UseQueryOptions<T, Error, T, readonly unknown[]>,
|
||||
"queryKey" | "queryFn"
|
||||
>;
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Endpoint interfaces
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
export interface EndpointWithInput<TInput, TResponse> {
|
||||
call(input: TInput): Promise<TResponse>;
|
||||
queryKey(input: TInput): readonly unknown[];
|
||||
queryOptions(
|
||||
config: { input: TInput } & QueryConfig<TResponse>,
|
||||
): UseQueryOptions<TResponse, Error, TResponse, readonly unknown[]>;
|
||||
}
|
||||
|
||||
export interface EndpointWithoutInput<TResponse> {
|
||||
call(): Promise<TResponse>;
|
||||
queryKey(): readonly unknown[];
|
||||
queryOptions(
|
||||
config?: QueryConfig<TResponse>,
|
||||
): UseQueryOptions<TResponse, Error, TResponse, readonly unknown[]>;
|
||||
}
|
||||
|
||||
export type Endpoint<TInput, TResponse> = TInput extends void
|
||||
? EndpointWithoutInput<TResponse>
|
||||
: EndpointWithInput<TInput, TResponse>;
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Endpoint builder
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
export function endpoint<TInput, TResponse>(
|
||||
service: string,
|
||||
action: string,
|
||||
execute: (input: TInput) => Promise<TResponse>,
|
||||
) {
|
||||
const buildKey = (input?: TInput): readonly unknown[] =>
|
||||
input === undefined
|
||||
? [service, action]
|
||||
: [service, action, input];
|
||||
|
||||
const call = (input: TInput) => execute(input);
|
||||
|
||||
const queryKey = (input?: TInput) => buildKey(input);
|
||||
|
||||
const queryOptions = (
|
||||
config?: { input?: TInput } & QueryConfig<TResponse>,
|
||||
): UseQueryOptions<
|
||||
TResponse,
|
||||
Error,
|
||||
TResponse,
|
||||
readonly unknown[]
|
||||
> => {
|
||||
const { input, ...rest } = config ?? {};
|
||||
|
||||
return {
|
||||
...rest,
|
||||
queryKey: buildKey(input),
|
||||
queryFn: () => execute(input as TInput),
|
||||
};
|
||||
};
|
||||
|
||||
return {
|
||||
call,
|
||||
queryKey,
|
||||
queryOptions,
|
||||
};
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Helper utilities
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
export function unwrap<T>(response: { data: T } | T): T {
|
||||
if (
|
||||
response &&
|
||||
typeof response === "object" &&
|
||||
"data" in (response as object)
|
||||
) {
|
||||
return (response as { data: T }).data;
|
||||
}
|
||||
|
||||
return response as T;
|
||||
}
|
||||
@@ -1,5 +1,5 @@
|
||||
export { default as Table } from "./components/Table";
|
||||
export type { TableProps, TableColumn } from "./components/Table";
|
||||
export { Table } from "./components/table";
|
||||
export type { TableProps, TableColumn } from "./components/Table/Table";
|
||||
|
||||
export { FormField } from "./components/Form";
|
||||
export type { FormFieldProps } from "./components/Form";
|
||||
@@ -10,8 +10,8 @@ export type { SmartFileInputProps } from "./components/SmartFileInput";
|
||||
export { default as Modal } from "./components/Modal";
|
||||
export type { ModalProps } from "./components/Modal";
|
||||
|
||||
export { default as Badge } from "./components/Badge";
|
||||
export type { BadgeProps, BadgeTone } from "./components/Badge";
|
||||
export { Badge } from "./components/badge";
|
||||
// export type { BadgeProps } from "./components/badge";
|
||||
|
||||
export { Sidebar, DashboardLayout } from "./components/Layout";
|
||||
|
||||
|
||||
1934
pnpm-lock.yaml
generated
1934
pnpm-lock.yaml
generated
File diff suppressed because it is too large
Load Diff
Reference in New Issue
Block a user