commit show calenda

This commit is contained in:
phaichayon
2026-07-14 16:15:30 +07:00
parent a16dc8e548
commit a9dcc11ea9
9 changed files with 1346 additions and 82 deletions

File diff suppressed because it is too large Load Diff

View File

@@ -1,13 +1,13 @@
organization_code,workflow_code,step_number,role_code,role_name,approval_mode,is_required,sla_hours,calendar_mode,timeout_action
ALLA,quotation_standard,1,sales_manager,Sales Manager,sequential,true,24,calendar_days,auto_escalate
ALLA,quotation_manager_review,1,sales_manager,Sales Manager,sequential,true,24,calendar_days,auto_escalate
ALLA,quotation_manager_review,2,department_manager,Department Manager,sequential,true,48,calendar_days,auto_escalate
ALLA,quotation_executive_review,1,sales_manager,Sales Manager,sequential,true,24,calendar_days,auto_escalate
ALLA,quotation_executive_review,2,department_manager,Department Manager,sequential,true,48,calendar_days,auto_escalate
ALLA,quotation_standard,1,sales_manager,Sales Manager,sequential,true,24,calendar_days,escalate
ALLA,quotation_manager_review,1,sales_manager,Sales Manager,sequential,true,24,calendar_days,escalate
ALLA,quotation_manager_review,2,department_manager,Department Manager,sequential,true,48,calendar_days,escalate
ALLA,quotation_executive_review,1,sales_manager,Sales Manager,sequential,true,24,calendar_days,escalate
ALLA,quotation_executive_review,2,department_manager,Department Manager,sequential,true,48,calendar_days,escalate
ALLA,quotation_executive_review,3,top_manager,Top Manager,sequential,true,72,calendar_days,none
ONVALLA,quotation_standard,1,sales_manager,Sales Manager,sequential,true,24,calendar_days,auto_escalate
ONVALLA,quotation_manager_review,1,sales_manager,Sales Manager,sequential,true,24,calendar_days,auto_escalate
ONVALLA,quotation_manager_review,2,department_manager,Department Manager,sequential,true,48,calendar_days,auto_escalate
ONVALLA,quotation_executive_review,1,sales_manager,Sales Manager,sequential,true,24,calendar_days,auto_escalate
ONVALLA,quotation_executive_review,2,department_manager,Department Manager,sequential,true,48,calendar_days,auto_escalate
ONVALLA,quotation_standard,1,sales_manager,Sales Manager,sequential,true,24,calendar_days,escalate
ONVALLA,quotation_manager_review,1,sales_manager,Sales Manager,sequential,true,24,calendar_days,escalate
ONVALLA,quotation_manager_review,2,department_manager,Department Manager,sequential,true,48,calendar_days,escalate
ONVALLA,quotation_executive_review,1,sales_manager,Sales Manager,sequential,true,24,calendar_days,escalate
ONVALLA,quotation_executive_review,2,department_manager,Department Manager,sequential,true,48,calendar_days,escalate
ONVALLA,quotation_executive_review,3,top_manager,Top Manager,sequential,true,72,calendar_days,none
1 organization_code workflow_code step_number role_code role_name approval_mode is_required sla_hours calendar_mode timeout_action
2 ALLA quotation_standard 1 sales_manager Sales Manager sequential true 24 calendar_days auto_escalate escalate
3 ALLA quotation_manager_review 1 sales_manager Sales Manager sequential true 24 calendar_days auto_escalate escalate
4 ALLA quotation_manager_review 2 department_manager Department Manager sequential true 48 calendar_days auto_escalate escalate
5 ALLA quotation_executive_review 1 sales_manager Sales Manager sequential true 24 calendar_days auto_escalate escalate
6 ALLA quotation_executive_review 2 department_manager Department Manager sequential true 48 calendar_days auto_escalate escalate
7 ALLA quotation_executive_review 3 top_manager Top Manager sequential true 72 calendar_days none
8 ONVALLA quotation_standard 1 sales_manager Sales Manager sequential true 24 calendar_days auto_escalate escalate
9 ONVALLA quotation_manager_review 1 sales_manager Sales Manager sequential true 24 calendar_days auto_escalate escalate
10 ONVALLA quotation_manager_review 2 department_manager Department Manager sequential true 48 calendar_days auto_escalate escalate
11 ONVALLA quotation_executive_review 1 sales_manager Sales Manager sequential true 24 calendar_days auto_escalate escalate
12 ONVALLA quotation_executive_review 2 department_manager Department Manager sequential true 48 calendar_days auto_escalate escalate
13 ONVALLA quotation_executive_review 3 top_manager Top Manager sequential true 72 calendar_days none

