mirror of
https://github.com/Tria-plc/edr-platform.git
synced 2026-08-26 18:42:49 +00:00
Ticket #238 — pre-approval edits and document uploads write straight to the live company row with no approval gate and, until now, no trace. Adds an append-only company_revisions log (diffed field changes, document uploads) recorded from updateProfile and uploadCompanyDocuments, exposed via GET /companies/:id/revisions and shown as "Version history" on the backoffice customer detail page.
193 lines
6.0 KiB
TypeScript
193 lines
6.0 KiB
TypeScript
import { api as apiClient } from "@/auth/http";
|
|
import { URL_CONSTANTS } from "@/constants/URLS";
|
|
import type {
|
|
Company,
|
|
CompanyChangeRequest,
|
|
CompanyListFilter,
|
|
CompanyProfile,
|
|
CompanyRevision,
|
|
CompanyStats,
|
|
CustomerBooking,
|
|
CustomerDocument,
|
|
CustomerPayment,
|
|
CustomerResetTarget,
|
|
PaginatedCompanies,
|
|
ProfileStatus,
|
|
ResetChannel,
|
|
ResetPasswordResult,
|
|
} from "@/types/customer";
|
|
|
|
const cleanParams = (params: object) =>
|
|
Object.fromEntries(
|
|
Object.entries(params).filter(
|
|
([, value]) => value !== undefined && value !== "" && value !== null,
|
|
),
|
|
);
|
|
|
|
/** Lift attributes JSONB into the flat contact/manager fields the UI reads. */
|
|
function mapCompany(dto: Record<string, unknown>): Company {
|
|
const attrs = (dto.attributes as Record<string, unknown> | null) ?? {};
|
|
return {
|
|
...(dto as unknown as Company),
|
|
companyProfiles: (dto.companyProfiles as Company["companyProfiles"]) ?? [],
|
|
contactPersonName: (attrs.contactPersonName as string | null) ?? null,
|
|
contactPersonPhone: (attrs.contactPersonPhone as string | null) ?? null,
|
|
generalManagerName: (attrs.generalManagerName as string | null) ?? null,
|
|
generalManagerEmail: (attrs.generalManagerEmail as string | null) ?? null,
|
|
generalManagerPhone: (attrs.generalManagerPhone as string | null) ?? null,
|
|
poaName: (attrs.poaName as string | null) ?? null,
|
|
poaEmail: (attrs.poaEmail as string | null) ?? null,
|
|
poaPhone: (attrs.poaPhone as string | null) ?? null,
|
|
poaLocation: (attrs.poaLocation as string | null) ?? null,
|
|
poaAddress: (attrs.poaAddress as string | null) ?? null,
|
|
};
|
|
}
|
|
|
|
export const customersService = {
|
|
stats(): Promise<CompanyStats> {
|
|
return apiClient
|
|
.get<CompanyStats>(URL_CONSTANTS.COMPANIES.STATS)
|
|
.then((r) => r.data);
|
|
},
|
|
|
|
list(filter: CompanyListFilter): Promise<PaginatedCompanies> {
|
|
return apiClient
|
|
.get<{ items: Record<string, unknown>[]; total: number }>(
|
|
URL_CONSTANTS.COMPANIES.BASE,
|
|
{ params: cleanParams(filter) },
|
|
)
|
|
.then((r) => ({
|
|
items: r.data.items.map(mapCompany),
|
|
total: r.data.total,
|
|
}));
|
|
},
|
|
|
|
getById(id: string): Promise<Company | undefined> {
|
|
return apiClient
|
|
.get<Record<string, unknown>>(URL_CONSTANTS.COMPANIES.BY_ID(id))
|
|
.then((r) => mapCompany(r.data));
|
|
},
|
|
|
|
bookingsFor(companyId: string): Promise<CustomerBooking[]> {
|
|
return apiClient
|
|
.get<CustomerBooking[]>(
|
|
URL_CONSTANTS.COMPANIES.BOOKINGS_CUSTOMER_VIEW(companyId),
|
|
)
|
|
.then((r) => r.data);
|
|
},
|
|
|
|
documentsFor(companyId: string): Promise<CustomerDocument[]> {
|
|
return apiClient
|
|
.get<CustomerDocument[]>(URL_CONSTANTS.COMPANIES.DOCUMENTS(companyId))
|
|
.then((r) => r.data);
|
|
},
|
|
|
|
paymentsFor(companyId: string): Promise<CustomerPayment[]> {
|
|
return apiClient
|
|
.get<CustomerPayment[]>(
|
|
URL_CONSTANTS.COMPANIES.PAYMENTS_CUSTOMER_VIEW(companyId),
|
|
)
|
|
.then((r) => r.data);
|
|
},
|
|
|
|
/**
|
|
* The IAM account a reset link would go to. Read before offering the action
|
|
* so staff see the credentials the link actually reaches, not the company's
|
|
* business contact details.
|
|
*/
|
|
resetTarget(companyId: string): Promise<CustomerResetTarget> {
|
|
return apiClient
|
|
.get<CustomerResetTarget>(URL_CONSTANTS.COMPANIES.RESET_TARGET(companyId))
|
|
.then((r) => r.data);
|
|
},
|
|
|
|
/**
|
|
* Send a password-reset link to the company's primary contact. Staff never
|
|
* receive a credential — the customer opens the link and sets their own
|
|
* password.
|
|
*/
|
|
resetPassword(
|
|
companyId: string,
|
|
channel: ResetChannel,
|
|
): Promise<ResetPasswordResult> {
|
|
return apiClient
|
|
.post<ResetPasswordResult>(
|
|
URL_CONSTANTS.COMPANIES.RESET_PASSWORD(companyId),
|
|
{ channel },
|
|
)
|
|
.then((r) => r.data);
|
|
},
|
|
|
|
setProfileStatus(
|
|
profileId: string,
|
|
status: ProfileStatus,
|
|
note?: string,
|
|
): Promise<CompanyProfile> {
|
|
return apiClient
|
|
.patch<CompanyProfile>(
|
|
URL_CONSTANTS.COMPANIES.PROFILE_STATUS(profileId),
|
|
{ status, note },
|
|
)
|
|
.then((r) => r.data);
|
|
},
|
|
|
|
/** Approve / change a company's status (e.g. pending → active). */
|
|
setCompanyStatus(companyId: string, status: string): Promise<unknown> {
|
|
return apiClient
|
|
.patch(URL_CONSTANTS.COMPANIES.BY_ID(companyId), { status })
|
|
.then((r) => r.data);
|
|
},
|
|
|
|
/** List a company's profile-edit change requests (newest first). */
|
|
changeRequests(companyId: string): Promise<CompanyChangeRequest[]> {
|
|
return apiClient
|
|
.get<CompanyChangeRequest[]>(
|
|
URL_CONSTANTS.COMPANIES.CHANGE_REQUESTS(companyId),
|
|
)
|
|
.then((r) => r.data);
|
|
},
|
|
|
|
/** Onboarding-phase edit history for a company (version history), newest first. */
|
|
revisions(companyId: string): Promise<CompanyRevision[]> {
|
|
return apiClient
|
|
.get<CompanyRevision[]>(URL_CONSTANTS.COMPANIES.REVISIONS(companyId))
|
|
.then((r) => r.data);
|
|
},
|
|
|
|
/** Approve a pending change request — applies the proposed changes. */
|
|
approveChangeRequest(id: string): Promise<CompanyChangeRequest> {
|
|
return apiClient
|
|
.post<CompanyChangeRequest>(
|
|
URL_CONSTANTS.COMPANIES.CHANGE_REQUEST_APPROVE(id),
|
|
)
|
|
.then((r) => r.data);
|
|
},
|
|
|
|
/** Reject a pending change request with a note. */
|
|
rejectChangeRequest(id: string, note: string): Promise<CompanyChangeRequest> {
|
|
return apiClient
|
|
.post<CompanyChangeRequest>(
|
|
URL_CONSTANTS.COMPANIES.CHANGE_REQUEST_REJECT(id),
|
|
{ note },
|
|
)
|
|
.then((r) => r.data);
|
|
},
|
|
|
|
/**
|
|
* Ask the customer to correct one uploaded document. Narrower than rejecting
|
|
* the whole role: the customer keeps their other documents and only re-uploads
|
|
* this one, but the role cannot be approved until they do.
|
|
*/
|
|
requestDocumentChange(
|
|
fileId: string,
|
|
note: string,
|
|
): Promise<CustomerDocument> {
|
|
return apiClient
|
|
.post<CustomerDocument>(
|
|
URL_CONSTANTS.COMPANIES.DOCUMENT_REQUEST_CHANGE(fileId),
|
|
{ note },
|
|
)
|
|
.then((r) => r.data);
|
|
},
|
|
};
|