task-b
task-b.1 complere
This commit is contained in:
593
src/features/crm-demo/api/service.ts
Normal file
593
src/features/crm-demo/api/service.ts
Normal file
@@ -0,0 +1,593 @@
|
||||
import type {
|
||||
ApprovalActionPayload,
|
||||
ApprovalFilters,
|
||||
ContactSharePayload,
|
||||
CrmDashboardData,
|
||||
CrmReferenceData,
|
||||
Customer,
|
||||
CustomerDetailResponse,
|
||||
CustomerFilters,
|
||||
CustomerMutationPayload,
|
||||
Enquiry,
|
||||
EnquiryDetailResponse,
|
||||
EnquiryFilters,
|
||||
PaginatedResponse,
|
||||
Quotation,
|
||||
QuotationDetailResponse,
|
||||
QuotationFilters
|
||||
} from './types';
|
||||
import { initialCrmState, type CrmMockState } from '../data/mock-crm';
|
||||
|
||||
function clone<T>(value: T): T {
|
||||
return JSON.parse(JSON.stringify(value)) as T;
|
||||
}
|
||||
|
||||
let crmState: CrmMockState = clone(initialCrmState);
|
||||
|
||||
function sortItems<T>(items: T[], sort?: string) {
|
||||
if (!sort) return items;
|
||||
|
||||
try {
|
||||
const parsed = JSON.parse(sort) as Array<{ id: string; desc?: boolean }>;
|
||||
const [rule] = parsed;
|
||||
|
||||
if (!rule) return items;
|
||||
|
||||
return [...items].sort((a, b) => {
|
||||
const av = String((a as Record<string, unknown>)[rule.id] ?? '');
|
||||
const bv = String((b as Record<string, unknown>)[rule.id] ?? '');
|
||||
return rule.desc ? bv.localeCompare(av) : av.localeCompare(bv);
|
||||
});
|
||||
} catch {
|
||||
return items;
|
||||
}
|
||||
}
|
||||
|
||||
function paginate<T>(items: T[], page = 1, limit = 10): PaginatedResponse<T> {
|
||||
const start = (page - 1) * limit;
|
||||
return {
|
||||
items: items.slice(start, start + limit),
|
||||
totalItems: items.length
|
||||
};
|
||||
}
|
||||
|
||||
function findBranch(branchId: string) {
|
||||
return crmState.branches.find((branch) => branch.id === branchId);
|
||||
}
|
||||
|
||||
function findSalesperson(id: string) {
|
||||
return crmState.salespersons.find((sales) => sales.id === id);
|
||||
}
|
||||
|
||||
export async function getCrmReferenceData(): Promise<CrmReferenceData> {
|
||||
return clone({
|
||||
branches: crmState.branches,
|
||||
salespersons: crmState.salespersons,
|
||||
masterOptions: crmState.masterOptions,
|
||||
sequences: crmState.sequences,
|
||||
templates: crmState.templates
|
||||
});
|
||||
}
|
||||
|
||||
export async function getCrmDashboardData(): Promise<CrmDashboardData> {
|
||||
const openEnquiries = crmState.enquiries.filter((enquiry) =>
|
||||
['new', 'qualifying', 'requirement', 'follow_up'].includes(enquiry.status)
|
||||
);
|
||||
const pendingQuotations = crmState.quotations.filter(
|
||||
(quotation) => quotation.status === 'pending_approval'
|
||||
);
|
||||
const wonQuotations = crmState.quotations.filter((quotation) => quotation.status === 'accepted');
|
||||
const lostQuotations = crmState.quotations.filter((quotation) => quotation.status === 'lost');
|
||||
const hotQuotations = crmState.quotations.filter((quotation) => quotation.isHotProject);
|
||||
const dueFollowUps = crmState.quotations.flatMap((quotation) => quotation.followUps).slice(0, 5);
|
||||
const conversionRate = Math.round(
|
||||
(wonQuotations.length / Math.max(1, crmState.enquiries.length)) * 100
|
||||
);
|
||||
|
||||
return clone({
|
||||
metrics: [
|
||||
{
|
||||
id: 'm-1',
|
||||
label: 'Total Enquiries',
|
||||
value: String(crmState.enquiries.length),
|
||||
change: '+12%',
|
||||
trend: 'up'
|
||||
},
|
||||
{
|
||||
id: 'm-2',
|
||||
label: 'Open Enquiries',
|
||||
value: String(openEnquiries.length),
|
||||
change: '+3',
|
||||
trend: 'up'
|
||||
},
|
||||
{
|
||||
id: 'm-3',
|
||||
label: 'Pending Quotations',
|
||||
value: String(pendingQuotations.length),
|
||||
change: '+2',
|
||||
trend: 'up'
|
||||
},
|
||||
{
|
||||
id: 'm-4',
|
||||
label: 'Pending Approval',
|
||||
value: String(crmState.approvals.filter((item) => item.status === 'pending').length),
|
||||
change: 'Needs review',
|
||||
trend: 'flat'
|
||||
},
|
||||
{
|
||||
id: 'm-5',
|
||||
label: 'Won Amount',
|
||||
value: `${Math.round(wonQuotations.reduce((sum, item) => sum + item.totalAmount, 0) / 1000000)}M`,
|
||||
change: '+18%',
|
||||
trend: 'up'
|
||||
},
|
||||
{
|
||||
id: 'm-6',
|
||||
label: 'Lost Amount',
|
||||
value: `${Math.round(lostQuotations.reduce((sum, item) => sum + item.totalAmount, 0) / 1000000)}M`,
|
||||
change: '-6%',
|
||||
trend: 'down'
|
||||
},
|
||||
{
|
||||
id: 'm-7',
|
||||
label: 'Conversion Rate',
|
||||
value: `${conversionRate}%`,
|
||||
change: '+4pts',
|
||||
trend: 'up'
|
||||
},
|
||||
{
|
||||
id: 'm-8',
|
||||
label: 'Hot Projects',
|
||||
value: String(hotQuotations.length),
|
||||
change: 'Focus',
|
||||
trend: 'flat'
|
||||
},
|
||||
{
|
||||
id: 'm-9',
|
||||
label: 'Follow-up Due Today',
|
||||
value: String(dueFollowUps.length),
|
||||
change: 'Today',
|
||||
trend: 'flat'
|
||||
}
|
||||
],
|
||||
enquiryPipeline: [
|
||||
{ label: 'New', value: crmState.enquiries.filter((item) => item.status === 'new').length },
|
||||
{
|
||||
label: 'Qualifying',
|
||||
value: crmState.enquiries.filter((item) => item.status === 'qualifying').length
|
||||
},
|
||||
{
|
||||
label: 'Requirement',
|
||||
value: crmState.enquiries.filter((item) => item.status === 'requirement').length
|
||||
},
|
||||
{
|
||||
label: 'Follow Up',
|
||||
value: crmState.enquiries.filter((item) => item.status === 'follow_up').length
|
||||
},
|
||||
{
|
||||
label: 'Converted',
|
||||
value: crmState.enquiries.filter((item) => item.status === 'converted').length
|
||||
}
|
||||
],
|
||||
quotationPipeline: [
|
||||
{
|
||||
label: 'Draft',
|
||||
value: crmState.quotations.filter((item) => item.status === 'draft').length
|
||||
},
|
||||
{
|
||||
label: 'Pending',
|
||||
value: crmState.quotations.filter((item) => item.status === 'pending_approval').length
|
||||
},
|
||||
{ label: 'Sent', value: crmState.quotations.filter((item) => item.status === 'sent').length },
|
||||
{
|
||||
label: 'Accepted',
|
||||
value: crmState.quotations.filter((item) => item.status === 'accepted').length
|
||||
},
|
||||
{ label: 'Lost', value: crmState.quotations.filter((item) => item.status === 'lost').length }
|
||||
],
|
||||
salesPerformance: crmState.salespersons.map((salesperson) => ({
|
||||
label: salesperson.nickname,
|
||||
value: crmState.quotations.filter((quotation) => quotation.salesmanId === salesperson.id)
|
||||
.length,
|
||||
secondaryValue: crmState.quotations
|
||||
.filter((quotation) => quotation.salesmanId === salesperson.id)
|
||||
.reduce((sum, quotation) => sum + quotation.totalAmount, 0)
|
||||
})),
|
||||
competitorAnalysis: [
|
||||
{ label: 'Apex Dock', value: 3 },
|
||||
{ label: 'LiftPro', value: 2 },
|
||||
{ label: 'SunNorth', value: 1 },
|
||||
{ label: 'Others', value: 2 }
|
||||
],
|
||||
wonLostTrend: [
|
||||
{ label: 'Jan', value: 1, secondaryValue: 0 },
|
||||
{ label: 'Feb', value: 2, secondaryValue: 1 },
|
||||
{ label: 'Mar', value: 1, secondaryValue: 1 },
|
||||
{ label: 'Apr', value: 3, secondaryValue: 1 },
|
||||
{ label: 'May', value: 2, secondaryValue: 2 },
|
||||
{ label: 'Jun', value: 2, secondaryValue: 1 }
|
||||
],
|
||||
pendingApprovals: crmState.approvals.filter((item) => item.status === 'pending'),
|
||||
hotQuotations,
|
||||
dueFollowUps
|
||||
});
|
||||
}
|
||||
|
||||
export async function getCustomers(filters: CustomerFilters): Promise<PaginatedResponse<Customer>> {
|
||||
let items = [...crmState.customers];
|
||||
|
||||
if (filters.search) {
|
||||
const search = filters.search.toLowerCase();
|
||||
items = items.filter(
|
||||
(customer) =>
|
||||
customer.name.toLowerCase().includes(search) || customer.code.toLowerCase().includes(search)
|
||||
);
|
||||
}
|
||||
|
||||
if (filters.status) {
|
||||
items = items.filter((customer) => customer.customerStatus === filters.status);
|
||||
}
|
||||
|
||||
if (filters.branch) {
|
||||
items = items.filter((customer) => customer.branchId === filters.branch);
|
||||
}
|
||||
|
||||
items = sortItems(items, filters.sort);
|
||||
return clone(paginate(items, filters.page, filters.limit));
|
||||
}
|
||||
|
||||
export async function getCustomerById(id: string): Promise<CustomerDetailResponse> {
|
||||
const customer = crmState.customers.find((item) => item.id === id);
|
||||
if (!customer) {
|
||||
throw new Error('Customer not found');
|
||||
}
|
||||
|
||||
return clone({
|
||||
customer,
|
||||
contacts: crmState.contacts.filter((contact) => contact.customerId === id),
|
||||
shares: crmState.contactShares.filter((share) =>
|
||||
crmState.contacts.some(
|
||||
(contact) => contact.id === share.contactId && contact.customerId === id
|
||||
)
|
||||
),
|
||||
shareLogs: crmState.contactShareLogs.filter((log) =>
|
||||
crmState.contacts.some((contact) => contact.id === log.contactId && contact.customerId === id)
|
||||
),
|
||||
enquiries: crmState.enquiries.filter((enquiry) => enquiry.customerId === id),
|
||||
quotations: crmState.quotations.filter((quotation) =>
|
||||
quotation.customerRoles.some((role) => role.customerId === id)
|
||||
),
|
||||
auditEvents: crmState.auditEvents.filter((event) => event.entityId === id),
|
||||
branch: findBranch(customer.branchId)!
|
||||
});
|
||||
}
|
||||
|
||||
export async function getEnquiries(filters: EnquiryFilters): Promise<PaginatedResponse<Enquiry>> {
|
||||
let items = [...crmState.enquiries];
|
||||
|
||||
if (filters.search) {
|
||||
const search = filters.search.toLowerCase();
|
||||
items = items.filter(
|
||||
(item) =>
|
||||
item.code.toLowerCase().includes(search) || item.title.toLowerCase().includes(search)
|
||||
);
|
||||
}
|
||||
if (filters.status) items = items.filter((item) => item.status === filters.status);
|
||||
if (filters.productType) items = items.filter((item) => item.productType === filters.productType);
|
||||
if (filters.salesman) items = items.filter((item) => item.salesmanId === filters.salesman);
|
||||
if (filters.dateFrom) items = items.filter((item) => item.createdAt >= filters.dateFrom!);
|
||||
if (filters.dateTo)
|
||||
items = items.filter((item) => item.createdAt <= `${filters.dateTo!}T23:59:59.999Z`);
|
||||
|
||||
items = sortItems(items, filters.sort);
|
||||
return clone(paginate(items, filters.page, filters.limit));
|
||||
}
|
||||
|
||||
export async function getEnquiryById(id: string): Promise<EnquiryDetailResponse> {
|
||||
const enquiry = crmState.enquiries.find((item) => item.id === id);
|
||||
if (!enquiry) {
|
||||
throw new Error('Enquiry not found');
|
||||
}
|
||||
|
||||
const customer = crmState.customers.find((item) => item.id === enquiry.customerId)!;
|
||||
const contact = crmState.contacts.find((item) => item.id === enquiry.contactId)!;
|
||||
return clone({
|
||||
enquiry,
|
||||
customer,
|
||||
contact,
|
||||
quotations: crmState.quotations.filter((quotation) => quotation.enquiryId === id),
|
||||
branch: findBranch(enquiry.branchId)!,
|
||||
salesman: findSalesperson(enquiry.salesmanId)!
|
||||
});
|
||||
}
|
||||
|
||||
export async function getQuotations(
|
||||
filters: QuotationFilters
|
||||
): Promise<PaginatedResponse<Quotation>> {
|
||||
let items = [...crmState.quotations];
|
||||
|
||||
if (filters.search) {
|
||||
const search = filters.search.toLowerCase();
|
||||
items = items.filter(
|
||||
(quotation) =>
|
||||
quotation.code.toLowerCase().includes(search) ||
|
||||
quotation.project.toLowerCase().includes(search)
|
||||
);
|
||||
}
|
||||
if (filters.status) items = items.filter((quotation) => quotation.status === filters.status);
|
||||
if (filters.quotationType)
|
||||
items = items.filter((quotation) => quotation.quotationType === filters.quotationType);
|
||||
if (filters.salesman)
|
||||
items = items.filter((quotation) => quotation.salesmanId === filters.salesman);
|
||||
if (filters.hot)
|
||||
items = items.filter((quotation) =>
|
||||
filters.hot === 'yes' ? quotation.isHotProject : !quotation.isHotProject
|
||||
);
|
||||
if (filters.dateFrom)
|
||||
items = items.filter((quotation) => quotation.quotationDate >= filters.dateFrom!);
|
||||
if (filters.dateTo)
|
||||
items = items.filter((quotation) => quotation.quotationDate <= filters.dateTo!);
|
||||
|
||||
items = sortItems(items, filters.sort);
|
||||
return clone(paginate(items, filters.page, filters.limit));
|
||||
}
|
||||
|
||||
export async function getQuotationById(id: string): Promise<QuotationDetailResponse> {
|
||||
const quotation = crmState.quotations.find((item) => item.id === id);
|
||||
if (!quotation) {
|
||||
throw new Error('Quotation not found');
|
||||
}
|
||||
|
||||
const enquiry = crmState.enquiries.find((item) => item.id === quotation.enquiryId)!;
|
||||
const customerIds = [...new Set(quotation.customerRoles.map((role) => role.customerId))];
|
||||
const contactIds = [
|
||||
...new Set(quotation.customerRoles.map((role) => role.contactId).filter(Boolean))
|
||||
] as string[];
|
||||
|
||||
return clone({
|
||||
quotation,
|
||||
enquiry,
|
||||
customers: crmState.customers.filter((item) => customerIds.includes(item.id)),
|
||||
contacts: crmState.contacts.filter((item) => contactIds.includes(item.id)),
|
||||
branch: findBranch(quotation.branchId)!,
|
||||
salesman: findSalesperson(quotation.salesmanId)!,
|
||||
saleAdmin: findSalesperson(quotation.saleAdminId)!,
|
||||
auditEvents: crmState.auditEvents.filter((event) => quotation.auditEventIds.includes(event.id))
|
||||
});
|
||||
}
|
||||
|
||||
export async function getApprovals(filters: ApprovalFilters) {
|
||||
let items = [...crmState.approvals];
|
||||
if (filters.branch) items = items.filter((item) => item.branchId === filters.branch);
|
||||
if (filters.status) items = items.filter((item) => item.status === filters.status);
|
||||
return clone(paginate(items, filters.page, filters.limit));
|
||||
}
|
||||
|
||||
export async function createCustomer(values: CustomerMutationPayload) {
|
||||
const customer: Customer = { id: `cus-${Date.now()}`, contactIds: [], ...values };
|
||||
crmState.customers.unshift(customer);
|
||||
return clone(customer);
|
||||
}
|
||||
|
||||
export async function updateCustomer(id: string, values: CustomerMutationPayload) {
|
||||
crmState.customers = crmState.customers.map((customer) =>
|
||||
customer.id === id ? { ...customer, ...values } : customer
|
||||
);
|
||||
return getCustomerById(id);
|
||||
}
|
||||
|
||||
export async function convertEnquiryToQuotation(enquiryId: string) {
|
||||
const enquiry = crmState.enquiries.find((item) => item.id === enquiryId);
|
||||
if (!enquiry) throw new Error('Enquiry not found');
|
||||
|
||||
const nextId = `qt-${Date.now()}`;
|
||||
const newQuotation: Quotation = {
|
||||
id: nextId,
|
||||
code: `QT2606-${String(crmState.quotations.length + 1).padStart(3, '0')}`,
|
||||
enquiryId,
|
||||
quotationDate: '2026-06-11',
|
||||
validUntil: '2026-07-11',
|
||||
quotationType: 'official',
|
||||
project: enquiry.title,
|
||||
siteLocation: enquiry.projectLocation,
|
||||
customerRoles: [
|
||||
{ role: 'owner', customerId: enquiry.customerId, contactId: enquiry.contactId }
|
||||
],
|
||||
salesmanId: enquiry.salesmanId,
|
||||
saleAdminId: 'sp-2',
|
||||
branchId: enquiry.branchId,
|
||||
status: 'draft',
|
||||
revision: 0,
|
||||
totalAmount: enquiry.expectedValue,
|
||||
chancePercent: enquiry.chancePercent,
|
||||
isHotProject: enquiry.chancePercent >= 70,
|
||||
items: [
|
||||
{
|
||||
id: `${nextId}-item-1`,
|
||||
topic: 'Scope',
|
||||
description: enquiry.requirementSummary,
|
||||
quantity: 1,
|
||||
unit: 'lot',
|
||||
unitPrice: enquiry.expectedValue,
|
||||
amount: enquiry.expectedValue
|
||||
}
|
||||
],
|
||||
topics: [
|
||||
{
|
||||
id: `${nextId}-topic-1`,
|
||||
type: 'scope',
|
||||
label: 'Scope',
|
||||
items: [enquiry.requirementSummary]
|
||||
},
|
||||
{
|
||||
id: `${nextId}-topic-2`,
|
||||
type: 'exclusion',
|
||||
label: 'Exclusion',
|
||||
items: ['To be confirmed']
|
||||
},
|
||||
{ id: `${nextId}-topic-3`, type: 'payment', label: 'Payment', items: ['Pending setup'] }
|
||||
],
|
||||
followUps: [],
|
||||
attachments: [],
|
||||
approvalSteps: [],
|
||||
auditEventIds: []
|
||||
};
|
||||
|
||||
crmState.quotations.unshift(newQuotation);
|
||||
crmState.enquiries = crmState.enquiries.map((item) =>
|
||||
item.id === enquiryId
|
||||
? {
|
||||
...item,
|
||||
status: 'converted',
|
||||
quotationIds: [...item.quotationIds, nextId],
|
||||
activities: [
|
||||
...item.activities,
|
||||
{
|
||||
id: `${enquiryId}-activity-${Date.now()}`,
|
||||
type: 'CONVERT',
|
||||
title: 'Convert to quotation',
|
||||
detail: `สร้าง ${newQuotation.code}`,
|
||||
actorName: 'CRM Demo',
|
||||
createdAt: '2026-06-11T10:00:00.000Z'
|
||||
}
|
||||
]
|
||||
}
|
||||
: item
|
||||
);
|
||||
|
||||
return clone(newQuotation);
|
||||
}
|
||||
|
||||
export async function submitQuotationApproval(quotationId: string) {
|
||||
crmState.quotations = crmState.quotations.map((quotation) =>
|
||||
quotation.id === quotationId
|
||||
? {
|
||||
...quotation,
|
||||
status: 'pending_approval',
|
||||
approvalSteps:
|
||||
quotation.approvalSteps.length > 0
|
||||
? quotation.approvalSteps
|
||||
: [
|
||||
{
|
||||
id: `${quotationId}-ap-1`,
|
||||
level: 1,
|
||||
approverName: 'Sales Director',
|
||||
approverPosition: 'Director',
|
||||
status: 'pending'
|
||||
}
|
||||
]
|
||||
}
|
||||
: quotation
|
||||
);
|
||||
}
|
||||
|
||||
export async function approveQuotation(payload: ApprovalActionPayload) {
|
||||
crmState.quotations = crmState.quotations.map((quotation) => {
|
||||
if (quotation.id !== payload.quotationId) return quotation;
|
||||
|
||||
const nextSteps: typeof quotation.approvalSteps = quotation.approvalSteps.map((step, index) =>
|
||||
index === quotation.approvalSteps.findIndex((item) => item.status === 'pending')
|
||||
? {
|
||||
...step,
|
||||
status: 'approved' as const,
|
||||
actedAt: '2026-06-11T11:00:00.000Z',
|
||||
comment: payload.comment
|
||||
}
|
||||
: step
|
||||
);
|
||||
|
||||
const hasPending = nextSteps.some((step) => step.status === 'pending');
|
||||
return {
|
||||
...quotation,
|
||||
approvalSteps: nextSteps,
|
||||
status: hasPending ? 'pending_approval' : 'approved',
|
||||
approvedPdfUrl: hasPending
|
||||
? quotation.approvedPdfUrl
|
||||
: `/mock/${quotation.code.toLowerCase()}.pdf`
|
||||
};
|
||||
});
|
||||
|
||||
crmState.approvals = crmState.approvals.map((item) =>
|
||||
item.quotationId === payload.quotationId ? { ...item, status: 'approved' } : item
|
||||
);
|
||||
}
|
||||
|
||||
export async function rejectQuotation(payload: ApprovalActionPayload) {
|
||||
crmState.quotations = crmState.quotations.map((quotation) =>
|
||||
quotation.id === payload.quotationId
|
||||
? {
|
||||
...quotation,
|
||||
status: 'rejected' as const,
|
||||
approvalSteps: quotation.approvalSteps.map((step, index) =>
|
||||
index === quotation.approvalSteps.findIndex((item) => item.status === 'pending')
|
||||
? {
|
||||
...step,
|
||||
status: 'rejected' as const,
|
||||
actedAt: '2026-06-11T11:30:00.000Z',
|
||||
comment: payload.comment
|
||||
}
|
||||
: step
|
||||
)
|
||||
}
|
||||
: quotation
|
||||
);
|
||||
crmState.approvals = crmState.approvals.map((item) =>
|
||||
item.quotationId === payload.quotationId ? { ...item, status: 'rejected' } : item
|
||||
);
|
||||
}
|
||||
|
||||
export async function createQuotationRevision(quotationId: string) {
|
||||
const quotation = crmState.quotations.find((item) => item.id === quotationId);
|
||||
if (!quotation) throw new Error('Quotation not found');
|
||||
|
||||
const revisionNumber = quotation.revision + 1;
|
||||
const newQuotation: Quotation = {
|
||||
...clone(quotation),
|
||||
id: `${quotationId}-rev-${revisionNumber}`,
|
||||
code: `${quotation.code}-R${String(revisionNumber).padStart(2, '0')}`,
|
||||
revision: revisionNumber,
|
||||
status: 'revised',
|
||||
totalAmount: quotation.totalAmount + 125000
|
||||
};
|
||||
|
||||
crmState.quotations.unshift(newQuotation);
|
||||
return clone(newQuotation);
|
||||
}
|
||||
|
||||
export async function markQuotationStatus(quotationId: string, status: Quotation['status']) {
|
||||
crmState.quotations = crmState.quotations.map((quotation) =>
|
||||
quotation.id === quotationId ? { ...quotation, status } : quotation
|
||||
);
|
||||
}
|
||||
|
||||
export async function shareContact(payload: ContactSharePayload) {
|
||||
const share = {
|
||||
id: `share-${Date.now()}`,
|
||||
createdAt: '2026-06-11T12:00:00.000Z',
|
||||
...payload
|
||||
};
|
||||
crmState.contactShares.unshift(share);
|
||||
crmState.contactShareLogs.unshift({
|
||||
id: `share-log-${Date.now()}`,
|
||||
contactId: payload.contactId,
|
||||
action: 'SHARE',
|
||||
targetUserName: payload.sharedWithUserName,
|
||||
actorName: 'CRM Demo',
|
||||
createdAt: '2026-06-11T12:00:00.000Z'
|
||||
});
|
||||
}
|
||||
|
||||
export async function revokeContactShare(contactId: string, shareId: string) {
|
||||
const current = crmState.contactShares.find((share) => share.id === shareId);
|
||||
crmState.contactShares = crmState.contactShares.filter((share) => share.id !== shareId);
|
||||
if (current) {
|
||||
crmState.contactShareLogs.unshift({
|
||||
id: `share-log-${Date.now()}`,
|
||||
contactId,
|
||||
action: 'REVOKE',
|
||||
targetUserName: current.sharedWithUserName,
|
||||
actorName: 'CRM Demo',
|
||||
createdAt: '2026-06-11T12:30:00.000Z'
|
||||
});
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user