(toState(sequence));
const createMutation = useMutation({
@@ -76,7 +99,9 @@ function SequenceDialog({
toast.success('Sequence created');
onOpenChange(false);
},
- onError: (error) => toast.error(error instanceof Error ? error.message : 'Create failed')
+ onError: (error) => {
+ toast.error(error instanceof Error ? error.message : 'Create failed');
+ }
});
const updateMutation = useMutation({
...updateDocumentSequenceMutation,
@@ -84,7 +109,9 @@ function SequenceDialog({
toast.success('Sequence updated');
onOpenChange(false);
},
- onError: (error) => toast.error(error instanceof Error ? error.message : 'Update failed')
+ onError: (error) => {
+ toast.error(error instanceof Error ? error.message : 'Update failed');
+ }
});
React.useEffect(() => {
@@ -120,38 +147,57 @@ function SequenceDialog({
setState((current) => ({ ...current, documentType: e.target.value }))}
+ onChange={(event) =>
+ setState((current) => ({ ...current, documentType: event.target.value }))
+ }
/>
setState((current) => ({ ...current, prefix: e.target.value }))}
+ onChange={(event) =>
+ setState((current) => ({ ...current, prefix: event.target.value }))
+ }
/>
setState((current) => ({ ...current, period: e.target.value }))}
+ onChange={(event) =>
+ setState((current) => ({ ...current, period: event.target.value }))
+ }
/>
- setState((current) => ({ ...current, branchId: e.target.value }))}
- />
+ onValueChange={(value) =>
+ setState((current) => ({ ...current, branchId: value }))
+ }
+ >
+
+
+
+
+ ALL - All Branches
+ {branches.map((branch) => (
+
+ {branch.displayName}
+
+ ))}
+
+
- setState((current) => ({ ...current, currentNumber: e.target.value }))
+ onChange={(event) =>
+ setState((current) => ({ ...current, currentNumber: event.target.value }))
}
/>
- setState((current) => ({ ...current, paddingLength: e.target.value }))
+ onChange={(event) =>
+ setState((current) => ({ ...current, paddingLength: event.target.value }))
}
/>
@@ -183,7 +229,7 @@ function SequenceDialog({
);
}
-export function DocumentSequenceSettings() {
+export function DocumentSequenceSettings({ branches }: { branches: FoundationBranch[] }) {
const { data } = useSuspenseQuery(documentSequencesQueryOptions());
const [dialogState, setDialogState] = React.useState<{
open: boolean;
@@ -191,15 +237,25 @@ export function DocumentSequenceSettings() {
}>({
open: false
});
+ const branchOptions = React.useMemo(() => toBranchOptions(branches), [branches]);
+
const resetMutation = useMutation({
...resetDocumentSequenceMutation,
- onSuccess: () => toast.success('Sequence reset'),
- onError: (error) => toast.error(error instanceof Error ? error.message : 'Reset failed')
+ onSuccess: () => {
+ toast.success('Sequence reset');
+ },
+ onError: (error) => {
+ toast.error(error instanceof Error ? error.message : 'Reset failed');
+ }
});
const deleteMutation = useMutation({
...deleteDocumentSequenceMutation,
- onSuccess: () => toast.success('Sequence deleted'),
- onError: (error) => toast.error(error instanceof Error ? error.message : 'Delete failed')
+ onSuccess: () => {
+ toast.success('Sequence deleted');
+ },
+ onError: (error) => {
+ toast.error(error instanceof Error ? error.message : 'Delete failed');
+ }
});
return (
@@ -229,8 +285,16 @@ export function DocumentSequenceSettings() {
{sequence.documentType}
- {sequence.prefix}
- {sequence.period} • branch {sequence.branchId || 'default'}
+ {sequence.prefix} {sequence.period} {'\u2022'}{' '}
+
+ {sequence.branchDisplayName ?? 'All Branches'}
+
@@ -240,7 +304,14 @@ export function DocumentSequenceSettings() {
Next {sequence.nextPreview}
+
+
+
Branch
+
+ {sequence.branchDisplayName ?? 'All Branches'}
+
+
Current Number
{sequence.currentNumber}
@@ -251,17 +322,15 @@ export function DocumentSequenceSettings() {
Updated At
-
- {formatDateTime(sequence.updatedAt)}
-
-
-
-
Preview
-
{sequence.nextPreview}
+
{formatDateTime(sequence.updatedAt)}
+
-
);
diff --git a/src/features/foundation/document-sequence/service.ts b/src/features/foundation/document-sequence/service.ts
index fb3cf39..bf352d2 100644
--- a/src/features/foundation/document-sequence/service.ts
+++ b/src/features/foundation/document-sequence/service.ts
@@ -1,8 +1,9 @@
import { and, asc, eq, sql } from 'drizzle-orm';
import { documentSequences } from '@/db/schema';
+import { resolveBranchDisplays } from '@/features/foundation/display/server/display-resolver';
+import { getCurrentOrganization } from '@/features/foundation/organization-context/service';
import { db } from '@/lib/db';
import { AuthError } from '@/lib/auth/session';
-import { getCurrentOrganization } from '@/features/foundation/organization-context/service';
import type {
DocumentSequenceInput,
DocumentSequenceListItem,
@@ -42,32 +43,24 @@ function normalizeBranchId(branchId?: string | null) {
return branchId?.trim() || '';
}
-function mapSequenceRecord(row: typeof documentSequences.$inferSelect): DocumentSequenceRecord {
- return {
- id: row.id,
- organizationId: row.organizationId,
- branchId: row.branchId || null,
- documentType: row.documentType,
- prefix: row.prefix,
- period: row.period,
- currentNumber: row.currentNumber,
- paddingLength: row.paddingLength,
- isActive: row.isActive,
- createdAt: row.createdAt.toISOString(),
- updatedAt: row.updatedAt.toISOString()
- };
-}
+function formatBranchDisplayName(input: {
+ branchId: string | null;
+ branchCode?: string | null;
+ branchName?: string | null;
+}) {
+ if (!input.branchId) {
+ return 'All Branches';
+ }
-function mapSequenceListItem(row: typeof documentSequences.$inferSelect): DocumentSequenceListItem {
- return {
- ...mapSequenceRecord(row),
- nextPreview: buildDocumentCode(
- row.prefix,
- row.period,
- row.currentNumber + 1,
- row.paddingLength
- )
- };
+ if (input.branchCode && input.branchName) {
+ return `${input.branchCode} - ${input.branchName}`;
+ }
+
+ if (input.branchName) {
+ return input.branchName;
+ }
+
+ return `Unknown Branch (${input.branchId})`;
}
async function resolveOrganizationId(organizationId?: string) {
@@ -76,31 +69,43 @@ async function resolveOrganizationId(organizationId?: string) {
}
const organization = await getCurrentOrganization();
-
if (!organization) {
- throw new Error('Active organization is required');
+ throw new AuthError('Active organization required', 400);
}
return organization.id;
}
-async function assertSequence(id: string, organizationId: string) {
- const [sequence] = await db
- .select()
- .from(documentSequences)
- .where(
- and(eq(documentSequences.id, id), eq(documentSequences.organizationId, organizationId))
- )
- .limit(1);
+function toSequenceResult(row: typeof documentSequences.$inferSelect): DocumentSequenceResult {
+ const nextNumber = row.currentNumber + 1;
- if (!sequence) {
+ return {
+ code: buildDocumentCode(row.prefix, row.period, nextNumber, row.paddingLength),
+ documentType: row.documentType,
+ branchId: row.branchId || null,
+ currentNumber: row.currentNumber,
+ nextNumber,
+ period: row.period,
+ prefix: row.prefix
+ };
+}
+
+async function assertSequence(id: string, organizationId: string) {
+ const row = await db.query.documentSequences.findFirst({
+ where: and(
+ eq(documentSequences.id, id),
+ eq(documentSequences.organizationId, organizationId)
+ )
+ });
+
+ if (!row) {
throw new AuthError('Document sequence not found', 404);
}
- return sequence;
+ return row;
}
-async function ensureSequence(
+async function ensureSequenceRow(
organizationId: string,
documentType: string,
period: string,
@@ -119,7 +124,8 @@ async function ensureSequence(
return existing;
}
- const prefix = DEFAULT_DOCUMENT_PREFIXES[documentType] ?? documentType.slice(0, 3).toUpperCase();
+ const prefix =
+ DEFAULT_DOCUMENT_PREFIXES[documentType] ?? documentType.slice(0, 3).toUpperCase();
const [created] = await db
.insert(documentSequences)
@@ -139,21 +145,45 @@ async function ensureSequence(
return created;
}
-function toSequenceResult(row: typeof documentSequences.$inferSelect): DocumentSequenceResult {
- const nextNumber = row.currentNumber + 1;
+async function mapSequenceRecords(
+ organizationId: string,
+ rows: Array
+) {
+ const branchDisplayMap = await resolveBranchDisplays({
+ organizationId,
+ branchIds: rows.map((row) => row.branchId || null)
+ });
- return {
- code: buildDocumentCode(row.prefix, row.period, nextNumber, row.paddingLength),
- documentType: row.documentType,
- branchId: row.branchId || null,
- currentNumber: row.currentNumber,
- nextNumber,
- period: row.period,
- prefix: row.prefix
- };
+ return rows.map((row) => {
+ const branchId = row.branchId || null;
+ const branchDisplay = branchId ? (branchDisplayMap.get(branchId) ?? null) : null;
+
+ return {
+ id: row.id,
+ organizationId: row.organizationId,
+ branchId,
+ branchCode: branchDisplay?.code ?? null,
+ branchName: branchDisplay?.name ?? null,
+ branchDisplayName: formatBranchDisplayName({
+ branchId,
+ branchCode: branchDisplay?.code ?? null,
+ branchName: branchDisplay?.name ?? null
+ }),
+ documentType: row.documentType,
+ prefix: row.prefix,
+ period: row.period,
+ currentNumber: row.currentNumber,
+ paddingLength: row.paddingLength,
+ isActive: row.isActive,
+ createdAt: row.createdAt.toISOString(),
+ updatedAt: row.updatedAt.toISOString()
+ } satisfies DocumentSequenceRecord;
+ });
}
-export async function listDocumentSequences(organizationId: string): Promise {
+export async function listDocumentSequences(
+ organizationId: string
+): Promise {
const rows = await db
.select()
.from(documentSequences)
@@ -164,12 +194,27 @@ export async function listDocumentSequences(organizationId: string): Promise ({
+ ...record,
+ nextPreview: buildDocumentCode(
+ record.prefix,
+ record.period,
+ record.currentNumber + 1,
+ record.paddingLength
+ )
+ }));
}
-export async function getDocumentSequence(id: string, organizationId: string): Promise {
+export async function getDocumentSequence(
+ id: string,
+ organizationId: string
+): Promise {
const row = await assertSequence(id, organizationId);
- return mapSequenceRecord(row);
+ const [record] = await mapSequenceRecords(organizationId, [row]);
+
+ return record;
}
export async function createDocumentSequence(
@@ -193,7 +238,8 @@ export async function createDocumentSequence(
})
.returning();
- return mapSequenceRecord(created);
+ const [record] = await mapSequenceRecords(organizationId, [created]);
+ return record;
}
export async function updateDocumentSequence(
@@ -209,7 +255,9 @@ export async function updateDocumentSequence(
prefix: payload.prefix?.trim() ?? current.prefix,
period: payload.period?.trim() ?? current.period,
branchId:
- payload.branchId === undefined ? current.branchId : normalizeBranchId(payload.branchId),
+ payload.branchId === undefined
+ ? current.branchId
+ : normalizeBranchId(payload.branchId),
currentNumber: payload.currentNumber ?? current.currentNumber,
paddingLength: payload.paddingLength ?? current.paddingLength,
isActive: payload.isActive ?? current.isActive,
@@ -218,7 +266,8 @@ export async function updateDocumentSequence(
.where(eq(documentSequences.id, id))
.returning();
- return mapSequenceRecord(updated);
+ const [record] = await mapSequenceRecords(organizationId, [updated]);
+ return record;
}
export async function resetDocumentSequence(
@@ -236,75 +285,40 @@ export async function resetDocumentSequence(
.where(eq(documentSequences.id, id))
.returning();
- return mapSequenceRecord(updated);
+ const [record] = await mapSequenceRecords(organizationId, [updated]);
+ return record;
}
-export async function deleteDocumentSequence(id: string, organizationId: string): Promise {
- await assertSequence(id, organizationId);
- const [deleted] = await db
- .delete(documentSequences)
- .where(eq(documentSequences.id, id))
- .returning();
-
- return mapSequenceRecord(deleted);
-}
-
-export async function previewDocumentSequenceById(
- id: string,
- organizationId: string
-): Promise {
+export async function deleteDocumentSequence(id: string, organizationId: string) {
const sequence = await assertSequence(id, organizationId);
- return toSequenceResult(sequence);
+ await db.delete(documentSequences).where(eq(documentSequences.id, id));
+
+ const [record] = await mapSequenceRecords(organizationId, [sequence]);
+ return record;
}
-export async function previewNextDocumentCode(
- input: DocumentSequenceInput
-): Promise {
+export async function previewDocumentSequenceById(id: string, organizationId: string) {
+ const row = await assertSequence(id, organizationId);
+ return toSequenceResult(row);
+}
+
+export async function generateNextDocumentCode(input: DocumentSequenceInput) {
const organizationId = await resolveOrganizationId(input.organizationId);
const period = input.period ?? getCurrentPeriod();
const branchId = normalizeBranchId(input.branchId);
- const sequence = await ensureSequence(organizationId, input.documentType, period, branchId);
-
- return toSequenceResult(sequence);
-}
-
-export async function generateNextDocumentCode(
- input: DocumentSequenceInput
-): Promise {
- const organizationId = await resolveOrganizationId(input.organizationId);
- const period = input.period ?? getCurrentPeriod();
- const branchId = normalizeBranchId(input.branchId);
-
- await ensureSequence(organizationId, input.documentType, period, branchId);
return db.transaction(async (tx) => {
- await tx.execute(sql`
- select id
- from document_sequences
- where organization_id = ${organizationId}
- and document_type = ${input.documentType}
- and period = ${period}
- and branch_id = ${branchId}
- for update
- `);
-
- const sequence = await tx.query.documentSequences.findFirst({
- where: and(
- eq(documentSequences.organizationId, organizationId),
- eq(documentSequences.documentType, input.documentType),
- eq(documentSequences.period, period),
- eq(documentSequences.branchId, branchId)
- )
- });
-
- if (!sequence) {
- throw new Error('Document sequence not found');
- }
+ const sequence = await ensureSequenceRow(
+ organizationId,
+ input.documentType.trim(),
+ period,
+ branchId
+ );
const [updated] = await tx
.update(documentSequences)
.set({
- currentNumber: sequence.currentNumber + 1,
+ currentNumber: sql`${documentSequences.currentNumber} + 1`,
updatedAt: new Date()
})
.where(eq(documentSequences.id, sequence.id))
@@ -323,7 +337,6 @@ export async function generateNextDocumentCode(
nextNumber: updated.currentNumber + 1,
period: updated.period,
prefix: updated.prefix
- };
+ } satisfies DocumentSequenceResult;
});
}
-
diff --git a/src/features/foundation/document-sequence/types.ts b/src/features/foundation/document-sequence/types.ts
index 160368f..2c07885 100644
--- a/src/features/foundation/document-sequence/types.ts
+++ b/src/features/foundation/document-sequence/types.ts
@@ -9,6 +9,9 @@ export interface DocumentSequenceRecord {
id: string;
organizationId: string;
branchId: string | null;
+ branchCode: string | null;
+ branchName: string | null;
+ branchDisplayName: string | null;
documentType: string;
prefix: string;
period: string;
@@ -33,6 +36,13 @@ export interface DocumentSequenceListItem extends DocumentSequenceRecord {
nextPreview: string;
}
+export interface DocumentSequenceBranchOption {
+ id: string;
+ code: string;
+ name: string;
+ displayName: string;
+}
+
export interface DocumentSequenceListResponse {
success: boolean;
time: string;
diff --git a/src/features/foundation/master-options/components/master-options-create-action.tsx b/src/features/foundation/master-options/components/master-options-create-action.tsx
new file mode 100644
index 0000000..46adbc1
--- /dev/null
+++ b/src/features/foundation/master-options/components/master-options-create-action.tsx
@@ -0,0 +1,46 @@
+'use client';
+
+import { useState } from 'react';
+import { useMutation, useQuery } from '@tanstack/react-query';
+import { toast } from 'sonner';
+import { Icons } from '@/components/icons';
+import { Button } from '@/components/ui/button';
+import { createMasterOptionMutation } from '../api/mutations';
+import { masterOptionsQueryOptions } from '../api/queries';
+import type { MasterOptionMutationPayload } from '../api/types';
+import { MasterOptionsFormDialog } from './master-options-form-dialog';
+
+export function MasterOptionsCreateAction() {
+ const [open, setOpen] = useState(false);
+ const { data } = useQuery(masterOptionsQueryOptions({ page: 1, limit: 200 }));
+ const createMutation = useMutation({
+ ...createMasterOptionMutation,
+ onSuccess: () => {
+ toast.success('Master option created.');
+ setOpen(false);
+ },
+ onError: (error) => {
+ toast.error(error instanceof Error ? error.message : 'Create failed');
+ }
+ });
+
+ async function handleSubmit(values: MasterOptionMutationPayload) {
+ await createMutation.mutateAsync(values);
+ }
+
+ return (
+ <>
+ setOpen(true)}>
+
+ Add Option
+
+
+ >
+ );
+}
diff --git a/src/features/foundation/master-options/components/master-options-table.tsx b/src/features/foundation/master-options/components/master-options-table.tsx
index 6716772..563bc4c 100644
--- a/src/features/foundation/master-options/components/master-options-table.tsx
+++ b/src/features/foundation/master-options/components/master-options-table.tsx
@@ -138,18 +138,7 @@ export function MasterOptionsTable() {
return (
-
-
- {
- setEditingOption(undefined);
- setIsFormOpen(true);
- }}
- >
-
- Add Option
-
-
+