task-fix-display2
This commit is contained in:
@@ -15,8 +15,16 @@ import {
|
||||
DialogTitle
|
||||
} from '@/components/ui/dialog';
|
||||
import { Input } from '@/components/ui/input';
|
||||
import {
|
||||
Select,
|
||||
SelectContent,
|
||||
SelectItem,
|
||||
SelectTrigger,
|
||||
SelectValue
|
||||
} from '@/components/ui/select';
|
||||
import { Switch } from '@/components/ui/switch';
|
||||
import { formatDateTime } from '@/lib/date-format';
|
||||
import type { FoundationBranch } from '@/features/foundation/branch-scope/types';
|
||||
import {
|
||||
createDocumentSequenceMutation,
|
||||
deleteDocumentSequenceMutation,
|
||||
@@ -24,7 +32,11 @@ import {
|
||||
updateDocumentSequenceMutation
|
||||
} from '../mutations';
|
||||
import { documentSequencesQueryOptions } from '../queries';
|
||||
import type { DocumentSequenceListItem, DocumentSequenceMutationPayload } from '../types';
|
||||
import type {
|
||||
DocumentSequenceBranchOption,
|
||||
DocumentSequenceListItem,
|
||||
DocumentSequenceMutationPayload
|
||||
} from '../types';
|
||||
|
||||
type SequenceState = {
|
||||
documentType: string;
|
||||
@@ -36,12 +48,21 @@ type SequenceState = {
|
||||
isActive: boolean;
|
||||
};
|
||||
|
||||
function toBranchOptions(branches: FoundationBranch[]): DocumentSequenceBranchOption[] {
|
||||
return branches.map((branch) => ({
|
||||
id: branch.id,
|
||||
code: branch.code,
|
||||
name: branch.name,
|
||||
displayName: `${branch.code.toUpperCase()} - ${branch.name}`
|
||||
}));
|
||||
}
|
||||
|
||||
function toState(sequence?: DocumentSequenceListItem): SequenceState {
|
||||
return {
|
||||
documentType: sequence?.documentType ?? 'quotation',
|
||||
prefix: sequence?.prefix ?? 'QT',
|
||||
period: sequence?.period ?? '',
|
||||
branchId: sequence?.branchId ?? '',
|
||||
branchId: sequence?.branchId ?? '__all__',
|
||||
currentNumber: String(sequence?.currentNumber ?? 0),
|
||||
paddingLength: String(sequence?.paddingLength ?? 3),
|
||||
isActive: sequence?.isActive ?? true
|
||||
@@ -53,7 +74,7 @@ function buildPayload(state: SequenceState): DocumentSequenceMutationPayload {
|
||||
documentType: state.documentType,
|
||||
prefix: state.prefix,
|
||||
period: state.period,
|
||||
branchId: state.branchId || null,
|
||||
branchId: state.branchId === '__all__' ? null : state.branchId,
|
||||
currentNumber: Number(state.currentNumber || 0),
|
||||
paddingLength: Number(state.paddingLength || 3),
|
||||
isActive: state.isActive
|
||||
@@ -63,11 +84,13 @@ function buildPayload(state: SequenceState): DocumentSequenceMutationPayload {
|
||||
function SequenceDialog({
|
||||
open,
|
||||
onOpenChange,
|
||||
sequence
|
||||
sequence,
|
||||
branches
|
||||
}: {
|
||||
open: boolean;
|
||||
onOpenChange: (open: boolean) => void;
|
||||
sequence?: DocumentSequenceListItem;
|
||||
branches: DocumentSequenceBranchOption[];
|
||||
}) {
|
||||
const [state, setState] = React.useState<SequenceState>(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({
|
||||
<Input
|
||||
placeholder='Document type'
|
||||
value={state.documentType}
|
||||
onChange={(e) => setState((current) => ({ ...current, documentType: e.target.value }))}
|
||||
onChange={(event) =>
|
||||
setState((current) => ({ ...current, documentType: event.target.value }))
|
||||
}
|
||||
/>
|
||||
<div className='grid gap-4 md:grid-cols-2'>
|
||||
<Input
|
||||
placeholder='Prefix'
|
||||
value={state.prefix}
|
||||
onChange={(e) => setState((current) => ({ ...current, prefix: e.target.value }))}
|
||||
onChange={(event) =>
|
||||
setState((current) => ({ ...current, prefix: event.target.value }))
|
||||
}
|
||||
/>
|
||||
<Input
|
||||
placeholder='Period'
|
||||
value={state.period}
|
||||
onChange={(e) => setState((current) => ({ ...current, period: e.target.value }))}
|
||||
onChange={(event) =>
|
||||
setState((current) => ({ ...current, period: event.target.value }))
|
||||
}
|
||||
/>
|
||||
</div>
|
||||
<div className='grid gap-4 md:grid-cols-3'>
|
||||
<Input
|
||||
placeholder='Branch ID (optional)'
|
||||
<Select
|
||||
value={state.branchId}
|
||||
onChange={(e) => setState((current) => ({ ...current, branchId: e.target.value }))}
|
||||
/>
|
||||
onValueChange={(value) =>
|
||||
setState((current) => ({ ...current, branchId: value }))
|
||||
}
|
||||
>
|
||||
<SelectTrigger>
|
||||
<SelectValue placeholder='Select branch scope' />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
<SelectItem value='__all__'>ALL - All Branches</SelectItem>
|
||||
{branches.map((branch) => (
|
||||
<SelectItem key={branch.id} value={branch.id}>
|
||||
{branch.displayName}
|
||||
</SelectItem>
|
||||
))}
|
||||
</SelectContent>
|
||||
</Select>
|
||||
<Input
|
||||
placeholder='Current number'
|
||||
value={state.currentNumber}
|
||||
onChange={(e) =>
|
||||
setState((current) => ({ ...current, currentNumber: e.target.value }))
|
||||
onChange={(event) =>
|
||||
setState((current) => ({ ...current, currentNumber: event.target.value }))
|
||||
}
|
||||
/>
|
||||
<Input
|
||||
placeholder='Padding length'
|
||||
value={state.paddingLength}
|
||||
onChange={(e) =>
|
||||
setState((current) => ({ ...current, paddingLength: e.target.value }))
|
||||
onChange={(event) =>
|
||||
setState((current) => ({ ...current, paddingLength: event.target.value }))
|
||||
}
|
||||
/>
|
||||
</div>
|
||||
@@ -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() {
|
||||
<div>
|
||||
<div className='text-xl font-semibold'>{sequence.documentType}</div>
|
||||
<div className='text-muted-foreground text-sm'>
|
||||
{sequence.prefix}
|
||||
{sequence.period} • branch {sequence.branchId || 'default'}
|
||||
{sequence.prefix} {sequence.period} {'\u2022'}{' '}
|
||||
<span
|
||||
className={
|
||||
sequence.branchName || !sequence.branchId
|
||||
? undefined
|
||||
: 'text-muted-foreground italic'
|
||||
}
|
||||
>
|
||||
{sequence.branchDisplayName ?? 'All Branches'}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
<div className='flex gap-2'>
|
||||
@@ -240,7 +304,14 @@ export function DocumentSequenceSettings() {
|
||||
<Badge variant='outline'>Next {sequence.nextPreview}</Badge>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className='mt-4 grid gap-4 md:grid-cols-4'>
|
||||
<div className='rounded-lg bg-muted/40 p-4'>
|
||||
<div className='text-muted-foreground text-xs'>Branch</div>
|
||||
<div className='mt-1 text-sm font-medium'>
|
||||
{sequence.branchDisplayName ?? 'All Branches'}
|
||||
</div>
|
||||
</div>
|
||||
<div className='rounded-lg bg-muted/40 p-4'>
|
||||
<div className='text-muted-foreground text-xs'>Current Number</div>
|
||||
<div className='mt-1 text-sm font-medium'>{sequence.currentNumber}</div>
|
||||
@@ -251,17 +322,15 @@ export function DocumentSequenceSettings() {
|
||||
</div>
|
||||
<div className='rounded-lg bg-muted/40 p-4'>
|
||||
<div className='text-muted-foreground text-xs'>Updated At</div>
|
||||
<div className='mt-1 text-sm font-medium'>
|
||||
{formatDateTime(sequence.updatedAt)}
|
||||
</div>
|
||||
</div>
|
||||
<div className='rounded-lg bg-muted/40 p-4'>
|
||||
<div className='text-muted-foreground text-xs'>Preview</div>
|
||||
<div className='mt-1 text-sm font-medium'>{sequence.nextPreview}</div>
|
||||
<div className='mt-1 text-sm font-medium'>{formatDateTime(sequence.updatedAt)}</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className='mt-4 flex flex-wrap gap-2'>
|
||||
<Button variant='outline' onClick={() => setDialogState({ open: true, sequence })}>
|
||||
<Button
|
||||
variant='outline'
|
||||
onClick={() => setDialogState({ open: true, sequence })}
|
||||
>
|
||||
Edit Sequence
|
||||
</Button>
|
||||
<Button
|
||||
@@ -292,6 +361,7 @@ export function DocumentSequenceSettings() {
|
||||
open={dialogState.open}
|
||||
onOpenChange={(open) => setDialogState((current) => ({ ...current, open }))}
|
||||
sequence={dialogState.sequence}
|
||||
branches={branchOptions}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
|
||||
@@ -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<typeof documentSequences.$inferSelect>
|
||||
) {
|
||||
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<DocumentSequenceListItem[]> {
|
||||
export async function listDocumentSequences(
|
||||
organizationId: string
|
||||
): Promise<DocumentSequenceListItem[]> {
|
||||
const rows = await db
|
||||
.select()
|
||||
.from(documentSequences)
|
||||
@@ -164,12 +194,27 @@ export async function listDocumentSequences(organizationId: string): Promise<Doc
|
||||
asc(documentSequences.branchId)
|
||||
);
|
||||
|
||||
return rows.map(mapSequenceListItem);
|
||||
const records = await mapSequenceRecords(organizationId, rows);
|
||||
|
||||
return records.map((record) => ({
|
||||
...record,
|
||||
nextPreview: buildDocumentCode(
|
||||
record.prefix,
|
||||
record.period,
|
||||
record.currentNumber + 1,
|
||||
record.paddingLength
|
||||
)
|
||||
}));
|
||||
}
|
||||
|
||||
export async function getDocumentSequence(id: string, organizationId: string): Promise<DocumentSequenceRecord> {
|
||||
export async function getDocumentSequence(
|
||||
id: string,
|
||||
organizationId: string
|
||||
): Promise<DocumentSequenceRecord> {
|
||||
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<DocumentSequenceRecord> {
|
||||
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<DocumentSequenceResult> {
|
||||
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<DocumentSequenceResult> {
|
||||
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<DocumentSequenceResult> {
|
||||
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;
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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 (
|
||||
<>
|
||||
<Button onClick={() => setOpen(true)}>
|
||||
<Icons.add className='mr-2 h-4 w-4' />
|
||||
Add Option
|
||||
</Button>
|
||||
<MasterOptionsFormDialog
|
||||
open={open}
|
||||
onOpenChange={setOpen}
|
||||
onSubmit={handleSubmit}
|
||||
pending={createMutation.isPending}
|
||||
availableParents={data?.items ?? []}
|
||||
/>
|
||||
</>
|
||||
);
|
||||
}
|
||||
@@ -138,18 +138,7 @@ export function MasterOptionsTable() {
|
||||
|
||||
return (
|
||||
<DataTable table={table}>
|
||||
<div className='flex flex-wrap items-center justify-between gap-3'>
|
||||
<DataTableToolbar table={table} />
|
||||
<Button
|
||||
onClick={() => {
|
||||
setEditingOption(undefined);
|
||||
setIsFormOpen(true);
|
||||
}}
|
||||
>
|
||||
<Icons.add className='mr-2 h-4 w-4' />
|
||||
Add Option
|
||||
</Button>
|
||||
</div>
|
||||
<DataTableToolbar table={table} />
|
||||
<MasterOptionsFormDialog
|
||||
open={isFormOpen}
|
||||
onOpenChange={setIsFormOpen}
|
||||
|
||||
Reference in New Issue
Block a user