View File

@@ -1,6 +1,13 @@
organization_code,branch_code,product_type_code,document_type,prefix,period,current_number,padding_length,format,reset_policy,is_active
ALLA,head_office,crane,customer,CUS,yearly,0,4,{prefix}-{yyyy}-{running},period,true
ALLA,head_office,crane,lead,LD,yearly,0,4,{prefix}-{yyyy}-{running},period,true
ALLA,head_office,crane,opportunity,OP,yearly,0,4,{prefix}-{yyyy}-{running},period,true
ALLA,head_office,crane,quotation,QT-AL-CR,monthly,0,4,{prefix}-{yy}{MM}-{running},period,true
ONVALLA,head_office,crane,quotation,QT-ON-CR,monthly,0,4,{prefix}-{yy}{MM}-{running},period,true
ALLA,head_office,,customer,CUS,2607,0,3,{prefix}{period}-{running},period,true
ALLA,head_office,,crm_lead,LD,2607,0,3,{prefix}{period}-{running},period,true
ALLA,head_office,,opportunity,OP,2607,0,3,{prefix}{period}-{running},period,true
ALLA,head_office,,crm_opportunity,EN,2607,0,3,{prefix}{period}-{running},period,true
ALLA,head_office,crane,quotation,QT-AL-CR,2607,0,4,{prefix}{period}-{running},period,true
ALLA,head_office,dockdoor,quotation,QT-AL-DK,2607,0,4,{prefix}{period}-{running},period,true
ONVALLA,head_office,,customer,CUS,2607,0,3,{prefix}{period}-{running},period,true
ONVALLA,head_office,,crm_lead,LD,2607,0,3,{prefix}{period}-{running},period,true
ONVALLA,head_office,,opportunity,OP,2607,0,3,{prefix}{period}-{running},period,true
ONVALLA,head_office,,crm_opportunity,EN,2607,0,3,{prefix}{period}-{running},period,true
ONVALLA,head_office,crane,quotation,QT-ON-CR,2607,0,4,{prefix}{period}-{running},period,true
ONVALLA,head_office,solarcell,quotation,QT-ON-SC,2607,0,4,{prefix}{period}-{running},period,true
1 organization_code branch_code product_type_code document_type prefix period current_number padding_length format reset_policy is_active
2 ALLA head_office crane customer CUS yearly 2607 0 4 3 {prefix}-{yyyy}-{running} {prefix}{period}-{running} period true
3 ALLA head_office crane lead crm_lead LD yearly 2607 0 4 3 {prefix}-{yyyy}-{running} {prefix}{period}-{running} period true
4 ALLA head_office crane opportunity OP yearly 2607 0 4 3 {prefix}-{yyyy}-{running} {prefix}{period}-{running} period true
5 ALLA head_office crane quotation crm_opportunity QT-AL-CR EN monthly 2607 0 4 3 {prefix}-{yy}{MM}-{running} {prefix}{period}-{running} period true
6 ONVALLA ALLA head_office crane quotation QT-ON-CR QT-AL-CR monthly 2607 0 4 {prefix}-{yy}{MM}-{running} {prefix}{period}-{running} period true
7 ALLA head_office dockdoor quotation QT-AL-DK 2607 0 4 {prefix}{period}-{running} period true
8 ONVALLA head_office customer CUS 2607 0 3 {prefix}{period}-{running} period true
9 ONVALLA head_office crm_lead LD 2607 0 3 {prefix}{period}-{running} period true
10 ONVALLA head_office opportunity OP 2607 0 3 {prefix}{period}-{running} period true
11 ONVALLA head_office crm_opportunity EN 2607 0 3 {prefix}{period}-{running} period true
12 ONVALLA head_office crane quotation QT-ON-CR 2607 0 4 {prefix}{period}-{running} period true
13 ONVALLA head_office solarcell quotation QT-ON-SC 2607 0 4 {prefix}{period}-{running} period true

View File

