commit show calenda
This commit is contained in:
1176
docs/setup/uat-system-flow.html
Normal file
1176
docs/setup/uat-system-flow.html
Normal file
File diff suppressed because it is too large
Load Diff
@@ -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,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,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,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>
|
||||
|
||||
@@ -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();
|
||||
}
|
||||
|
||||
@@ -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');
|
||||
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user