2026-02-24 20:44:16 +00:00
|
|
|
import { Button, Notification, toast } from '@/components/ui'
|
2026-05-23 21:12:01 +00:00
|
|
|
import { GridDto, UiCommandButtonPositionTypeEnum, WorkflowDto } from '@/proxy/form/models'
|
2026-02-24 20:44:16 +00:00
|
|
|
import { dynamicFetch } from '@/services/form.service'
|
|
|
|
|
import { useLocalization } from '@/utils/hooks/useLocalization'
|
|
|
|
|
import { usePermission } from '@/utils/hooks/usePermission'
|
|
|
|
|
import { DataGridTypes } from 'devextreme-react/data-grid'
|
|
|
|
|
import { ToolbarItem } from 'devextreme/ui/data_grid_types'
|
|
|
|
|
import { useEffect, useState } from 'react'
|
|
|
|
|
import { useDialogContext } from '../shared/DialogContext'
|
|
|
|
|
import { usePWA } from '@/utils/hooks/usePWA'
|
|
|
|
|
import { layoutTypes, ListViewLayoutType } from '../admin/listForm/edit/types'
|
|
|
|
|
import { useStoreState } from '@/store'
|
2026-05-23 21:12:01 +00:00
|
|
|
import { workflowService } from '@/services/workflow.service'
|
2026-02-24 20:44:16 +00:00
|
|
|
|
|
|
|
|
type ToolbarModalData = {
|
|
|
|
|
open: boolean
|
|
|
|
|
content?: JSX.Element
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// https://js.devexpress.com/Documentation/ApiReference/UI_Components/dxDataGrid/Configuration/toolbar/
|
|
|
|
|
// item.name > Accepted Values: 'addRowButton', 'applyFilterButton', 'columnChooserButton', 'exportButton', 'groupPanel', 'revertButton', 'saveButton', 'searchPanel'
|
|
|
|
|
const useToolbar = ({
|
|
|
|
|
gridDto,
|
|
|
|
|
listFormCode,
|
|
|
|
|
getSelectedRowKeys,
|
|
|
|
|
getSelectedRowsData,
|
|
|
|
|
refreshData,
|
|
|
|
|
getFilter,
|
|
|
|
|
layout,
|
|
|
|
|
expandAll,
|
|
|
|
|
collapseAll,
|
|
|
|
|
}: {
|
|
|
|
|
gridDto?: GridDto
|
|
|
|
|
listFormCode: string
|
2026-06-07 11:07:48 +00:00
|
|
|
getSelectedRowKeys: () => unknown[] | Promise<unknown[]>
|
2026-02-24 20:44:16 +00:00
|
|
|
getSelectedRowsData: () => any
|
|
|
|
|
refreshData: () => void
|
|
|
|
|
getFilter: () => void
|
|
|
|
|
layout: ListViewLayoutType | string
|
|
|
|
|
expandAll?: () => void
|
|
|
|
|
collapseAll?: () => void
|
|
|
|
|
}): {
|
|
|
|
|
toolbarData: ToolbarItem[]
|
|
|
|
|
toolbarModalData: ToolbarModalData | undefined
|
|
|
|
|
setToolbarModalData: (data: ToolbarModalData | undefined) => void
|
|
|
|
|
} => {
|
|
|
|
|
const dialog: any = useDialogContext()
|
|
|
|
|
const { translate } = useLocalization()
|
|
|
|
|
const { checkPermission } = usePermission()
|
|
|
|
|
const isPwaMode = usePWA()
|
2026-06-06 18:31:03 +00:00
|
|
|
const currentUser = useStoreState((state) => state.auth.user)
|
2026-02-24 20:44:16 +00:00
|
|
|
|
|
|
|
|
const [toolbarData, setToolbarData] = useState<ToolbarItem[]>([])
|
|
|
|
|
const [toolbarModalData, setToolbarModalData] = useState<ToolbarModalData>()
|
|
|
|
|
const grdOpt = gridDto?.gridOptions
|
|
|
|
|
|
|
|
|
|
function getToolbarData() {
|
|
|
|
|
const items: ToolbarItem[] = []
|
|
|
|
|
|
|
|
|
|
if (!gridDto || !grdOpt) {
|
|
|
|
|
setToolbarData(items)
|
|
|
|
|
return
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Add searchPanel
|
|
|
|
|
if (grdOpt.searchPanelDto?.visible) {
|
|
|
|
|
items.push({
|
|
|
|
|
locateInMenu: 'auto',
|
|
|
|
|
showText: 'inMenu',
|
|
|
|
|
name: 'searchPanel',
|
|
|
|
|
})
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Add InsertNewRecord button
|
|
|
|
|
if (grdOpt.editingOptionDto?.allowAdding && checkPermission(grdOpt.permissionDto?.c)) {
|
2026-03-13 18:04:39 +00:00
|
|
|
if (grdOpt.editingOptionDto?.addPageUrl) {
|
|
|
|
|
items.push({
|
|
|
|
|
widget: 'dxButton',
|
|
|
|
|
showText: 'always',
|
|
|
|
|
name: 'addRowButton',
|
|
|
|
|
location: 'after',
|
|
|
|
|
options: {
|
|
|
|
|
text: translate('::ListForms.ListForm.AddNewRecord'),
|
|
|
|
|
hint: translate('::ListForms.ListForm.AddNewRecord'),
|
|
|
|
|
onClick() {
|
|
|
|
|
window.open(grdOpt.editingOptionDto?.addPageUrl, isPwaMode ? '_self' : '_blank')
|
|
|
|
|
},
|
|
|
|
|
},
|
|
|
|
|
})
|
|
|
|
|
} else {
|
|
|
|
|
items.push({
|
|
|
|
|
locateInMenu: 'auto',
|
|
|
|
|
showText: 'always',
|
|
|
|
|
name: 'addRowButton',
|
|
|
|
|
location: 'after',
|
|
|
|
|
options: {
|
|
|
|
|
text: translate('::ListForms.ListForm.AddNewRecord'),
|
|
|
|
|
hint: translate('::ListForms.ListForm.AddNewRecord'),
|
|
|
|
|
},
|
|
|
|
|
})
|
|
|
|
|
}
|
2026-02-24 20:44:16 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
items.push({
|
|
|
|
|
widget: 'dxButton',
|
|
|
|
|
name: 'refreshButton',
|
|
|
|
|
options: {
|
|
|
|
|
icon: 'refresh',
|
|
|
|
|
onClick: refreshData,
|
|
|
|
|
text: translate('::ListForms.ListForm.Refresh'),
|
|
|
|
|
},
|
|
|
|
|
location: 'after',
|
|
|
|
|
})
|
|
|
|
|
|
2026-05-23 21:12:01 +00:00
|
|
|
const workflowOptions = grdOpt.workflowDto
|
2026-06-07 11:07:48 +00:00
|
|
|
const approvalCriteria =
|
|
|
|
|
workflowOptions?.criteria?.filter((item) => item.kind === 'Approval') ?? []
|
2026-05-23 21:12:01 +00:00
|
|
|
if (
|
|
|
|
|
workflowOptions?.approvalStatusFieldName &&
|
|
|
|
|
approvalCriteria.length > 0 &&
|
|
|
|
|
grdOpt.updateServiceAddress
|
|
|
|
|
) {
|
|
|
|
|
items.push({
|
|
|
|
|
widget: 'dxButton',
|
|
|
|
|
name: 'workflowStart',
|
|
|
|
|
location: 'after',
|
|
|
|
|
options: {
|
|
|
|
|
icon: 'play',
|
|
|
|
|
text: 'Workflow Start',
|
|
|
|
|
hint: 'Workflow Start',
|
|
|
|
|
visible: true,
|
|
|
|
|
disabled: true,
|
|
|
|
|
onClick: async () => {
|
|
|
|
|
const keys = (await Promise.resolve(getSelectedRowKeys() as any)) as unknown[]
|
|
|
|
|
if (!keys?.length) {
|
|
|
|
|
toast.push(
|
|
|
|
|
<Notification type="warning" duration={2000}>
|
|
|
|
|
{translate('::ListForms.ListForm.SelectRecord')}
|
|
|
|
|
</Notification>,
|
|
|
|
|
{ placement: 'top-end' },
|
|
|
|
|
)
|
|
|
|
|
return
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
const selectedRows = ((await Promise.resolve(getSelectedRowsData() as any)) ||
|
|
|
|
|
[]) as Record<string, unknown>[]
|
|
|
|
|
if (
|
|
|
|
|
selectedRows.length === 0 ||
|
|
|
|
|
!selectedRows.every((row) => isWorkflowNotStarted(row, workflowOptions))
|
|
|
|
|
) {
|
|
|
|
|
toast.push(
|
|
|
|
|
<Notification type="warning" duration={2500}>
|
2026-06-07 11:07:48 +00:00
|
|
|
{translate('::WorkflowAlreadyStarted')}
|
2026-05-23 21:12:01 +00:00
|
|
|
</Notification>,
|
|
|
|
|
{ placement: 'top-end' },
|
|
|
|
|
)
|
|
|
|
|
return
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
try {
|
|
|
|
|
await workflowService.startWorkflow(listFormCode, keys)
|
|
|
|
|
refreshData()
|
|
|
|
|
} catch (error: any) {
|
|
|
|
|
toast.push(
|
|
|
|
|
<Notification type="danger" duration={3000}>
|
|
|
|
|
{error?.response?.data?.error?.message ||
|
|
|
|
|
error?.response?.data?.message ||
|
|
|
|
|
error?.message ||
|
|
|
|
|
'Workflow baslatilamadi.'}
|
|
|
|
|
</Notification>,
|
|
|
|
|
{ placement: 'top-end' },
|
|
|
|
|
)
|
|
|
|
|
}
|
|
|
|
|
},
|
|
|
|
|
},
|
|
|
|
|
})
|
|
|
|
|
|
|
|
|
|
approvalCriteria.forEach((criteria) => {
|
|
|
|
|
items.push({
|
|
|
|
|
widget: 'dxButton',
|
|
|
|
|
name: `workflowApproval_${criteria.id}`,
|
|
|
|
|
location: 'after',
|
|
|
|
|
options: {
|
|
|
|
|
icon: 'check',
|
|
|
|
|
text: criteria.title,
|
|
|
|
|
hint: criteria.title,
|
|
|
|
|
visible: true,
|
|
|
|
|
disabled: true,
|
|
|
|
|
onClick: async () => {
|
|
|
|
|
const keys = (await Promise.resolve(getSelectedRowKeys() as any)) as unknown[]
|
|
|
|
|
if (!keys?.length) {
|
|
|
|
|
toast.push(
|
|
|
|
|
<Notification type="warning" duration={2000}>
|
|
|
|
|
{translate('::ListForms.ListForm.SelectRecord')}
|
|
|
|
|
</Notification>,
|
|
|
|
|
{ placement: 'top-end' },
|
|
|
|
|
)
|
|
|
|
|
return
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
const selectedRows = ((await Promise.resolve(getSelectedRowsData() as any)) ||
|
|
|
|
|
[]) as Record<string, unknown>[]
|
|
|
|
|
const activeRows = selectedRows.filter((row) =>
|
|
|
|
|
isWorkflowApprovalCriteriaActive(
|
|
|
|
|
row,
|
|
|
|
|
workflowOptions,
|
|
|
|
|
criteria.title,
|
2026-06-06 18:31:03 +00:00
|
|
|
getCurrentUserWorkflowIdentities(currentUser),
|
2026-05-23 21:12:01 +00:00
|
|
|
),
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
if (activeRows.length !== selectedRows.length) {
|
|
|
|
|
toast.push(
|
|
|
|
|
<Notification type="warning" duration={2500}>
|
2026-06-07 11:07:48 +00:00
|
|
|
{translate('::SeciliKayitBekliyor')}
|
2026-05-23 21:12:01 +00:00
|
|
|
</Notification>,
|
|
|
|
|
{ placement: 'top-end' },
|
|
|
|
|
)
|
|
|
|
|
return
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
setToolbarModalData({
|
|
|
|
|
open: true,
|
|
|
|
|
content: (
|
|
|
|
|
<>
|
|
|
|
|
<WorkflowApprovalDecisionDialog
|
|
|
|
|
criteriaTitle={criteria.title}
|
|
|
|
|
keys={keys}
|
|
|
|
|
listFormCode={listFormCode}
|
|
|
|
|
criteriaId={criteria.id}
|
|
|
|
|
onCancel={() => setToolbarModalData(undefined)}
|
|
|
|
|
onCompleted={() => {
|
|
|
|
|
refreshData()
|
|
|
|
|
setToolbarModalData(undefined)
|
|
|
|
|
}}
|
|
|
|
|
/>
|
|
|
|
|
</>
|
|
|
|
|
),
|
|
|
|
|
})
|
|
|
|
|
},
|
|
|
|
|
},
|
|
|
|
|
})
|
|
|
|
|
})
|
|
|
|
|
}
|
|
|
|
|
|
2026-02-24 20:44:16 +00:00
|
|
|
// Add Expand All button for TreeList
|
|
|
|
|
if (layout === layoutTypes.tree && grdOpt.treeOptionDto?.parentIdExpr) {
|
|
|
|
|
items.push({
|
|
|
|
|
widget: 'dxButton',
|
|
|
|
|
name: 'expandAllButton',
|
|
|
|
|
options: {
|
|
|
|
|
icon: 'plus',
|
|
|
|
|
text: translate('::ListForms.ListFormEdit.ExpandAll'),
|
|
|
|
|
onClick: expandAll,
|
|
|
|
|
},
|
|
|
|
|
location: 'after',
|
|
|
|
|
})
|
|
|
|
|
|
|
|
|
|
// Add Collapse All button for TreeList
|
|
|
|
|
items.push({
|
|
|
|
|
widget: 'dxButton',
|
|
|
|
|
name: 'collapseAllButton',
|
|
|
|
|
options: {
|
|
|
|
|
icon: 'minus',
|
|
|
|
|
text: translate('::ListForms.ListFormEdit.CollapseAll'),
|
|
|
|
|
onClick: collapseAll,
|
|
|
|
|
},
|
|
|
|
|
location: 'after',
|
|
|
|
|
})
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// field chooser panel
|
|
|
|
|
if (grdOpt.columnOptionDto?.columnChooserEnabled) {
|
|
|
|
|
items.push({
|
|
|
|
|
locateInMenu: 'auto',
|
|
|
|
|
showText: 'inMenu',
|
|
|
|
|
name: 'columnChooserButton',
|
|
|
|
|
options: {
|
|
|
|
|
hint: translate('::ListForms.ListForm.ColumnChooser'),
|
|
|
|
|
},
|
|
|
|
|
})
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Add group panel
|
|
|
|
|
if (grdOpt.groupPanelDto?.visible) {
|
|
|
|
|
items.push({
|
|
|
|
|
locateInMenu: 'auto',
|
|
|
|
|
showText: 'inMenu',
|
|
|
|
|
name: 'groupPanel',
|
|
|
|
|
})
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Add DeleteSelectedRecords button
|
|
|
|
|
// coklu silme icin
|
|
|
|
|
if (grdOpt.editingOptionDto?.allowDeleting && checkPermission(grdOpt.permissionDto?.d)) {
|
|
|
|
|
items.push({
|
|
|
|
|
location: 'after',
|
|
|
|
|
widget: 'dxButton',
|
|
|
|
|
locateInMenu: 'auto',
|
|
|
|
|
showText: 'inMenu',
|
|
|
|
|
name: 'deleteSelectedRecords',
|
|
|
|
|
options: {
|
|
|
|
|
text: translate('::ListForms.ListForm.DeleteSelectedRecords'),
|
|
|
|
|
icon: 'trash',
|
|
|
|
|
visible: false,
|
2026-06-07 11:07:48 +00:00
|
|
|
async onClick() {
|
2026-02-24 20:44:16 +00:00
|
|
|
if (!grdOpt.deleteServiceAddress) {
|
|
|
|
|
return
|
|
|
|
|
}
|
|
|
|
|
|
2026-06-07 11:07:48 +00:00
|
|
|
const selectedKeys = await Promise.resolve(getSelectedRowKeys())
|
|
|
|
|
const keys = Array.isArray(selectedKeys) ? [...selectedKeys] : []
|
|
|
|
|
|
|
|
|
|
if (!keys.length) {
|
|
|
|
|
toast.push(
|
|
|
|
|
<Notification type="warning" duration={2000}>
|
|
|
|
|
{translate('::ListForms.ListForm.SelectRecord')}
|
|
|
|
|
</Notification>,
|
|
|
|
|
{ placement: 'top-end' },
|
|
|
|
|
)
|
|
|
|
|
return
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
setToolbarModalData({
|
|
|
|
|
open: true,
|
|
|
|
|
content: (
|
|
|
|
|
<>
|
|
|
|
|
<h5 className="mb-4">
|
|
|
|
|
{translate('::ListForms.ListForm.DeleteSelectedRecords')}
|
|
|
|
|
</h5>
|
|
|
|
|
|
|
|
|
|
<p>
|
|
|
|
|
{translate('::SeciliKayitlarSilmekIstiyormusunuz', {
|
|
|
|
|
0: keys.length,
|
|
|
|
|
})}
|
|
|
|
|
</p>
|
|
|
|
|
|
|
|
|
|
<div className="text-right mt-6">
|
|
|
|
|
<Button
|
|
|
|
|
className="ltr:mr-2 rtl:ml-2"
|
|
|
|
|
variant="plain"
|
|
|
|
|
onClick={() => setToolbarModalData(undefined)}
|
|
|
|
|
>
|
|
|
|
|
{translate('::Cancel')}
|
|
|
|
|
</Button>
|
|
|
|
|
<Button
|
|
|
|
|
variant="solid"
|
|
|
|
|
onClick={() => {
|
|
|
|
|
dynamicFetch(grdOpt.deleteServiceAddress!, 'POST', null, {
|
|
|
|
|
keys,
|
|
|
|
|
listFormCode,
|
|
|
|
|
})
|
|
|
|
|
.then(() => {
|
|
|
|
|
refreshData()
|
|
|
|
|
setToolbarModalData(undefined)
|
|
|
|
|
})
|
|
|
|
|
.catch((error: any) => {
|
|
|
|
|
toast.push(
|
|
|
|
|
<Notification type="danger" duration={3000}>
|
|
|
|
|
{error?.response?.data?.error?.message ||
|
|
|
|
|
error?.response?.data?.message ||
|
|
|
|
|
error?.message ||
|
|
|
|
|
translate('::SilmeIslemiBasarisiz')}
|
|
|
|
|
</Notification>,
|
|
|
|
|
{ placement: 'top-end' },
|
|
|
|
|
)
|
|
|
|
|
})
|
|
|
|
|
}}
|
|
|
|
|
>
|
|
|
|
|
{translate('::Delete')}
|
|
|
|
|
</Button>
|
|
|
|
|
</div>
|
|
|
|
|
</>
|
|
|
|
|
),
|
2026-02-24 20:44:16 +00:00
|
|
|
})
|
|
|
|
|
},
|
|
|
|
|
},
|
|
|
|
|
})
|
|
|
|
|
|
|
|
|
|
// Add DeleteAllRecords button
|
|
|
|
|
// butun kayitlari (filtreli) icin
|
|
|
|
|
if (grdOpt.editingOptionDto?.allowAllDeleting) {
|
|
|
|
|
const buttonDeleteAll: DataGridTypes.ToolbarItem = {
|
|
|
|
|
location: 'after',
|
|
|
|
|
widget: 'dxButton',
|
|
|
|
|
name: 'deleteAllRecords',
|
|
|
|
|
options: {
|
|
|
|
|
text: translate('::ListForms.ListForm.DeleteAllRecords'),
|
|
|
|
|
hint: translate('::ListForms.ListForm.DeleteAllRecords'),
|
|
|
|
|
icon: 'trash',
|
|
|
|
|
visible: true,
|
|
|
|
|
onClick() {
|
|
|
|
|
const parameters = {
|
|
|
|
|
listFormCode,
|
|
|
|
|
filter: JSON.stringify(getFilter()),
|
|
|
|
|
onlyTotalCountQuery: true,
|
|
|
|
|
createDeleteQuery: false,
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
dynamicFetch('list-form-select/select', 'GET', parameters).then((r: any) => {
|
|
|
|
|
setToolbarModalData({
|
|
|
|
|
open: true,
|
|
|
|
|
content: (
|
|
|
|
|
<>
|
2026-06-07 11:07:48 +00:00
|
|
|
<h5 className="mb-4">{translate('::ListForms.ListForm.DeleteAllRecords')}</h5>
|
2026-02-24 20:44:16 +00:00
|
|
|
|
2026-06-07 11:07:48 +00:00
|
|
|
<p>
|
|
|
|
|
{translate('::TumKayitlariSilmekIstiyormusunuz', {
|
|
|
|
|
0: r.data.totalCount,
|
|
|
|
|
})}
|
|
|
|
|
</p>
|
2026-02-24 20:44:16 +00:00
|
|
|
|
|
|
|
|
<div className="text-right mt-6">
|
|
|
|
|
<Button
|
|
|
|
|
className="ltr:mr-2 rtl:ml-2"
|
|
|
|
|
variant="plain"
|
|
|
|
|
onClick={() => setToolbarModalData(undefined)}
|
|
|
|
|
>
|
|
|
|
|
Cancel
|
|
|
|
|
</Button>
|
|
|
|
|
<Button
|
|
|
|
|
variant="solid"
|
|
|
|
|
onClick={() => {
|
|
|
|
|
//delete parameters.onlyTotalCountQuery
|
|
|
|
|
parameters.createDeleteQuery = true // tumunu silme islemi parametresi > set
|
|
|
|
|
dynamicFetch('list-form-select/select', 'GET', parameters).then(() => {
|
|
|
|
|
toast.push(
|
|
|
|
|
<Notification type="success" duration={2000}>
|
2026-06-07 11:07:48 +00:00
|
|
|
{translate('::TumKayitlarSilindi')}
|
2026-02-24 20:44:16 +00:00
|
|
|
</Notification>,
|
|
|
|
|
{
|
|
|
|
|
placement: 'top-end',
|
|
|
|
|
},
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
refreshData()
|
|
|
|
|
|
|
|
|
|
setToolbarModalData(undefined)
|
|
|
|
|
})
|
|
|
|
|
}}
|
|
|
|
|
>
|
|
|
|
|
Save
|
|
|
|
|
</Button>
|
|
|
|
|
</div>
|
|
|
|
|
</>
|
|
|
|
|
),
|
|
|
|
|
})
|
|
|
|
|
})
|
|
|
|
|
},
|
|
|
|
|
},
|
|
|
|
|
}
|
|
|
|
|
items.push(buttonDeleteAll)
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// #region Toolbar icin kullanici tanimli dinamik butonlari ekler
|
|
|
|
|
for (let i = 0; i < grdOpt.commandColumnDto.length; i++) {
|
|
|
|
|
const action = grdOpt.commandColumnDto[i]
|
|
|
|
|
|
|
|
|
|
// action.buttonPosition == 1 ise Toolbar butonudur, burada sadece Toolbar butonunu eklenir
|
|
|
|
|
if (action.buttonPosition !== UiCommandButtonPositionTypeEnum.Toolbar) {
|
|
|
|
|
continue
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if (checkPermission(action.authName)) {
|
|
|
|
|
const buttonCustom: DataGridTypes.ToolbarItem = {
|
|
|
|
|
location: 'after',
|
|
|
|
|
widget: 'dxButton',
|
|
|
|
|
name: action.hint,
|
|
|
|
|
options: {
|
2026-03-03 06:15:23 +00:00
|
|
|
hint: translate('::' + action.hint),
|
|
|
|
|
text: translate('::' + action.text),
|
2026-02-24 20:44:16 +00:00
|
|
|
icon: action.icon,
|
|
|
|
|
visible: true,
|
|
|
|
|
onClick(e: any) {
|
|
|
|
|
if (typeof e.event?.preventDefault === 'function') {
|
|
|
|
|
e?.event?.preventDefault()
|
|
|
|
|
}
|
|
|
|
|
if (action.url) {
|
|
|
|
|
let url = action.url
|
|
|
|
|
// griddeki secili satirlari al
|
|
|
|
|
const selectedRowsData = getSelectedRowsData()
|
|
|
|
|
// constsa her bir secili satir icin donguye gir
|
|
|
|
|
for (let i = 0; i < selectedRowsData.length; i++) {
|
|
|
|
|
// secili satirin objesine ait property verilerini al
|
|
|
|
|
const keys = Object.keys(selectedRowsData[i])
|
|
|
|
|
// secili satirin her bir property si icin donguye gir
|
|
|
|
|
for (let j = 0; j < keys.length; j++) {
|
|
|
|
|
// secili satirin j indexine sahip property ismini al
|
|
|
|
|
const fieldName = keys[j]
|
|
|
|
|
// secili satirin j indexine sahip property isminin degerini al
|
|
|
|
|
const fieldValue = selectedRowsData[i][fieldName]
|
|
|
|
|
// url icerisindeki {PropertyName} seklindeki anahtarlari secili satirdaki uyusan propertyler ile degistir
|
|
|
|
|
url = url.replace(`@${fieldName}`, fieldValue)
|
|
|
|
|
}
|
|
|
|
|
break // Url cagirmak icin kullanilacak parametreler sadece secili olan ilk satirdan alinir!
|
|
|
|
|
}
|
|
|
|
|
window.open(url, isPwaMode ? '_self' : action.urlTarget)
|
|
|
|
|
} else if (action.dialogName) {
|
|
|
|
|
if (action.dialogParameters) {
|
|
|
|
|
var dynamicMap = JSON.parse(action.dialogParameters)
|
|
|
|
|
for (const [key, value] of Object.entries<string>(dynamicMap)) {
|
|
|
|
|
dynamicMap[key] = value.startsWith('@')
|
|
|
|
|
? e.row.data[value.replace('@', '')]
|
|
|
|
|
: value
|
|
|
|
|
}
|
|
|
|
|
dialog.setConfig({
|
|
|
|
|
component: action.dialogName,
|
|
|
|
|
props: dynamicMap,
|
|
|
|
|
})
|
|
|
|
|
}
|
|
|
|
|
} else if (action.onClick) {
|
|
|
|
|
eval(action.onClick)
|
|
|
|
|
}
|
|
|
|
|
},
|
|
|
|
|
},
|
|
|
|
|
}
|
|
|
|
|
items.push(buttonCustom)
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
// #endregion
|
|
|
|
|
|
|
|
|
|
// batch editing icin kaydet ve geri al butonu
|
|
|
|
|
if (
|
2026-04-25 18:12:31 +00:00
|
|
|
grdOpt.editingOptionDto?.allowUpdating &&
|
2026-02-24 20:44:16 +00:00
|
|
|
grdOpt.editingOptionDto?.mode == 'batch' &&
|
|
|
|
|
checkPermission(grdOpt.permissionDto?.u)
|
|
|
|
|
) {
|
|
|
|
|
items.push({
|
|
|
|
|
locateInMenu: 'auto',
|
|
|
|
|
showText: 'inMenu',
|
|
|
|
|
name: 'saveButton',
|
|
|
|
|
options: { hint: translate('::App.SaveChanges') },
|
|
|
|
|
})
|
|
|
|
|
|
|
|
|
|
items.push({
|
|
|
|
|
locateInMenu: 'auto',
|
|
|
|
|
showText: 'inMenu',
|
|
|
|
|
name: 'revertButton',
|
|
|
|
|
options: { hint: translate('::App.UndoChanges') },
|
|
|
|
|
})
|
|
|
|
|
}
|
|
|
|
|
// #endregion
|
|
|
|
|
|
|
|
|
|
setToolbarData(items)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
useEffect(() => {
|
|
|
|
|
if (!gridDto && !listFormCode) return
|
|
|
|
|
|
|
|
|
|
getToolbarData()
|
2026-06-06 18:31:03 +00:00
|
|
|
}, [gridDto, listFormCode, currentUser])
|
2026-02-24 20:44:16 +00:00
|
|
|
|
|
|
|
|
return {
|
|
|
|
|
toolbarData,
|
|
|
|
|
toolbarModalData,
|
|
|
|
|
setToolbarModalData,
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2026-05-23 21:12:01 +00:00
|
|
|
function isWorkflowApprovalCriteriaActive(
|
|
|
|
|
row: Record<string, unknown>,
|
|
|
|
|
workflowOptions: WorkflowDto,
|
|
|
|
|
criteriaTitle: string,
|
|
|
|
|
currentUserIdentities: string[] = [],
|
|
|
|
|
) {
|
|
|
|
|
if (!workflowOptions.approvalStatusFieldName || !criteriaTitle) {
|
|
|
|
|
return false
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
const statusMatches =
|
|
|
|
|
normalizeWorkflowValue(row?.[workflowOptions.approvalStatusFieldName]) ===
|
|
|
|
|
normalizeWorkflowValue(criteriaTitle)
|
|
|
|
|
|
|
|
|
|
if (!statusMatches) {
|
|
|
|
|
return false
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if (!workflowOptions.approvalUserFieldName) {
|
|
|
|
|
return true
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
const approver = normalizeWorkflowValue(row?.[workflowOptions.approvalUserFieldName])
|
|
|
|
|
return currentUserIdentities.some((identity) => normalizeWorkflowValue(identity) === approver)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
function normalizeWorkflowValue(value: unknown) {
|
2026-06-07 11:07:48 +00:00
|
|
|
return String(value ?? '')
|
|
|
|
|
.trim()
|
|
|
|
|
.toLocaleLowerCase('tr-TR')
|
2026-05-23 21:12:01 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
function isWorkflowNotStarted(row: Record<string, unknown>, workflowOptions: WorkflowDto) {
|
|
|
|
|
return normalizeWorkflowValue(row?.[workflowOptions.approvalStatusFieldName]) === ''
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
function getCurrentUserWorkflowIdentities(currentUser?: {
|
|
|
|
|
userName?: string
|
|
|
|
|
email?: string
|
|
|
|
|
name?: string
|
|
|
|
|
}) {
|
|
|
|
|
return [currentUser?.email, currentUser?.userName, currentUser?.name].filter(Boolean) as string[]
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
export function updateWorkflowApprovalToolbarItems(
|
|
|
|
|
component: any,
|
|
|
|
|
workflowOptions: WorkflowDto | undefined,
|
|
|
|
|
selectedRowsData: Record<string, unknown>[] = [],
|
|
|
|
|
currentUser?: {
|
|
|
|
|
userName?: string
|
|
|
|
|
email?: string
|
|
|
|
|
name?: string
|
|
|
|
|
},
|
|
|
|
|
) {
|
2026-06-07 11:07:48 +00:00
|
|
|
const approvalCriteria =
|
|
|
|
|
workflowOptions?.criteria?.filter((item) => item.kind === 'Approval') ?? []
|
2026-05-23 21:12:01 +00:00
|
|
|
if (!component || !workflowOptions?.approvalStatusFieldName || !approvalCriteria.length) {
|
|
|
|
|
return
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
const toolbarOptions = component.option('toolbar')
|
|
|
|
|
if (!toolbarOptions?.items || !Array.isArray(toolbarOptions.items)) {
|
|
|
|
|
return
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
const workflowStartItemIndex = toolbarOptions.items
|
|
|
|
|
.map((item: any) => item.name)
|
|
|
|
|
.indexOf('workflowStart')
|
|
|
|
|
|
|
|
|
|
if (workflowStartItemIndex >= 0) {
|
|
|
|
|
const startEnabled =
|
|
|
|
|
selectedRowsData.length > 0 &&
|
|
|
|
|
selectedRowsData.every((row) => isWorkflowNotStarted(row, workflowOptions))
|
|
|
|
|
const startOptionPath = `toolbar.items[${workflowStartItemIndex}].options.disabled`
|
|
|
|
|
const nextStartDisabled = !startEnabled
|
|
|
|
|
if (component.option(startOptionPath) !== nextStartDisabled) {
|
|
|
|
|
component.option(startOptionPath, nextStartDisabled)
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
const currentUserIdentities = getCurrentUserWorkflowIdentities(currentUser)
|
|
|
|
|
|
|
|
|
|
approvalCriteria.forEach((criteria) => {
|
|
|
|
|
const toolbarItemIndex = toolbarOptions.items
|
|
|
|
|
.map((item: any) => item.name)
|
|
|
|
|
.indexOf(`workflowApproval_${criteria.id}`)
|
|
|
|
|
|
|
|
|
|
if (toolbarItemIndex < 0) {
|
|
|
|
|
return
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
const enabled =
|
|
|
|
|
selectedRowsData.length > 0 &&
|
|
|
|
|
selectedRowsData.every((row) =>
|
2026-06-07 11:07:48 +00:00
|
|
|
isWorkflowApprovalCriteriaActive(
|
|
|
|
|
row,
|
|
|
|
|
workflowOptions,
|
|
|
|
|
criteria.title,
|
|
|
|
|
currentUserIdentities,
|
|
|
|
|
),
|
2026-05-23 21:12:01 +00:00
|
|
|
)
|
|
|
|
|
|
|
|
|
|
const optionPath = `toolbar.items[${toolbarItemIndex}].options.disabled`
|
|
|
|
|
const nextDisabled = !enabled
|
|
|
|
|
if (component.option(optionPath) !== nextDisabled) {
|
|
|
|
|
component.option(optionPath, nextDisabled)
|
|
|
|
|
}
|
|
|
|
|
})
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
function WorkflowApprovalDecisionDialog({
|
|
|
|
|
criteriaTitle,
|
|
|
|
|
keys,
|
|
|
|
|
listFormCode,
|
|
|
|
|
criteriaId,
|
|
|
|
|
onCancel,
|
|
|
|
|
onCompleted,
|
|
|
|
|
}: {
|
|
|
|
|
criteriaTitle: string
|
|
|
|
|
keys: unknown[]
|
|
|
|
|
listFormCode: string
|
|
|
|
|
criteriaId: string
|
|
|
|
|
onCancel: () => void
|
|
|
|
|
onCompleted: () => void
|
|
|
|
|
}) {
|
|
|
|
|
const { translate } = useLocalization()
|
|
|
|
|
const [note, setNote] = useState('')
|
|
|
|
|
const [submitting, setSubmitting] = useState(false)
|
|
|
|
|
|
|
|
|
|
const decide = async (approved: boolean) => {
|
|
|
|
|
setSubmitting(true)
|
|
|
|
|
try {
|
|
|
|
|
await Promise.all(
|
|
|
|
|
keys.map((key) =>
|
2026-06-07 11:07:48 +00:00
|
|
|
workflowService.decideWorkflow(listFormCode, [key], approved, note, criteriaId),
|
2026-05-23 21:12:01 +00:00
|
|
|
),
|
|
|
|
|
)
|
|
|
|
|
onCompleted()
|
|
|
|
|
} catch (error: any) {
|
|
|
|
|
toast.push(
|
|
|
|
|
<Notification type="danger" duration={3000}>
|
|
|
|
|
{error?.response?.data?.error?.message ||
|
|
|
|
|
error?.response?.data?.message ||
|
|
|
|
|
error?.message ||
|
|
|
|
|
'Workflow karari verilemedi.'}
|
|
|
|
|
</Notification>,
|
|
|
|
|
{ placement: 'top-end' },
|
|
|
|
|
)
|
|
|
|
|
} finally {
|
|
|
|
|
setSubmitting(false)
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
return (
|
|
|
|
|
<>
|
|
|
|
|
<h5 className="mb-4">{criteriaTitle}</h5>
|
|
|
|
|
<p>{keys.length} kayit icin workflow karari verilecek.</p>
|
|
|
|
|
<label className="mb-2 block font-semibold">Not</label>
|
|
|
|
|
<textarea
|
|
|
|
|
className="input input-textarea mb-4 min-h-[96px] w-full resize-y"
|
|
|
|
|
rows={4}
|
|
|
|
|
value={note}
|
2026-06-07 07:52:28 +00:00
|
|
|
autoFocus
|
2026-05-23 21:12:01 +00:00
|
|
|
placeholder="Onay veya red aciklamasi"
|
|
|
|
|
onChange={(event) => setNote(event.target.value)}
|
|
|
|
|
/>
|
|
|
|
|
<div className="text-right mt-6">
|
2026-06-07 11:07:48 +00:00
|
|
|
<Button
|
|
|
|
|
className="ltr:mr-2 rtl:ml-2"
|
|
|
|
|
variant="plain"
|
|
|
|
|
disabled={submitting}
|
|
|
|
|
onClick={onCancel}
|
|
|
|
|
>
|
2026-05-23 21:12:01 +00:00
|
|
|
{translate('::Cancel')}
|
|
|
|
|
</Button>
|
|
|
|
|
<Button className="ltr:mr-2 rtl:ml-2" disabled={submitting} onClick={() => decide(false)}>
|
2026-06-07 11:07:48 +00:00
|
|
|
{translate('::App.Listform.ListformField.Rejecter')}
|
2026-05-23 21:12:01 +00:00
|
|
|
</Button>
|
|
|
|
|
<Button variant="solid" disabled={submitting} onClick={() => decide(true)}>
|
2026-06-07 11:07:48 +00:00
|
|
|
{translate('::App.Listform.ListformField.Approver')}
|
2026-05-23 21:12:01 +00:00
|
|
|
</Button>
|
|
|
|
|
</div>
|
|
|
|
|
</>
|
|
|
|
|
)
|
|
|
|
|
}
|
|
|
|
|
|
2026-02-24 20:44:16 +00:00
|
|
|
export { useToolbar }
|