@@ -1,4 +1,16 @@
organization_code,category,code,label,value,parent_category,parent_code,sort_order,is_active,metadata_json
ALLA,crm_currency,thb,Thai Baht,THB,,,1,true,{}
ONVALLA,crm_currency,thb,Thai Baht,THB,,,1,true,{}
ALLA,crm_lead_status,new_job,New Job,new_job,,,1,true,{}
ALLA,crm_lead_status,follow_up,Follow Up,follow_up,,,2,true,{}
ALLA,crm_lead_status,assigned,Assigned,assigned,,,3,true,{}
ALLA,crm_lead_status,closed_lost,Closed Lost,closed_lost,,,4,true,{}
ALLA,crm_lead_status,cancel,Cancel,cancel,,,5,true,{}
ONVALLA,crm_lead_status,new_job,New Job,new_job,,,1,true,{}
ONVALLA,crm_lead_status,follow_up,Follow Up,follow_up,,,2,true,{}
ONVALLA,crm_lead_status,assigned,Assigned,assigned,,,3,true,{}
ONVALLA,crm_lead_status,closed_lost,Closed Lost,closed_lost,,,4,true,{}
ONVALLA,crm_lead_status,cancel,Cancel,cancel,,,5,true,{}
ALLA,crm_customer_group,construction,Construction,,,,1,true,{}
ALLA,crm_customer_group,industrial,Industrial,,,,2,true,{}
1 organization_code category code label value parent_category parent_code sort_order is_active metadata_json
2 ALLA crm_currency thb Thai Baht THB 1 true {}
3 ONVALLA crm_currency thb Thai Baht THB 1 true {}
4 ALLA crm_lead_status new_job New Job new_job 1 true {}
5 ALLA crm_lead_status follow_up Follow Up follow_up 2 true {}
6 ALLA crm_lead_status assigned Assigned assigned 3 true {}
7 ALLA crm_lead_status closed_lost Closed Lost closed_lost 4 true {}
8 ALLA crm_lead_status cancel Cancel cancel 5 true {}
9 ONVALLA crm_lead_status new_job New Job new_job 1 true {}
10 ONVALLA crm_lead_status follow_up Follow Up follow_up 2 true {}
11 ONVALLA crm_lead_status assigned Assigned assigned 3 true {}
12 ONVALLA crm_lead_status closed_lost Closed Lost closed_lost 4 true {}
13 ONVALLA crm_lead_status cancel Cancel cancel 5 true {}
14 ALLA crm_customer_group construction Construction 1 true {}
15 ALLA crm_customer_group industrial Industrial 2 true {}
16 ALLA crm_customer_sub_group factory Factory crm_customer_group industrial 1 true {}

View File

@@ -1,25 +1,29 @@
import KBar from '@/components/kbar';
import AppSidebar from '@/components/layout/app-sidebar';
import Header from '@/components/layout/header';
import { InfoSidebar } from '@/components/layout/info-sidebar';
import { InfobarProvider } from '@/components/ui/infobar';
import { SidebarInset, SidebarProvider } from '@/components/ui/sidebar';
import type { Metadata } from 'next';
import { cookies } from 'next/headers';
import KBar from "@/components/kbar";
import AppSidebar from "@/components/layout/app-sidebar";
import Header from "@/components/layout/header";
import { InfoSidebar } from "@/components/layout/info-sidebar";
import { InfobarProvider } from "@/components/ui/infobar";
import { SidebarInset, SidebarProvider } from "@/components/ui/sidebar";
import type { Metadata } from "next";
import { cookies } from "next/headers";
export const metadata: Metadata = {
title: 'Next Shadcn Dashboard Starter',
description: 'Basic dashboard with Next.js and Shadcn',
title: "ALLA OS",
description: "ALLA OS",
robots: {
index: false,
follow: false
}
follow: false,
},
};
export default async function DashboardLayout({ children }: { children: React.ReactNode }) {
export default async function DashboardLayout({
children,
}: {
children: React.ReactNode;
}) {
// Persisting the sidebar state in the cookie.
const cookieStore = await cookies();
const defaultOpen = cookieStore.get('sidebar_state')?.value === 'true';
const defaultOpen = cookieStore.get("sidebar_state")?.value === "true";
return (
<KBar>
<SidebarProvider defaultOpen={defaultOpen}>
@@ -28,7 +32,7 @@ export default async function DashboardLayout({ children }: { children: React.Re
<Header />
<InfobarProvider defaultOpen={false}>
{children}
<InfoSidebar side='right' />
<InfoSidebar side="right" />
</InfobarProvider>
</SidebarInset>
</SidebarProvider>

View File

@@ -4,6 +4,7 @@ import { db } from '@/lib/db';
import { AuthError } from '@/lib/auth/session';
import { generateNextDocumentCode } from '@/features/foundation/document-sequence/service';
import { getActiveOptionsByCategory } from '@/features/foundation/master-options/service';
import { drainProjectionOutboxBestEffort } from '@/features/foundation/projections/server/best-effort-drain';
import {
CRM_ACTIVITY_ENTITY_TYPES,
CRM_ACTIVITY_PERMISSIONS,
@@ -677,6 +678,7 @@ export async function createActivity(
return createdRow;
});
await drainProjectionOutboxBestEffort();
const activity = await getActivityById(created.id, organizationId, context);
return activity;
@@ -810,6 +812,7 @@ correlationId
return nextActivity;
});
await drainProjectionOutboxBestEffort();
const activity = await getActivityById(activityEventRecord.id, organizationId, context);
return activity;
@@ -846,6 +849,7 @@ actorUserId: userId
return nextActivity;
});
await drainProjectionOutboxBestEffort();
const activity = await getActivityById(completedEventRecord.id, organizationId, context);
return activity;
}
@@ -880,6 +884,7 @@ actorUserId: userId
return nextActivity;
});
await drainProjectionOutboxBestEffort();
const activity = await getActivityById(cancelledEventRecord.id, organizationId, context);
return activity;
}
@@ -917,7 +922,8 @@ actorUserId: userId
return nextActivity;
});
const activity = await getActivityById(assignedEventRecord.id, organizationId, context);
await drainProjectionOutboxBestEffort();
const activity = await getActivityById(assignedEventRecord.id, organizationId, context);
return activity;
}
@@ -964,6 +970,7 @@ actorUserId: userId
return nextActivity;
});
await drainProjectionOutboxBestEffort();
const activity = await getActivityById(rescheduledEventRecord.id, organizationId, context);
return activity;
}
@@ -991,4 +998,6 @@ activity: current,
actorUserId: userId
});
});
await drainProjectionOutboxBestEffort();
}

