feat: add procurement and sales sections

This commit is contained in:
2026-02-27 14:42:19 -06:00
parent 7486bcf939
commit 5a6970a4c5
24 changed files with 4739 additions and 134 deletions
+4
View File
@@ -9,6 +9,8 @@ import { permission } from "./optima-api/modules/permissions";
import { user } from "./optima-api/modules/user";
import { users } from "./optima-api/modules/users";
import { unifi } from "./optima-api/modules/unifi";
import { procurement } from "./optima-api/modules/procurement";
import { sales } from "./optima-api/modules/sales";
export const optima = {
auth,
@@ -20,6 +22,8 @@ export const optima = {
user,
users,
unifi,
procurement,
sales,
};
/**
* @TODO
+35 -1
View File
@@ -1,4 +1,4 @@
import { error } from "@sveltejs/kit";
import { error, redirect } from "@sveltejs/kit";
export class ApiError extends Error {
constructor(
@@ -11,9 +11,43 @@ export class ApiError extends Error {
}
}
/**
* Detects "invalid signature" or malformed-token errors from the API,
* which indicate the access or refresh token has been tampered with or
* the server signing key has changed.
*/
export function isInvalidSignatureError(err: unknown): boolean {
if (err && typeof err === "object") {
const axiosErr = err as Record<string, unknown>;
const responseData = (axiosErr?.response as Record<string, unknown>)
?.data as Record<string, unknown> | undefined;
const candidates = [
responseData?.message,
responseData?.error,
(err as Error)?.message,
];
return candidates.some((val) => {
if (typeof val !== "string") return false;
const lower = val.toLowerCase();
return (
lower.includes("invalid signature") ||
lower.includes("jwt malformed") ||
lower.includes("invalid token")
);
});
}
return false;
}
export function handleApiError(err: unknown): never {
console.error("API Error:", err);
// Treat invalid-signature errors as a forced logout
if (isInvalidSignatureError(err)) {
console.warn("Invalid token signature detected — forcing logout.");
throw redirect(303, "/logout");
}
if (err instanceof ApiError) {
throw error(err.statusCode, {
message: err.message,
+104
View File
@@ -0,0 +1,104 @@
import api from "../axios";
export const procurement = {
async fetchMany(
accessToken: string,
page: number = 1,
search?: string,
rpp: number = 30,
includeInactive: boolean = false,
) {
const params: Record<string, unknown> = { page, rpp };
if (search && search.length > 0) params.search = search;
if (includeInactive) params.includeInactive = true;
const response = await api.get("/v1/procurement/items", {
params,
headers: {
Authorization: `Bearer ${accessToken}`,
},
});
return response.data;
},
async fetch(
accessToken: string,
identifier: string,
options?: { includeLinkedItems?: boolean },
) {
const params: Record<string, string> = {};
if (options?.includeLinkedItems) params.includeLinkedItems = "true";
const response = await api.get(`/v1/procurement/items/${identifier}`, {
params,
headers: {
Authorization: `Bearer ${accessToken}`,
},
});
return response.data;
},
async count(accessToken: string, activeOnly: boolean = false) {
const params: Record<string, string> = {};
if (activeOnly) params.activeOnly = "true";
const response = await api.get("/v1/procurement/count", {
params,
headers: {
Authorization: `Bearer ${accessToken}`,
},
});
return response.data.data.count;
},
async refreshInventory(accessToken: string, identifier: string) {
const response = await api.post(
`/v1/procurement/items/${identifier}/refresh-inventory`,
{},
{
headers: {
Authorization: `Bearer ${accessToken}`,
},
},
);
return response.data;
},
async fetchLinkedItems(accessToken: string, identifier: string) {
const response = await api.get(
`/v1/procurement/items/${identifier}/linked`,
{
headers: {
Authorization: `Bearer ${accessToken}`,
},
},
);
return response.data;
},
async linkItem(accessToken: string, identifier: string, targetId: string) {
const response = await api.post(
`/v1/procurement/items/${identifier}/link`,
{ targetId },
{
headers: {
Authorization: `Bearer ${accessToken}`,
},
},
);
return response.data;
},
async unlinkItem(accessToken: string, identifier: string, targetId: string) {
const response = await api.post(
`/v1/procurement/items/${identifier}/unlink`,
{ targetId },
{
headers: {
Authorization: `Bearer ${accessToken}`,
},
},
);
return response.data;
},
};
+62
View File
@@ -0,0 +1,62 @@
import api from "../axios";
export interface SalesOpportunity {
id: string;
cwOpportunityId?: number;
name: string;
notes?: string | null;
type?: { id?: number; name?: string } | null;
stage?: { id?: number; name?: string } | null;
status?: { id?: number; name?: string } | null;
priority?: { id?: number; name?: string } | null;
rating?: { id?: number; name?: string } | null;
source?: string | null;
campaign?: string | null;
primarySalesRep?: {
id?: number;
identifier?: string;
name?: string;
} | null;
secondarySalesRep?: {
id?: number;
identifier?: string;
name?: string;
} | null;
company?: { id?: number | string; name?: string } | null;
contact?: { id?: number | string; name?: string } | null;
site?: { id?: number | string; name?: string } | null;
customerPO?: string | null;
totalSalesTax?: number | null;
expectedCloseDate?: string | null;
pipelineChangeDate?: string | null;
dateBecameLead?: string | null;
closedDate?: string | null;
closedFlag?: boolean;
closedBy?: string | null;
companyId?: string;
cwLastUpdated?: string | null;
createdAt?: string;
updatedAt?: string;
}
export const sales = {
async fetchMany(
accessToken: string,
page: number = 1,
search: string = "",
rpp: number = 30,
includeClosed: boolean = true,
) {
const params: Record<string, unknown> = { page, rpp };
if (search) params.search = search;
if (includeClosed) params.includeClosed = true;
const response = await api.get("/v1/sales/opportunities", {
params,
headers: {
Authorization: `Bearer ${accessToken}`,
},
});
return response.data;
},
};