task-fix-display2
This commit is contained in:
2
next-env.d.ts
vendored
2
next-env.d.ts
vendored
@@ -1,6 +1,6 @@
|
|||||||
/// <reference types="next" />
|
/// <reference types="next" />
|
||||||
/// <reference types="next/image-types/global" />
|
/// <reference types="next/image-types/global" />
|
||||||
import "./.next/dev/types/routes.d.ts";
|
import "./.next/types/routes.d.ts";
|
||||||
|
|
||||||
// NOTE: This file should not be edited
|
// NOTE: This file should not be edited
|
||||||
// see https://nextjs.org/docs/app/api-reference/config/typescript for more information.
|
// see https://nextjs.org/docs/app/api-reference/config/typescript for more information.
|
||||||
|
|||||||
228
plans/task-fix-display2.md
Normal file
228
plans/task-fix-display2.md
Normal file
@@ -0,0 +1,228 @@
|
|||||||
|
# Task Fix: Document Sequence Branch Display Name
|
||||||
|
|
||||||
|
## Objective
|
||||||
|
|
||||||
|
Fix `/dashboard/crm/settings/document-sequences` so Branch displays as a readable name/code instead of raw UUID.
|
||||||
|
|
||||||
|
Current issue:
|
||||||
|
|
||||||
|
```txt
|
||||||
|
1b960bfc-91de-4b69-a389-f060ef21bed2
|
||||||
|
```
|
||||||
|
|
||||||
|
should display as:
|
||||||
|
|
||||||
|
```txt
|
||||||
|
Branch Name
|
||||||
|
```
|
||||||
|
|
||||||
|
or:
|
||||||
|
|
||||||
|
```txt
|
||||||
|
Branch Code - Branch Name
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Problem
|
||||||
|
|
||||||
|
Document Sequence records store:
|
||||||
|
|
||||||
|
```txt
|
||||||
|
branchId
|
||||||
|
```
|
||||||
|
|
||||||
|
but the settings UI currently displays the raw ID directly.
|
||||||
|
|
||||||
|
This breaks admin usability and is inconsistent with the display-name cleanup direction.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Mandatory Review
|
||||||
|
|
||||||
|
Review:
|
||||||
|
|
||||||
|
```txt
|
||||||
|
src/features/foundation/document-sequence/**
|
||||||
|
src/app/dashboard/crm/settings/document-sequences/**
|
||||||
|
src/db/schema.ts
|
||||||
|
src/features/foundation/display/**
|
||||||
|
src/features/foundation/master-options/**
|
||||||
|
```
|
||||||
|
|
||||||
|
Search:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
rg "branchId|branch_id|document-sequences|DocumentSequence" src/features/foundation src/app/dashboard/crm/settings/document-sequences
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Scope 1: API / DTO Display Fields
|
||||||
|
|
||||||
|
Update document sequence read model to include:
|
||||||
|
|
||||||
|
```ts
|
||||||
|
branchId?: string | null;
|
||||||
|
branchCode?: string | null;
|
||||||
|
branchName?: string | null;
|
||||||
|
branchDisplayName?: string | null;
|
||||||
|
```
|
||||||
|
|
||||||
|
Display rule:
|
||||||
|
|
||||||
|
```txt
|
||||||
|
branchId = null
|
||||||
|
→ All Branches
|
||||||
|
|
||||||
|
branchCode + branchName
|
||||||
|
→ {branchCode} - {branchName}
|
||||||
|
|
||||||
|
branchName only
|
||||||
|
→ {branchName}
|
||||||
|
|
||||||
|
unknown branch
|
||||||
|
→ Unknown Branch ({branchId})
|
||||||
|
```
|
||||||
|
|
||||||
|
Do not remove `branchId`; keep it for update/generate logic.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Scope 2: Resolve Branch Display Server-Side
|
||||||
|
|
||||||
|
In document sequence service/list API, resolve branch display before returning response.
|
||||||
|
|
||||||
|
Preferred:
|
||||||
|
|
||||||
|
```txt
|
||||||
|
Use shared branch display resolver if already created.
|
||||||
|
```
|
||||||
|
|
||||||
|
Fallback:
|
||||||
|
|
||||||
|
* If branch is table-backed, join branch table.
|
||||||
|
* If branch is `ms_options`, resolve from the branch option category.
|
||||||
|
* If unknown, return fallback display string.
|
||||||
|
|
||||||
|
Avoid N+1 queries.
|
||||||
|
|
||||||
|
Batch resolve all branch IDs in the list.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Scope 3: UI Rendering Fix
|
||||||
|
|
||||||
|
Update document sequence table/cards.
|
||||||
|
|
||||||
|
Replace:
|
||||||
|
|
||||||
|
```tsx
|
||||||
|
{row.branchId}
|
||||||
|
```
|
||||||
|
|
||||||
|
with:
|
||||||
|
|
||||||
|
```tsx
|
||||||
|
{row.branchDisplayName ?? 'All Branches'}
|
||||||
|
```
|
||||||
|
|
||||||
|
or:
|
||||||
|
|
||||||
|
```tsx
|
||||||
|
<BranchDisplay value={row.branchDisplayName} />
|
||||||
|
```
|
||||||
|
|
||||||
|
Rules:
|
||||||
|
|
||||||
|
* never show raw UUID in normal UI
|
||||||
|
* raw ID may be visible only in debug/dev details if explicitly labelled
|
||||||
|
* use muted text for fallback unknown branch
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Scope 4: Form Select Display
|
||||||
|
|
||||||
|
If create/edit form has branch select:
|
||||||
|
|
||||||
|
* show readable branch names in dropdown
|
||||||
|
* value remains `branchId`
|
||||||
|
* include option:
|
||||||
|
|
||||||
|
```txt
|
||||||
|
All Branches
|
||||||
|
```
|
||||||
|
|
||||||
|
for nullable branch
|
||||||
|
|
||||||
|
Dropdown labels:
|
||||||
|
|
||||||
|
```txt
|
||||||
|
ALL - All Branches
|
||||||
|
BKK - Bangkok
|
||||||
|
RYG - Rayong
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Scope 5: Seed Check
|
||||||
|
|
||||||
|
Verify seed creates branch data that can be resolved.
|
||||||
|
|
||||||
|
Check whether branch source is:
|
||||||
|
|
||||||
|
```txt
|
||||||
|
branch table
|
||||||
|
```
|
||||||
|
|
||||||
|
or:
|
||||||
|
|
||||||
|
```txt
|
||||||
|
ms_options category
|
||||||
|
```
|
||||||
|
|
||||||
|
If document sequence references a branch ID that seed does not create, fix seed.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Scope 6: Verification
|
||||||
|
|
||||||
|
Run:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
npm exec tsc --noEmit
|
||||||
|
npm run build
|
||||||
|
```
|
||||||
|
|
||||||
|
Manual verification:
|
||||||
|
|
||||||
|
```txt
|
||||||
|
Open /dashboard/crm/settings/document-sequences
|
||||||
|
No raw UUID is shown for branch
|
||||||
|
All Branches displays correctly for null branchId
|
||||||
|
Known branch displays readable name/code
|
||||||
|
Unknown branch fallback is readable
|
||||||
|
Create/edit branch dropdown shows names, not IDs
|
||||||
|
Saving still persists branchId correctly
|
||||||
|
Document number generation still works
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Definition of Done
|
||||||
|
|
||||||
|
Task is complete when:
|
||||||
|
|
||||||
|
* Document Sequence list does not show raw branch UUID
|
||||||
|
* API returns branch display fields
|
||||||
|
* UI uses branch display fields
|
||||||
|
* branch select uses readable labels
|
||||||
|
* document sequence generation behavior remains unchanged
|
||||||
|
|
||||||
|
Result:
|
||||||
|
|
||||||
|
```txt
|
||||||
|
Document Sequence Branch Display = Fixed
|
||||||
|
Admin UX = Improved
|
||||||
|
Raw Branch UUID Leakage = Removed
|
||||||
|
```
|
||||||
@@ -1,6 +1,7 @@
|
|||||||
import { auth } from '@/auth';
|
import { auth } from '@/auth';
|
||||||
import { HydrationBoundary, dehydrate } from '@tanstack/react-query';
|
import { HydrationBoundary, dehydrate } from '@tanstack/react-query';
|
||||||
import PageContainer from '@/components/layout/page-container';
|
import PageContainer from '@/components/layout/page-container';
|
||||||
|
import { getUserBranches } from '@/features/foundation/branch-scope/service';
|
||||||
import { DocumentSequenceSettings } from '@/features/foundation/document-sequence/components/document-sequence-settings';
|
import { DocumentSequenceSettings } from '@/features/foundation/document-sequence/components/document-sequence-settings';
|
||||||
import { documentSequencesQueryOptions } from '@/features/foundation/document-sequence/queries';
|
import { documentSequencesQueryOptions } from '@/features/foundation/document-sequence/queries';
|
||||||
import { PERMISSIONS } from '@/lib/auth/rbac';
|
import { PERMISSIONS } from '@/lib/auth/rbac';
|
||||||
@@ -19,6 +20,11 @@ export default async function DocumentSequencesRoute() {
|
|||||||
void queryClient.prefetchQuery(documentSequencesQueryOptions());
|
void queryClient.prefetchQuery(documentSequencesQueryOptions());
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const branches =
|
||||||
|
canRead && session?.user?.activeOrganizationId
|
||||||
|
? await getUserBranches(session.user.activeOrganizationId)
|
||||||
|
: [];
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<PageContainer
|
<PageContainer
|
||||||
pageTitle='Document Sequences'
|
pageTitle='Document Sequences'
|
||||||
@@ -32,7 +38,7 @@ export default async function DocumentSequencesRoute() {
|
|||||||
>
|
>
|
||||||
{canRead ? (
|
{canRead ? (
|
||||||
<HydrationBoundary state={dehydrate(queryClient)}>
|
<HydrationBoundary state={dehydrate(queryClient)}>
|
||||||
<DocumentSequenceSettings />
|
<DocumentSequenceSettings branches={branches} />
|
||||||
</HydrationBoundary>
|
</HydrationBoundary>
|
||||||
) : null}
|
) : null}
|
||||||
</PageContainer>
|
</PageContainer>
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
import { auth } from '@/auth';
|
import { auth } from '@/auth';
|
||||||
import PageContainer from '@/components/layout/page-container';
|
import PageContainer from '@/components/layout/page-container';
|
||||||
|
import { MasterOptionsCreateAction } from '@/features/foundation/master-options/components/master-options-create-action';
|
||||||
import MasterOptionsListingPage from '@/features/foundation/master-options/components/master-options-listing';
|
import MasterOptionsListingPage from '@/features/foundation/master-options/components/master-options-listing';
|
||||||
import { PERMISSIONS } from '@/lib/auth/rbac';
|
import { PERMISSIONS } from '@/lib/auth/rbac';
|
||||||
import { searchParamsCache } from '@/lib/searchparams';
|
import { searchParamsCache } from '@/lib/searchparams';
|
||||||
@@ -12,11 +13,16 @@ type PageProps = {
|
|||||||
export default async function MasterOptionsRoute(props: PageProps) {
|
export default async function MasterOptionsRoute(props: PageProps) {
|
||||||
const searchParams = await props.searchParams;
|
const searchParams = await props.searchParams;
|
||||||
const session = await auth();
|
const session = await auth();
|
||||||
const canManageOptions =
|
const canReadOptions =
|
||||||
session?.user?.systemRole === 'super_admin' ||
|
session?.user?.systemRole === 'super_admin' ||
|
||||||
(!!session?.user?.activeOrganizationId &&
|
(!!session?.user?.activeOrganizationId &&
|
||||||
(session.user.activeMembershipRole === 'admin' ||
|
(session.user.activeMembershipRole === 'admin' ||
|
||||||
session.user.activePermissions.includes(PERMISSIONS.crmMasterOptionRead)));
|
session.user.activePermissions.includes(PERMISSIONS.crmMasterOptionRead)));
|
||||||
|
const canCreateOptions =
|
||||||
|
session?.user?.systemRole === 'super_admin' ||
|
||||||
|
(!!session?.user?.activeOrganizationId &&
|
||||||
|
(session.user.activeMembershipRole === 'admin' ||
|
||||||
|
session.user.activePermissions.includes(PERMISSIONS.crmMasterOptionCreate)));
|
||||||
|
|
||||||
searchParamsCache.parse(searchParams);
|
searchParamsCache.parse(searchParams);
|
||||||
|
|
||||||
@@ -24,12 +30,13 @@ export default async function MasterOptionsRoute(props: PageProps) {
|
|||||||
<PageContainer
|
<PageContainer
|
||||||
pageTitle='Master Options'
|
pageTitle='Master Options'
|
||||||
pageDescription='Organization-scoped CRM option registry for statuses, product types, payment terms, currency, and branch abstractions.'
|
pageDescription='Organization-scoped CRM option registry for statuses, product types, payment terms, currency, and branch abstractions.'
|
||||||
access={canManageOptions}
|
access={canReadOptions}
|
||||||
accessFallback={
|
accessFallback={
|
||||||
<div className='text-muted-foreground rounded-lg border border-dashed p-8 text-center'>
|
<div className='text-muted-foreground rounded-lg border border-dashed p-8 text-center'>
|
||||||
You do not have access to master option management.
|
You do not have access to master option management.
|
||||||
</div>
|
</div>
|
||||||
}
|
}
|
||||||
|
pageHeaderAction={canCreateOptions ? <MasterOptionsCreateAction /> : null}
|
||||||
>
|
>
|
||||||
<MasterOptionsListingPage />
|
<MasterOptionsListingPage />
|
||||||
</PageContainer>
|
</PageContainer>
|
||||||
|
|||||||
@@ -15,8 +15,16 @@ import {
|
|||||||
DialogTitle
|
DialogTitle
|
||||||
} from '@/components/ui/dialog';
|
} from '@/components/ui/dialog';
|
||||||
import { Input } from '@/components/ui/input';
|
import { Input } from '@/components/ui/input';
|
||||||
|
import {
|
||||||
|
Select,
|
||||||
|
SelectContent,
|
||||||
|
SelectItem,
|
||||||
|
SelectTrigger,
|
||||||
|
SelectValue
|
||||||
|
} from '@/components/ui/select';
|
||||||
import { Switch } from '@/components/ui/switch';
|
import { Switch } from '@/components/ui/switch';
|
||||||
import { formatDateTime } from '@/lib/date-format';
|
import { formatDateTime } from '@/lib/date-format';
|
||||||
|
import type { FoundationBranch } from '@/features/foundation/branch-scope/types';
|
||||||
import {
|
import {
|
||||||
createDocumentSequenceMutation,
|
createDocumentSequenceMutation,
|
||||||
deleteDocumentSequenceMutation,
|
deleteDocumentSequenceMutation,
|
||||||
@@ -24,7 +32,11 @@ import {
|
|||||||
updateDocumentSequenceMutation
|
updateDocumentSequenceMutation
|
||||||
} from '../mutations';
|
} from '../mutations';
|
||||||
import { documentSequencesQueryOptions } from '../queries';
|
import { documentSequencesQueryOptions } from '../queries';
|
||||||
import type { DocumentSequenceListItem, DocumentSequenceMutationPayload } from '../types';
|
import type {
|
||||||
|
DocumentSequenceBranchOption,
|
||||||
|
DocumentSequenceListItem,
|
||||||
|
DocumentSequenceMutationPayload
|
||||||
|
} from '../types';
|
||||||
|
|
||||||
type SequenceState = {
|
type SequenceState = {
|
||||||
documentType: string;
|
documentType: string;
|
||||||
@@ -36,12 +48,21 @@ type SequenceState = {
|
|||||||
isActive: boolean;
|
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 {
|
function toState(sequence?: DocumentSequenceListItem): SequenceState {
|
||||||
return {
|
return {
|
||||||
documentType: sequence?.documentType ?? 'quotation',
|
documentType: sequence?.documentType ?? 'quotation',
|
||||||
prefix: sequence?.prefix ?? 'QT',
|
prefix: sequence?.prefix ?? 'QT',
|
||||||
period: sequence?.period ?? '',
|
period: sequence?.period ?? '',
|
||||||
branchId: sequence?.branchId ?? '',
|
branchId: sequence?.branchId ?? '__all__',
|
||||||
currentNumber: String(sequence?.currentNumber ?? 0),
|
currentNumber: String(sequence?.currentNumber ?? 0),
|
||||||
paddingLength: String(sequence?.paddingLength ?? 3),
|
paddingLength: String(sequence?.paddingLength ?? 3),
|
||||||
isActive: sequence?.isActive ?? true
|
isActive: sequence?.isActive ?? true
|
||||||
@@ -53,7 +74,7 @@ function buildPayload(state: SequenceState): DocumentSequenceMutationPayload {
|
|||||||
documentType: state.documentType,
|
documentType: state.documentType,
|
||||||
prefix: state.prefix,
|
prefix: state.prefix,
|
||||||
period: state.period,
|
period: state.period,
|
||||||
branchId: state.branchId || null,
|
branchId: state.branchId === '__all__' ? null : state.branchId,
|
||||||
currentNumber: Number(state.currentNumber || 0),
|
currentNumber: Number(state.currentNumber || 0),
|
||||||
paddingLength: Number(state.paddingLength || 3),
|
paddingLength: Number(state.paddingLength || 3),
|
||||||
isActive: state.isActive
|
isActive: state.isActive
|
||||||
@@ -63,11 +84,13 @@ function buildPayload(state: SequenceState): DocumentSequenceMutationPayload {
|
|||||||
function SequenceDialog({
|
function SequenceDialog({
|
||||||
open,
|
open,
|
||||||
onOpenChange,
|
onOpenChange,
|
||||||
sequence
|
sequence,
|
||||||
|
branches
|
||||||
}: {
|
}: {
|
||||||
open: boolean;
|
open: boolean;
|
||||||
onOpenChange: (open: boolean) => void;
|
onOpenChange: (open: boolean) => void;
|
||||||
sequence?: DocumentSequenceListItem;
|
sequence?: DocumentSequenceListItem;
|
||||||
|
branches: DocumentSequenceBranchOption[];
|
||||||
}) {
|
}) {
|
||||||
const [state, setState] = React.useState<SequenceState>(toState(sequence));
|
const [state, setState] = React.useState<SequenceState>(toState(sequence));
|
||||||
const createMutation = useMutation({
|
const createMutation = useMutation({
|
||||||
@@ -76,7 +99,9 @@ function SequenceDialog({
|
|||||||
toast.success('Sequence created');
|
toast.success('Sequence created');
|
||||||
onOpenChange(false);
|
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({
|
const updateMutation = useMutation({
|
||||||
...updateDocumentSequenceMutation,
|
...updateDocumentSequenceMutation,
|
||||||
@@ -84,7 +109,9 @@ function SequenceDialog({
|
|||||||
toast.success('Sequence updated');
|
toast.success('Sequence updated');
|
||||||
onOpenChange(false);
|
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(() => {
|
React.useEffect(() => {
|
||||||
@@ -120,38 +147,57 @@ function SequenceDialog({
|
|||||||
<Input
|
<Input
|
||||||
placeholder='Document type'
|
placeholder='Document type'
|
||||||
value={state.documentType}
|
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'>
|
<div className='grid gap-4 md:grid-cols-2'>
|
||||||
<Input
|
<Input
|
||||||
placeholder='Prefix'
|
placeholder='Prefix'
|
||||||
value={state.prefix}
|
value={state.prefix}
|
||||||
onChange={(e) => setState((current) => ({ ...current, prefix: e.target.value }))}
|
onChange={(event) =>
|
||||||
|
setState((current) => ({ ...current, prefix: event.target.value }))
|
||||||
|
}
|
||||||
/>
|
/>
|
||||||
<Input
|
<Input
|
||||||
placeholder='Period'
|
placeholder='Period'
|
||||||
value={state.period}
|
value={state.period}
|
||||||
onChange={(e) => setState((current) => ({ ...current, period: e.target.value }))}
|
onChange={(event) =>
|
||||||
|
setState((current) => ({ ...current, period: event.target.value }))
|
||||||
|
}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
<div className='grid gap-4 md:grid-cols-3'>
|
<div className='grid gap-4 md:grid-cols-3'>
|
||||||
<Input
|
<Select
|
||||||
placeholder='Branch ID (optional)'
|
|
||||||
value={state.branchId}
|
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
|
<Input
|
||||||
placeholder='Current number'
|
placeholder='Current number'
|
||||||
value={state.currentNumber}
|
value={state.currentNumber}
|
||||||
onChange={(e) =>
|
onChange={(event) =>
|
||||||
setState((current) => ({ ...current, currentNumber: e.target.value }))
|
setState((current) => ({ ...current, currentNumber: event.target.value }))
|
||||||
}
|
}
|
||||||
/>
|
/>
|
||||||
<Input
|
<Input
|
||||||
placeholder='Padding length'
|
placeholder='Padding length'
|
||||||
value={state.paddingLength}
|
value={state.paddingLength}
|
||||||
onChange={(e) =>
|
onChange={(event) =>
|
||||||
setState((current) => ({ ...current, paddingLength: e.target.value }))
|
setState((current) => ({ ...current, paddingLength: event.target.value }))
|
||||||
}
|
}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
@@ -183,7 +229,7 @@ function SequenceDialog({
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
export function DocumentSequenceSettings() {
|
export function DocumentSequenceSettings({ branches }: { branches: FoundationBranch[] }) {
|
||||||
const { data } = useSuspenseQuery(documentSequencesQueryOptions());
|
const { data } = useSuspenseQuery(documentSequencesQueryOptions());
|
||||||
const [dialogState, setDialogState] = React.useState<{
|
const [dialogState, setDialogState] = React.useState<{
|
||||||
open: boolean;
|
open: boolean;
|
||||||
@@ -191,15 +237,25 @@ export function DocumentSequenceSettings() {
|
|||||||
}>({
|
}>({
|
||||||
open: false
|
open: false
|
||||||
});
|
});
|
||||||
|
const branchOptions = React.useMemo(() => toBranchOptions(branches), [branches]);
|
||||||
|
|
||||||
const resetMutation = useMutation({
|
const resetMutation = useMutation({
|
||||||
...resetDocumentSequenceMutation,
|
...resetDocumentSequenceMutation,
|
||||||
onSuccess: () => toast.success('Sequence reset'),
|
onSuccess: () => {
|
||||||
onError: (error) => toast.error(error instanceof Error ? error.message : 'Reset failed')
|
toast.success('Sequence reset');
|
||||||
|
},
|
||||||
|
onError: (error) => {
|
||||||
|
toast.error(error instanceof Error ? error.message : 'Reset failed');
|
||||||
|
}
|
||||||
});
|
});
|
||||||
const deleteMutation = useMutation({
|
const deleteMutation = useMutation({
|
||||||
...deleteDocumentSequenceMutation,
|
...deleteDocumentSequenceMutation,
|
||||||
onSuccess: () => toast.success('Sequence deleted'),
|
onSuccess: () => {
|
||||||
onError: (error) => toast.error(error instanceof Error ? error.message : 'Delete failed')
|
toast.success('Sequence deleted');
|
||||||
|
},
|
||||||
|
onError: (error) => {
|
||||||
|
toast.error(error instanceof Error ? error.message : 'Delete failed');
|
||||||
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
return (
|
return (
|
||||||
@@ -229,8 +285,16 @@ export function DocumentSequenceSettings() {
|
|||||||
<div>
|
<div>
|
||||||
<div className='text-xl font-semibold'>{sequence.documentType}</div>
|
<div className='text-xl font-semibold'>{sequence.documentType}</div>
|
||||||
<div className='text-muted-foreground text-sm'>
|
<div className='text-muted-foreground text-sm'>
|
||||||
{sequence.prefix}
|
{sequence.prefix} {sequence.period} {'\u2022'}{' '}
|
||||||
{sequence.period} • branch {sequence.branchId || 'default'}
|
<span
|
||||||
|
className={
|
||||||
|
sequence.branchName || !sequence.branchId
|
||||||
|
? undefined
|
||||||
|
: 'text-muted-foreground italic'
|
||||||
|
}
|
||||||
|
>
|
||||||
|
{sequence.branchDisplayName ?? 'All Branches'}
|
||||||
|
</span>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div className='flex gap-2'>
|
<div className='flex gap-2'>
|
||||||
@@ -240,7 +304,14 @@ export function DocumentSequenceSettings() {
|
|||||||
<Badge variant='outline'>Next {sequence.nextPreview}</Badge>
|
<Badge variant='outline'>Next {sequence.nextPreview}</Badge>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className='mt-4 grid gap-4 md:grid-cols-4'>
|
<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='rounded-lg bg-muted/40 p-4'>
|
||||||
<div className='text-muted-foreground text-xs'>Current Number</div>
|
<div className='text-muted-foreground text-xs'>Current Number</div>
|
||||||
<div className='mt-1 text-sm font-medium'>{sequence.currentNumber}</div>
|
<div className='mt-1 text-sm font-medium'>{sequence.currentNumber}</div>
|
||||||
@@ -251,17 +322,15 @@ export function DocumentSequenceSettings() {
|
|||||||
</div>
|
</div>
|
||||||
<div className='rounded-lg bg-muted/40 p-4'>
|
<div className='rounded-lg bg-muted/40 p-4'>
|
||||||
<div className='text-muted-foreground text-xs'>Updated At</div>
|
<div className='text-muted-foreground text-xs'>Updated At</div>
|
||||||
<div className='mt-1 text-sm font-medium'>
|
<div className='mt-1 text-sm font-medium'>{formatDateTime(sequence.updatedAt)}</div>
|
||||||
{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>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className='mt-4 flex flex-wrap gap-2'>
|
<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
|
Edit Sequence
|
||||||
</Button>
|
</Button>
|
||||||
<Button
|
<Button
|
||||||
@@ -292,6 +361,7 @@ export function DocumentSequenceSettings() {
|
|||||||
open={dialogState.open}
|
open={dialogState.open}
|
||||||
onOpenChange={(open) => setDialogState((current) => ({ ...current, open }))}
|
onOpenChange={(open) => setDialogState((current) => ({ ...current, open }))}
|
||||||
sequence={dialogState.sequence}
|
sequence={dialogState.sequence}
|
||||||
|
branches={branchOptions}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -1,8 +1,9 @@
|
|||||||
import { and, asc, eq, sql } from 'drizzle-orm';
|
import { and, asc, eq, sql } from 'drizzle-orm';
|
||||||
import { documentSequences } from '@/db/schema';
|
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 { db } from '@/lib/db';
|
||||||
import { AuthError } from '@/lib/auth/session';
|
import { AuthError } from '@/lib/auth/session';
|
||||||
import { getCurrentOrganization } from '@/features/foundation/organization-context/service';
|
|
||||||
import type {
|
import type {
|
||||||
DocumentSequenceInput,
|
DocumentSequenceInput,
|
||||||
DocumentSequenceListItem,
|
DocumentSequenceListItem,
|
||||||
@@ -42,32 +43,24 @@ function normalizeBranchId(branchId?: string | null) {
|
|||||||
return branchId?.trim() || '';
|
return branchId?.trim() || '';
|
||||||
}
|
}
|
||||||
|
|
||||||
function mapSequenceRecord(row: typeof documentSequences.$inferSelect): DocumentSequenceRecord {
|
function formatBranchDisplayName(input: {
|
||||||
return {
|
branchId: string | null;
|
||||||
id: row.id,
|
branchCode?: string | null;
|
||||||
organizationId: row.organizationId,
|
branchName?: string | null;
|
||||||
branchId: row.branchId || null,
|
}) {
|
||||||
documentType: row.documentType,
|
if (!input.branchId) {
|
||||||
prefix: row.prefix,
|
return 'All Branches';
|
||||||
period: row.period,
|
}
|
||||||
currentNumber: row.currentNumber,
|
|
||||||
paddingLength: row.paddingLength,
|
|
||||||
isActive: row.isActive,
|
|
||||||
createdAt: row.createdAt.toISOString(),
|
|
||||||
updatedAt: row.updatedAt.toISOString()
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
function mapSequenceListItem(row: typeof documentSequences.$inferSelect): DocumentSequenceListItem {
|
if (input.branchCode && input.branchName) {
|
||||||
return {
|
return `${input.branchCode} - ${input.branchName}`;
|
||||||
...mapSequenceRecord(row),
|
}
|
||||||
nextPreview: buildDocumentCode(
|
|
||||||
row.prefix,
|
if (input.branchName) {
|
||||||
row.period,
|
return input.branchName;
|
||||||
row.currentNumber + 1,
|
}
|
||||||
row.paddingLength
|
|
||||||
)
|
return `Unknown Branch (${input.branchId})`;
|
||||||
};
|
|
||||||
}
|
}
|
||||||
|
|
||||||
async function resolveOrganizationId(organizationId?: string) {
|
async function resolveOrganizationId(organizationId?: string) {
|
||||||
@@ -76,31 +69,43 @@ async function resolveOrganizationId(organizationId?: string) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const organization = await getCurrentOrganization();
|
const organization = await getCurrentOrganization();
|
||||||
|
|
||||||
if (!organization) {
|
if (!organization) {
|
||||||
throw new Error('Active organization is required');
|
throw new AuthError('Active organization required', 400);
|
||||||
}
|
}
|
||||||
|
|
||||||
return organization.id;
|
return organization.id;
|
||||||
}
|
}
|
||||||
|
|
||||||
async function assertSequence(id: string, organizationId: string) {
|
function toSequenceResult(row: typeof documentSequences.$inferSelect): DocumentSequenceResult {
|
||||||
const [sequence] = await db
|
const nextNumber = row.currentNumber + 1;
|
||||||
.select()
|
|
||||||
.from(documentSequences)
|
|
||||||
.where(
|
|
||||||
and(eq(documentSequences.id, id), eq(documentSequences.organizationId, organizationId))
|
|
||||||
)
|
|
||||||
.limit(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);
|
throw new AuthError('Document sequence not found', 404);
|
||||||
}
|
}
|
||||||
|
|
||||||
return sequence;
|
return row;
|
||||||
}
|
}
|
||||||
|
|
||||||
async function ensureSequence(
|
async function ensureSequenceRow(
|
||||||
organizationId: string,
|
organizationId: string,
|
||||||
documentType: string,
|
documentType: string,
|
||||||
period: string,
|
period: string,
|
||||||
@@ -119,7 +124,8 @@ async function ensureSequence(
|
|||||||
return existing;
|
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
|
const [created] = await db
|
||||||
.insert(documentSequences)
|
.insert(documentSequences)
|
||||||
@@ -139,21 +145,45 @@ async function ensureSequence(
|
|||||||
return created;
|
return created;
|
||||||
}
|
}
|
||||||
|
|
||||||
function toSequenceResult(row: typeof documentSequences.$inferSelect): DocumentSequenceResult {
|
async function mapSequenceRecords(
|
||||||
const nextNumber = row.currentNumber + 1;
|
organizationId: string,
|
||||||
|
rows: Array<typeof documentSequences.$inferSelect>
|
||||||
|
) {
|
||||||
|
const branchDisplayMap = await resolveBranchDisplays({
|
||||||
|
organizationId,
|
||||||
|
branchIds: rows.map((row) => row.branchId || null)
|
||||||
|
});
|
||||||
|
|
||||||
|
return rows.map((row) => {
|
||||||
|
const branchId = row.branchId || null;
|
||||||
|
const branchDisplay = branchId ? (branchDisplayMap.get(branchId) ?? null) : null;
|
||||||
|
|
||||||
return {
|
return {
|
||||||
code: buildDocumentCode(row.prefix, row.period, nextNumber, row.paddingLength),
|
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,
|
documentType: row.documentType,
|
||||||
branchId: row.branchId || null,
|
prefix: row.prefix,
|
||||||
currentNumber: row.currentNumber,
|
|
||||||
nextNumber,
|
|
||||||
period: row.period,
|
period: row.period,
|
||||||
prefix: row.prefix
|
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
|
const rows = await db
|
||||||
.select()
|
.select()
|
||||||
.from(documentSequences)
|
.from(documentSequences)
|
||||||
@@ -164,12 +194,27 @@ export async function listDocumentSequences(organizationId: string): Promise<Doc
|
|||||||
asc(documentSequences.branchId)
|
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);
|
const row = await assertSequence(id, organizationId);
|
||||||
return mapSequenceRecord(row);
|
const [record] = await mapSequenceRecords(organizationId, [row]);
|
||||||
|
|
||||||
|
return record;
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function createDocumentSequence(
|
export async function createDocumentSequence(
|
||||||
@@ -193,7 +238,8 @@ export async function createDocumentSequence(
|
|||||||
})
|
})
|
||||||
.returning();
|
.returning();
|
||||||
|
|
||||||
return mapSequenceRecord(created);
|
const [record] = await mapSequenceRecords(organizationId, [created]);
|
||||||
|
return record;
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function updateDocumentSequence(
|
export async function updateDocumentSequence(
|
||||||
@@ -209,7 +255,9 @@ export async function updateDocumentSequence(
|
|||||||
prefix: payload.prefix?.trim() ?? current.prefix,
|
prefix: payload.prefix?.trim() ?? current.prefix,
|
||||||
period: payload.period?.trim() ?? current.period,
|
period: payload.period?.trim() ?? current.period,
|
||||||
branchId:
|
branchId:
|
||||||
payload.branchId === undefined ? current.branchId : normalizeBranchId(payload.branchId),
|
payload.branchId === undefined
|
||||||
|
? current.branchId
|
||||||
|
: normalizeBranchId(payload.branchId),
|
||||||
currentNumber: payload.currentNumber ?? current.currentNumber,
|
currentNumber: payload.currentNumber ?? current.currentNumber,
|
||||||
paddingLength: payload.paddingLength ?? current.paddingLength,
|
paddingLength: payload.paddingLength ?? current.paddingLength,
|
||||||
isActive: payload.isActive ?? current.isActive,
|
isActive: payload.isActive ?? current.isActive,
|
||||||
@@ -218,7 +266,8 @@ export async function updateDocumentSequence(
|
|||||||
.where(eq(documentSequences.id, id))
|
.where(eq(documentSequences.id, id))
|
||||||
.returning();
|
.returning();
|
||||||
|
|
||||||
return mapSequenceRecord(updated);
|
const [record] = await mapSequenceRecords(organizationId, [updated]);
|
||||||
|
return record;
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function resetDocumentSequence(
|
export async function resetDocumentSequence(
|
||||||
@@ -236,75 +285,40 @@ export async function resetDocumentSequence(
|
|||||||
.where(eq(documentSequences.id, id))
|
.where(eq(documentSequences.id, id))
|
||||||
.returning();
|
.returning();
|
||||||
|
|
||||||
return mapSequenceRecord(updated);
|
const [record] = await mapSequenceRecords(organizationId, [updated]);
|
||||||
|
return record;
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function deleteDocumentSequence(id: string, organizationId: string): Promise<DocumentSequenceRecord> {
|
export async function deleteDocumentSequence(id: string, organizationId: string) {
|
||||||
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> {
|
|
||||||
const sequence = await assertSequence(id, organizationId);
|
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(
|
export async function previewDocumentSequenceById(id: string, organizationId: string) {
|
||||||
input: DocumentSequenceInput
|
const row = await assertSequence(id, organizationId);
|
||||||
): Promise<DocumentSequenceResult> {
|
return toSequenceResult(row);
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function generateNextDocumentCode(input: DocumentSequenceInput) {
|
||||||
const organizationId = await resolveOrganizationId(input.organizationId);
|
const organizationId = await resolveOrganizationId(input.organizationId);
|
||||||
const period = input.period ?? getCurrentPeriod();
|
const period = input.period ?? getCurrentPeriod();
|
||||||
const branchId = normalizeBranchId(input.branchId);
|
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) => {
|
return db.transaction(async (tx) => {
|
||||||
await tx.execute(sql`
|
const sequence = await ensureSequenceRow(
|
||||||
select id
|
organizationId,
|
||||||
from document_sequences
|
input.documentType.trim(),
|
||||||
where organization_id = ${organizationId}
|
period,
|
||||||
and document_type = ${input.documentType}
|
branchId
|
||||||
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 [updated] = await tx
|
const [updated] = await tx
|
||||||
.update(documentSequences)
|
.update(documentSequences)
|
||||||
.set({
|
.set({
|
||||||
currentNumber: sequence.currentNumber + 1,
|
currentNumber: sql`${documentSequences.currentNumber} + 1`,
|
||||||
updatedAt: new Date()
|
updatedAt: new Date()
|
||||||
})
|
})
|
||||||
.where(eq(documentSequences.id, sequence.id))
|
.where(eq(documentSequences.id, sequence.id))
|
||||||
@@ -323,7 +337,6 @@ export async function generateNextDocumentCode(
|
|||||||
nextNumber: updated.currentNumber + 1,
|
nextNumber: updated.currentNumber + 1,
|
||||||
period: updated.period,
|
period: updated.period,
|
||||||
prefix: updated.prefix
|
prefix: updated.prefix
|
||||||
};
|
} satisfies DocumentSequenceResult;
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -9,6 +9,9 @@ export interface DocumentSequenceRecord {
|
|||||||
id: string;
|
id: string;
|
||||||
organizationId: string;
|
organizationId: string;
|
||||||
branchId: string | null;
|
branchId: string | null;
|
||||||
|
branchCode: string | null;
|
||||||
|
branchName: string | null;
|
||||||
|
branchDisplayName: string | null;
|
||||||
documentType: string;
|
documentType: string;
|
||||||
prefix: string;
|
prefix: string;
|
||||||
period: string;
|
period: string;
|
||||||
@@ -33,6 +36,13 @@ export interface DocumentSequenceListItem extends DocumentSequenceRecord {
|
|||||||
nextPreview: string;
|
nextPreview: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export interface DocumentSequenceBranchOption {
|
||||||
|
id: string;
|
||||||
|
code: string;
|
||||||
|
name: string;
|
||||||
|
displayName: string;
|
||||||
|
}
|
||||||
|
|
||||||
export interface DocumentSequenceListResponse {
|
export interface DocumentSequenceListResponse {
|
||||||
success: boolean;
|
success: boolean;
|
||||||
time: string;
|
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 (
|
return (
|
||||||
<DataTable table={table}>
|
<DataTable table={table}>
|
||||||
<div className='flex flex-wrap items-center justify-between gap-3'>
|
|
||||||
<DataTableToolbar table={table} />
|
<DataTableToolbar table={table} />
|
||||||
<Button
|
|
||||||
onClick={() => {
|
|
||||||
setEditingOption(undefined);
|
|
||||||
setIsFormOpen(true);
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
<Icons.add className='mr-2 h-4 w-4' />
|
|
||||||
Add Option
|
|
||||||
</Button>
|
|
||||||
</div>
|
|
||||||
<MasterOptionsFormDialog
|
<MasterOptionsFormDialog
|
||||||
open={isFormOpen}
|
open={isFormOpen}
|
||||||
onOpenChange={setIsFormOpen}
|
onOpenChange={setIsFormOpen}
|
||||||
|
|||||||
Reference in New Issue
Block a user