View File

@@ -1,4 +1,5 @@
import type { BusinessEvent } from '../business-events/types.ts';
import { calendarProjectionConsumer as crmCalendarProjectionConsumer } from '../../crm/calendar/server/consumer.ts';
import { getProjectionDefinition } from './registry.ts';
import type { ProjectionConsumer, ProjectionHandleResult } from './types.ts';
@@ -21,7 +22,7 @@ function createNoopConsumer(projectionName: string): ProjectionConsumer {
}
export const timelineProjectionConsumer = createNoopConsumer('Timeline');
export const calendarProjectionConsumer = createNoopConsumer('Calendar');
export const calendarProjectionConsumer = crmCalendarProjectionConsumer;
export const dashboardProjectionConsumer = createNoopConsumer('Dashboard');
export const notificationProjectionConsumer = createNoopConsumer('Notification');

View File

@@ -0,0 +1,23 @@
import { INITIAL_PROJECTION_CONSUMERS } from '../consumers.ts';
import { ProjectionOutboxWorker } from '../worker.ts';
import { DrizzleProjectionRuntimeStore } from './store.ts';
export async function drainProjectionOutboxBestEffort(): Promise<void> {
try {
const worker = new ProjectionOutboxWorker({
store: new DrizzleProjectionRuntimeStore(),
consumers: INITIAL_PROJECTION_CONSUMERS,
config: {
enabled: true,
batchSize: 25,
maxConcurrentEvents: 1,
maxConcurrentConsumers: 4,
workerId: `inline-projection-${crypto.randomUUID().slice(0, 8)}`
}
});
await worker.drainOnce();
} catch (error) {
console.error('[projections] best-effort inline drain failed', error);
}
}

View File

