task-e
This commit is contained in:
@@ -11,6 +11,7 @@ import { Separator } from '@/components/ui/separator';
|
||||
import { Tabs, TabsContent, TabsList, TabsTrigger } from '@/components/ui/tabs';
|
||||
import { enquiryByIdOptions } from '../api/queries';
|
||||
import type { EnquiryReferenceData } from '../api/types';
|
||||
import type { QuotationRelationItem } from '@/features/crm/quotations/api/types';
|
||||
import { EnquiryFormSheet } from './enquiry-form-sheet';
|
||||
import { EnquiryFollowupsTab } from './enquiry-followups-tab';
|
||||
import { EnquiryStatusBadge } from './enquiry-status-badge';
|
||||
@@ -27,11 +28,13 @@ function FieldItem({ label, value }: { label: string; value: string | null | und
|
||||
export function EnquiryDetail({
|
||||
enquiryId,
|
||||
referenceData,
|
||||
relatedQuotations,
|
||||
canUpdate,
|
||||
canManageFollowups
|
||||
}: {
|
||||
enquiryId: string;
|
||||
referenceData: EnquiryReferenceData;
|
||||
relatedQuotations: QuotationRelationItem[];
|
||||
canUpdate: boolean;
|
||||
canManageFollowups: {
|
||||
create: boolean;
|
||||
@@ -245,14 +248,35 @@ export function EnquiryDetail({
|
||||
<CardHeader>
|
||||
<CardTitle>Related Quotations</CardTitle>
|
||||
<CardDescription>
|
||||
Task D ends at enquiry readiness, so quotation production stays
|
||||
intentionally deferred.
|
||||
Production quotations already linked to this enquiry.
|
||||
</CardDescription>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<div className='text-muted-foreground rounded-lg border border-dashed p-8 text-center text-sm'>
|
||||
Quotation linkage will land here in Task E.
|
||||
</div>
|
||||
{!relatedQuotations.length ? (
|
||||
<div className='text-muted-foreground rounded-lg border border-dashed p-8 text-center text-sm'>
|
||||
No quotations have been linked to this enquiry yet.
|
||||
</div>
|
||||
) : (
|
||||
<div className='space-y-3'>
|
||||
{relatedQuotations.map((quotation) => (
|
||||
<Link
|
||||
key={quotation.id}
|
||||
href={`/dashboard/crm/quotations/${quotation.id}`}
|
||||
className='block rounded-lg border p-4 transition-colors hover:bg-muted/40'
|
||||
>
|
||||
<div className='flex items-center justify-between gap-3'>
|
||||
<div className='space-y-1'>
|
||||
<div className='font-medium'>{quotation.code}</div>
|
||||
<div className='text-muted-foreground text-sm'>
|
||||
{quotation.totalAmount.toLocaleString()}
|
||||
</div>
|
||||
</div>
|
||||
<Icons.arrowRight className='text-muted-foreground h-4 w-4' />
|
||||
</div>
|
||||
</Link>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
</CardContent>
|
||||
</Card>
|
||||
</TabsContent>
|
||||
|
||||
@@ -178,13 +178,17 @@ async function assertMasterOptionValue(
|
||||
}
|
||||
|
||||
export async function assertCustomerBelongsToOrganization(id: string, organizationId: string) {
|
||||
const customer = await db.query.crmCustomers.findFirst({
|
||||
where: and(
|
||||
eq(crmCustomers.id, id),
|
||||
eq(crmCustomers.organizationId, organizationId),
|
||||
isNull(crmCustomers.deletedAt)
|
||||
const [customer] = await db
|
||||
.select()
|
||||
.from(crmCustomers)
|
||||
.where(
|
||||
and(
|
||||
eq(crmCustomers.id, id),
|
||||
eq(crmCustomers.organizationId, organizationId),
|
||||
isNull(crmCustomers.deletedAt)
|
||||
)
|
||||
)
|
||||
});
|
||||
.limit(1);
|
||||
|
||||
if (!customer) {
|
||||
throw new AuthError('Customer not found', 404);
|
||||
@@ -198,14 +202,18 @@ export async function assertContactBelongsToOrganization(
|
||||
organizationId: string,
|
||||
customerId?: string | null
|
||||
) {
|
||||
const contact = await db.query.crmCustomerContacts.findFirst({
|
||||
where: and(
|
||||
eq(crmCustomerContacts.id, contactId),
|
||||
eq(crmCustomerContacts.organizationId, organizationId),
|
||||
isNull(crmCustomerContacts.deletedAt),
|
||||
...(customerId ? [eq(crmCustomerContacts.customerId, customerId)] : [])
|
||||
const [contact] = await db
|
||||
.select()
|
||||
.from(crmCustomerContacts)
|
||||
.where(
|
||||
and(
|
||||
eq(crmCustomerContacts.id, contactId),
|
||||
eq(crmCustomerContacts.organizationId, organizationId),
|
||||
isNull(crmCustomerContacts.deletedAt),
|
||||
...(customerId ? [eq(crmCustomerContacts.customerId, customerId)] : [])
|
||||
)
|
||||
)
|
||||
});
|
||||
.limit(1);
|
||||
|
||||
if (!contact) {
|
||||
throw new AuthError('Contact not found', 404);
|
||||
@@ -215,13 +223,17 @@ export async function assertContactBelongsToOrganization(
|
||||
}
|
||||
|
||||
async function assertEnquiryBelongsToOrganization(id: string, organizationId: string) {
|
||||
const enquiry = await db.query.crmEnquiries.findFirst({
|
||||
where: and(
|
||||
eq(crmEnquiries.id, id),
|
||||
eq(crmEnquiries.organizationId, organizationId),
|
||||
isNull(crmEnquiries.deletedAt)
|
||||
const [enquiry] = await db
|
||||
.select()
|
||||
.from(crmEnquiries)
|
||||
.where(
|
||||
and(
|
||||
eq(crmEnquiries.id, id),
|
||||
eq(crmEnquiries.organizationId, organizationId),
|
||||
isNull(crmEnquiries.deletedAt)
|
||||
)
|
||||
)
|
||||
});
|
||||
.limit(1);
|
||||
|
||||
if (!enquiry) {
|
||||
throw new AuthError('Enquiry not found', 404);
|
||||
@@ -235,14 +247,18 @@ async function assertFollowupBelongsToEnquiry(
|
||||
enquiryId: string,
|
||||
organizationId: string
|
||||
) {
|
||||
const followup = await db.query.crmEnquiryFollowups.findFirst({
|
||||
where: and(
|
||||
eq(crmEnquiryFollowups.id, followupId),
|
||||
eq(crmEnquiryFollowups.enquiryId, enquiryId),
|
||||
eq(crmEnquiryFollowups.organizationId, organizationId),
|
||||
isNull(crmEnquiryFollowups.deletedAt)
|
||||
const [followup] = await db
|
||||
.select()
|
||||
.from(crmEnquiryFollowups)
|
||||
.where(
|
||||
and(
|
||||
eq(crmEnquiryFollowups.id, followupId),
|
||||
eq(crmEnquiryFollowups.enquiryId, enquiryId),
|
||||
eq(crmEnquiryFollowups.organizationId, organizationId),
|
||||
isNull(crmEnquiryFollowups.deletedAt)
|
||||
)
|
||||
)
|
||||
});
|
||||
.limit(1);
|
||||
|
||||
if (!followup) {
|
||||
throw new AuthError('Follow-up not found', 404);
|
||||
@@ -345,17 +361,21 @@ export async function getEnquiryReferenceData(
|
||||
getActiveOptionsByCategory(ENQUIRY_OPTION_CATEGORIES.priority, { organizationId }),
|
||||
getActiveOptionsByCategory(ENQUIRY_OPTION_CATEGORIES.leadChannel, { organizationId }),
|
||||
getActiveOptionsByCategory(ENQUIRY_OPTION_CATEGORIES.followupType, { organizationId }),
|
||||
db.query.crmCustomers.findMany({
|
||||
where: and(eq(crmCustomers.organizationId, organizationId), isNull(crmCustomers.deletedAt)),
|
||||
orderBy: [asc(crmCustomers.name)]
|
||||
}),
|
||||
db.query.crmCustomerContacts.findMany({
|
||||
where: and(
|
||||
eq(crmCustomerContacts.organizationId, organizationId),
|
||||
isNull(crmCustomerContacts.deletedAt)
|
||||
),
|
||||
orderBy: [desc(crmCustomerContacts.isPrimary), asc(crmCustomerContacts.name)]
|
||||
})
|
||||
db
|
||||
.select()
|
||||
.from(crmCustomers)
|
||||
.where(and(eq(crmCustomers.organizationId, organizationId), isNull(crmCustomers.deletedAt)))
|
||||
.orderBy(asc(crmCustomers.name)),
|
||||
db
|
||||
.select()
|
||||
.from(crmCustomerContacts)
|
||||
.where(
|
||||
and(
|
||||
eq(crmCustomerContacts.organizationId, organizationId),
|
||||
isNull(crmCustomerContacts.deletedAt)
|
||||
)
|
||||
)
|
||||
.orderBy(desc(crmCustomerContacts.isPrimary), asc(crmCustomerContacts.name))
|
||||
]);
|
||||
|
||||
return {
|
||||
@@ -407,20 +427,26 @@ export async function listEnquiries(
|
||||
|
||||
const [customers, contacts, followupCounts] = await Promise.all([
|
||||
customerIds.length
|
||||
? db.query.crmCustomers.findMany({
|
||||
where: and(
|
||||
eq(crmCustomers.organizationId, organizationId),
|
||||
inArray(crmCustomers.id, customerIds)
|
||||
? db
|
||||
.select()
|
||||
.from(crmCustomers)
|
||||
.where(
|
||||
and(
|
||||
eq(crmCustomers.organizationId, organizationId),
|
||||
inArray(crmCustomers.id, customerIds)
|
||||
)
|
||||
)
|
||||
})
|
||||
: [],
|
||||
contactIds.length
|
||||
? db.query.crmCustomerContacts.findMany({
|
||||
where: and(
|
||||
eq(crmCustomerContacts.organizationId, organizationId),
|
||||
inArray(crmCustomerContacts.id, contactIds)
|
||||
? db
|
||||
.select()
|
||||
.from(crmCustomerContacts)
|
||||
.where(
|
||||
and(
|
||||
eq(crmCustomerContacts.organizationId, organizationId),
|
||||
inArray(crmCustomerContacts.id, contactIds)
|
||||
)
|
||||
)
|
||||
})
|
||||
: [],
|
||||
enquiryIds.length
|
||||
? db
|
||||
@@ -618,14 +644,17 @@ export async function softDeleteEnquiry(id: string, organizationId: string, user
|
||||
|
||||
export async function listEnquiryFollowups(id: string, organizationId: string) {
|
||||
await assertEnquiryBelongsToOrganization(id, organizationId);
|
||||
const rows = await db.query.crmEnquiryFollowups.findMany({
|
||||
where: and(
|
||||
eq(crmEnquiryFollowups.enquiryId, id),
|
||||
eq(crmEnquiryFollowups.organizationId, organizationId),
|
||||
isNull(crmEnquiryFollowups.deletedAt)
|
||||
),
|
||||
orderBy: [desc(crmEnquiryFollowups.followupDate)]
|
||||
});
|
||||
const rows = await db
|
||||
.select()
|
||||
.from(crmEnquiryFollowups)
|
||||
.where(
|
||||
and(
|
||||
eq(crmEnquiryFollowups.enquiryId, id),
|
||||
eq(crmEnquiryFollowups.organizationId, organizationId),
|
||||
isNull(crmEnquiryFollowups.deletedAt)
|
||||
)
|
||||
)
|
||||
.orderBy(desc(crmEnquiryFollowups.followupDate));
|
||||
|
||||
return rows.map(mapFollowupRecord);
|
||||
}
|
||||
@@ -713,15 +742,18 @@ export async function listCustomerEnquiryRelations(
|
||||
customerId: string,
|
||||
organizationId: string
|
||||
): Promise<EnquiryCustomerRelationItem[]> {
|
||||
const rows = await db.query.crmEnquiries.findMany({
|
||||
where: and(
|
||||
eq(crmEnquiries.customerId, customerId),
|
||||
eq(crmEnquiries.organizationId, organizationId),
|
||||
isNull(crmEnquiries.deletedAt)
|
||||
),
|
||||
orderBy: [desc(crmEnquiries.updatedAt)],
|
||||
limit: 10
|
||||
});
|
||||
const rows = await db
|
||||
.select()
|
||||
.from(crmEnquiries)
|
||||
.where(
|
||||
and(
|
||||
eq(crmEnquiries.customerId, customerId),
|
||||
eq(crmEnquiries.organizationId, organizationId),
|
||||
isNull(crmEnquiries.deletedAt)
|
||||
)
|
||||
)
|
||||
.orderBy(desc(crmEnquiries.updatedAt))
|
||||
.limit(10);
|
||||
|
||||
return rows.map((row) => ({
|
||||
id: row.id,
|
||||
|
||||
Reference in New Issue
Block a user