@@ -1,4 +1,4 @@
import { and, eq, sql } from 'drizzle-orm';
import { and, asc, eq, inArray, lte, or } from 'drizzle-orm';
import {
businessEventOutbox,
projectionConsumerCheckpoints,
@@ -51,37 +51,47 @@ export class DrizzleProjectionRuntimeStore implements ProjectionRuntimeStore {
async claimAvailableEvents(
input: ClaimBusinessEventDeliveriesInput
): Promise<BusinessEventDeliveryRecord[]> {
const result = await db.execute(sql`
WITH claimable AS (
SELECT id
FROM business_event_outbox
WHERE
(
delivery_status IN ('pending', 'retry_scheduled')
AND available_at <= ${input.now}
const claimable = await db
.select({ id: businessEventOutbox.id })
.from(businessEventOutbox)
.where(
or(
and(
inArray(businessEventOutbox.deliveryStatus, ['pending', 'retry_scheduled']),
lte(businessEventOutbox.availableAt, input.now)
),
and(
inArray(businessEventOutbox.deliveryStatus, ['claimed', 'processing']),
lte(businessEventOutbox.claimExpiresAt, input.now)
)
OR (
delivery_status IN ('claimed', 'processing')
AND claim_expires_at IS NOT NULL
AND claim_expires_at <= ${input.now}
)
ORDER BY available_at ASC, created_at ASC, id ASC
LIMIT ${input.batchSize}
FOR UPDATE SKIP LOCKED
)
UPDATE business_event_outbox
SET
delivery_status = 'claimed',
claimed_by = ${input.workerId},
claimed_at = ${input.now},
claim_expires_at = ${input.leaseExpiresAt},
updated_at = ${input.now}
FROM claimable
WHERE business_event_outbox.id = claimable.id
RETURNING business_event_outbox.*
`);
.orderBy(
asc(businessEventOutbox.availableAt),
asc(businessEventOutbox.createdAt),
asc(businessEventOutbox.id)
)
.limit(input.batchSize);
if (claimable.length === 0) return [];
const rows = await db
.update(businessEventOutbox)
.set({
deliveryStatus: 'claimed',
claimedBy: input.workerId,
claimedAt: input.now,
claimExpiresAt: input.leaseExpiresAt,
updatedAt: input.now
})
.where(
inArray(
businessEventOutbox.id,
claimable.map((item) => item.id)
)
)
.returning();
const rows = result as unknown as Array<typeof businessEventOutbox.$inferSelect>;
return rows.map((row) => mapDelivery(row));
}
@@ -256,27 +266,49 @@ export class DrizzleProjectionRuntimeStore implements ProjectionRuntimeStore {
});
}
async recordHealthSnapshot(input: ProjectionHealthSnapshotInput): Promise<void> {
await db.insert(projectionHealthSnapshots).values({
id: crypto.randomUUID(),
organizationId: input.organizationId,
projectionName: input.projectionName,
consumerName: input.consumerName,
status: input.status,
lastProcessedEventId: input.lastProcessedEventId ?? null,
lastSuccessfulProcessingAt: input.lastSuccessfulProcessingAt
? new Date(input.lastSuccessfulProcessingAt)
: null,
pendingCount: input.pendingCount ?? 0,
retryCount: input.retryCount ?? 0,
deadLetterCount: input.deadLetterCount ?? 0,
unsupportedVersionCount: input.unsupportedVersionCount ?? 0,
processingLagMs: input.processingLagMs ?? 0,
averageProcessingDurationMs: input.averageProcessingDurationMs ?? 0,
lastRebuildAt: input.lastRebuildAt ? new Date(input.lastRebuildAt) : null,
rebuildSource: input.rebuildSource ?? null
});
}
async recordHealthSnapshot(input: ProjectionHealthSnapshotInput): Promise<void> {
const values = {
id: crypto.randomUUID(),
organizationId: input.organizationId,
projectionName: input.projectionName,
consumerName: input.consumerName,
status: input.status,
lastProcessedEventId: input.lastProcessedEventId ?? null,
lastSuccessfulProcessingAt: input.lastSuccessfulProcessingAt
? new Date(input.lastSuccessfulProcessingAt)
: null,
pendingCount: input.pendingCount ?? 0,
retryCount: input.retryCount ?? 0,
deadLetterCount: input.deadLetterCount ?? 0,
unsupportedVersionCount: input.unsupportedVersionCount ?? 0,
processingLagMs: input.processingLagMs ?? 0,
averageProcessingDurationMs: input.averageProcessingDurationMs ?? 0,
lastRebuildAt: input.lastRebuildAt ? new Date(input.lastRebuildAt) : null,
rebuildSource: input.rebuildSource ?? null
};
await db
.insert(projectionHealthSnapshots)
.values(values)
.onConflictDoUpdate({
target: [projectionHealthSnapshots.organizationId, projectionHealthSnapshots.consumerName],
set: {
projectionName: values.projectionName,
status: values.status,
lastProcessedEventId: values.lastProcessedEventId,
lastSuccessfulProcessingAt: values.lastSuccessfulProcessingAt,
pendingCount: values.pendingCount,
retryCount: values.retryCount,
deadLetterCount: values.deadLetterCount,
unsupportedVersionCount: values.unsupportedVersionCount,
processingLagMs: values.processingLagMs,
averageProcessingDurationMs: values.averageProcessingDurationMs,
lastRebuildAt: values.lastRebuildAt,
rebuildSource: values.rebuildSource,
updatedAt: new Date()
}
});
}
private async getDelivery(eventId: string): Promise<BusinessEventDeliveryRecord | null> {
const [row] = await db