Ölü kodlar kaldırıldı. Optimizasyon
This commit is contained in:
parent
b13f0b6a57
commit
f744d18f83
42 changed files with 100 additions and 2182 deletions
File diff suppressed because one or more lines are too long
|
|
@ -1,62 +0,0 @@
|
||||||
import React from "react";
|
|
||||||
import classNames from "classnames";
|
|
||||||
|
|
||||||
interface InfoSectionProps {
|
|
||||||
title: string;
|
|
||||||
children: React.ReactNode;
|
|
||||||
}
|
|
||||||
|
|
||||||
export const InfoSection: React.FC<InfoSectionProps> = ({
|
|
||||||
title,
|
|
||||||
children,
|
|
||||||
}) => (
|
|
||||||
<div className="space-y-2">
|
|
||||||
<h3 className="text-sm font-medium text-gray-900 border-b pb-1 mb-2">
|
|
||||||
{title}
|
|
||||||
</h3>
|
|
||||||
<div className="space-y-2.5 text-sm">{children}</div>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
|
|
||||||
interface InfoItemProps {
|
|
||||||
label: string;
|
|
||||||
value: string | number;
|
|
||||||
isMono?: boolean;
|
|
||||||
isBold?: boolean;
|
|
||||||
isBadge?: boolean;
|
|
||||||
badgeColor?: string;
|
|
||||||
}
|
|
||||||
|
|
||||||
export const InfoItem: React.FC<InfoItemProps> = ({
|
|
||||||
label,
|
|
||||||
value,
|
|
||||||
isMono,
|
|
||||||
isBold,
|
|
||||||
isBadge,
|
|
||||||
badgeColor,
|
|
||||||
}) => (
|
|
||||||
<div>
|
|
||||||
<label className="text-xs font-medium text-gray-500">{label}</label>
|
|
||||||
{isBadge ? (
|
|
||||||
<p>
|
|
||||||
<span
|
|
||||||
className={classNames(
|
|
||||||
"px-2 py-0.5 rounded-full text-xs font-medium",
|
|
||||||
badgeColor || "bg-gray-100 text-gray-800"
|
|
||||||
)}
|
|
||||||
>
|
|
||||||
{value}
|
|
||||||
</span>
|
|
||||||
</p>
|
|
||||||
) : (
|
|
||||||
<p
|
|
||||||
className={classNames("text-gray-900", {
|
|
||||||
"font-mono": isMono,
|
|
||||||
"font-semibold": isBold,
|
|
||||||
})}
|
|
||||||
>
|
|
||||||
{value}
|
|
||||||
</p>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
|
|
@ -1,41 +0,0 @@
|
||||||
import React from 'react';
|
|
||||||
import classNames from 'classnames';
|
|
||||||
|
|
||||||
interface LoadingSpinnerProps {
|
|
||||||
size?: 'sm' | 'md' | 'lg';
|
|
||||||
color?: 'blue' | 'white' | 'gray';
|
|
||||||
text?: string;
|
|
||||||
}
|
|
||||||
|
|
||||||
const LoadingSpinner: React.FC<LoadingSpinnerProps> = ({
|
|
||||||
size = 'md',
|
|
||||||
color = 'blue',
|
|
||||||
text
|
|
||||||
}) => {
|
|
||||||
const sizeClasses = {
|
|
||||||
sm: 'h-4 w-4',
|
|
||||||
md: 'h-6 w-6',
|
|
||||||
lg: 'h-8 w-8'
|
|
||||||
};
|
|
||||||
|
|
||||||
const colorClasses = {
|
|
||||||
blue: 'border-blue-600',
|
|
||||||
white: 'border-white',
|
|
||||||
gray: 'border-gray-600'
|
|
||||||
};
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div className="flex items-center justify-center">
|
|
||||||
<div
|
|
||||||
className={classNames(
|
|
||||||
'animate-spin rounded-full border-b-2',
|
|
||||||
sizeClasses[size],
|
|
||||||
colorClasses[color]
|
|
||||||
)}
|
|
||||||
/>
|
|
||||||
{text && <span className="ml-3 text-gray-600">{text}</span>}
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
export default LoadingSpinner;
|
|
||||||
|
|
@ -1,64 +0,0 @@
|
||||||
import React from "react";
|
|
||||||
|
|
||||||
interface ModuleHeaderProps {
|
|
||||||
title: string;
|
|
||||||
breadcrumbs?: Array<{
|
|
||||||
name: string;
|
|
||||||
href?: string;
|
|
||||||
}>;
|
|
||||||
}
|
|
||||||
|
|
||||||
const ModuleHeader: React.FC<ModuleHeaderProps> = ({
|
|
||||||
title,
|
|
||||||
breadcrumbs = [],
|
|
||||||
}) => {
|
|
||||||
return (
|
|
||||||
<div className="bg-white border-b border-gray-200 px-4 py-3">
|
|
||||||
{/* Breadcrumb Navigation */}
|
|
||||||
<nav className="flex" aria-label="Breadcrumb">
|
|
||||||
<ol className="flex items-center space-x-1 text-sm">
|
|
||||||
<li>
|
|
||||||
<div className="flex items-center">
|
|
||||||
<svg
|
|
||||||
className="h-4 w-4 text-gray-400"
|
|
||||||
fill="none"
|
|
||||||
viewBox="0 0 24 24"
|
|
||||||
stroke="currentColor"
|
|
||||||
>
|
|
||||||
<path
|
|
||||||
strokeLinecap="round"
|
|
||||||
strokeLinejoin="round"
|
|
||||||
strokeWidth={2}
|
|
||||||
d="M3 12l2-2m0 0l7-7 7 7M5 10v10a1 1 0 001 1h3m10-11l2 2m-2-2v10a1 1 0 01-1 1h-3m-6 0a1 1 0 001-1v-4a1 1 0 011-1h2a1 1 0 011 1v4a1 1 0 001 1m-6 0h6"
|
|
||||||
/>
|
|
||||||
</svg>
|
|
||||||
<span className="ml-1 font-medium text-gray-500">Anasayfa</span>
|
|
||||||
</div>
|
|
||||||
</li>
|
|
||||||
|
|
||||||
{breadcrumbs.map((breadcrumb, index) => (
|
|
||||||
<li key={index}>
|
|
||||||
<div className="flex items-center">
|
|
||||||
<span className="text-gray-400 mx-2">/</span>
|
|
||||||
<span className="font-medium text-gray-500">
|
|
||||||
{breadcrumb.name}
|
|
||||||
</span>
|
|
||||||
</div>
|
|
||||||
</li>
|
|
||||||
))}
|
|
||||||
|
|
||||||
<li>
|
|
||||||
<div className="flex items-center">
|
|
||||||
<span className="text-gray-400 mx-2">/</span>
|
|
||||||
<span className="font-medium text-gray-900" aria-current="page">
|
|
||||||
{title}
|
|
||||||
</span>
|
|
||||||
</div>
|
|
||||||
</li>
|
|
||||||
</ol>
|
|
||||||
</nav>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
export default ModuleHeader;
|
|
||||||
|
|
@ -1,80 +0,0 @@
|
||||||
import React from 'react'
|
|
||||||
import classNames from 'classnames'
|
|
||||||
import { FaCheckCircle, FaExclamationTriangle, FaClock, FaTimes } from 'react-icons/fa'
|
|
||||||
|
|
||||||
interface StatusBadgeProps {
|
|
||||||
status: string
|
|
||||||
size?: 'sm' | 'md'
|
|
||||||
showIcon?: boolean
|
|
||||||
}
|
|
||||||
|
|
||||||
const StatusBadge: React.FC<StatusBadgeProps> = ({ status, size = 'md', showIcon = true }) => {
|
|
||||||
const sizeClasses = {
|
|
||||||
sm: 'px-1.5 py-0.5 text-xs',
|
|
||||||
md: 'px-2 py-0.5 text-xs',
|
|
||||||
}
|
|
||||||
|
|
||||||
const getStatusConfig = (status: string) => {
|
|
||||||
switch (status) {
|
|
||||||
case 'active':
|
|
||||||
return {
|
|
||||||
color: 'bg-green-100 text-green-800',
|
|
||||||
icon: <FaCheckCircle size={14} />,
|
|
||||||
text: 'Aktif',
|
|
||||||
}
|
|
||||||
case 'inactive':
|
|
||||||
return {
|
|
||||||
color: 'bg-red-100 text-red-800',
|
|
||||||
icon: <FaTimes size={14} />,
|
|
||||||
text: 'Pasif',
|
|
||||||
}
|
|
||||||
case 'pending':
|
|
||||||
return {
|
|
||||||
color: 'bg-yellow-100 text-yellow-800',
|
|
||||||
icon: <FaClock size={14} />,
|
|
||||||
text: 'Beklemede',
|
|
||||||
}
|
|
||||||
case 'critical':
|
|
||||||
return {
|
|
||||||
color: 'bg-red-100 text-red-800',
|
|
||||||
icon: <FaExclamationTriangle size={14} />,
|
|
||||||
text: 'Kritik',
|
|
||||||
}
|
|
||||||
case 'low':
|
|
||||||
return {
|
|
||||||
color: 'bg-yellow-100 text-yellow-800',
|
|
||||||
icon: <FaExclamationTriangle size={14} />,
|
|
||||||
text: 'App.ListFormTodoBoard.PriorityLow',
|
|
||||||
}
|
|
||||||
case 'normal':
|
|
||||||
return {
|
|
||||||
color: 'bg-green-100 text-green-800',
|
|
||||||
icon: <FaCheckCircle size={14} />,
|
|
||||||
text: 'Normal',
|
|
||||||
}
|
|
||||||
default:
|
|
||||||
return {
|
|
||||||
color: 'bg-gray-100 text-gray-800',
|
|
||||||
icon: <FaCheckCircle size={14} />,
|
|
||||||
text: 'Bilinmiyor',
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
const config = getStatusConfig(status)
|
|
||||||
|
|
||||||
return (
|
|
||||||
<span
|
|
||||||
className={classNames(
|
|
||||||
'inline-flex items-center rounded-full font-medium',
|
|
||||||
config.color,
|
|
||||||
sizeClasses[size],
|
|
||||||
)}
|
|
||||||
>
|
|
||||||
{showIcon && <span className="mr-1">{config.icon}</span>}
|
|
||||||
{config.text}
|
|
||||||
</span>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
export default StatusBadge
|
|
||||||
|
|
@ -1,44 +0,0 @@
|
||||||
import classNames from "classnames";
|
|
||||||
import Widget, { type colorType } from "./Widget";
|
|
||||||
import { WidgetEditDto, WidgetGroupDto } from "../../types/common";
|
|
||||||
|
|
||||||
export default function WidgetGroup({
|
|
||||||
widgetGroups,
|
|
||||||
}: {
|
|
||||||
widgetGroups: WidgetGroupDto[];
|
|
||||||
}) {
|
|
||||||
return (
|
|
||||||
<div>
|
|
||||||
{widgetGroups.map((group, gIdx) => (
|
|
||||||
<div
|
|
||||||
key={gIdx}
|
|
||||||
className={classNames(
|
|
||||||
`grid grid-cols-12 gap-${group.colGap} ${group.className || ""}`
|
|
||||||
)}
|
|
||||||
>
|
|
||||||
{group.items.map((item: WidgetEditDto, order: number) => (
|
|
||||||
<div
|
|
||||||
key={`${gIdx}-${order}`}
|
|
||||||
className={classNames(`col-span-${group.colSpan}`)}
|
|
||||||
>
|
|
||||||
<Widget
|
|
||||||
title={item.title}
|
|
||||||
value={item.value}
|
|
||||||
color={item.color as colorType}
|
|
||||||
icon={item.icon}
|
|
||||||
subTitle={item.subTitle}
|
|
||||||
valueClassName={item.valueClassName}
|
|
||||||
onClick={() => {
|
|
||||||
if (item.onClick) {
|
|
||||||
// eslint-disable-next-line no-eval
|
|
||||||
eval(item.onClick);
|
|
||||||
}
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
))}
|
|
||||||
</div>
|
|
||||||
))}
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
@ -1,48 +0,0 @@
|
||||||
import React, { useMemo } from 'react'
|
|
||||||
import DynamicRenderer from './DynamicRenderer'
|
|
||||||
import { useComponents } from '@/contexts/ComponentContext'
|
|
||||||
import { parseComponentDependencies } from '@/contexts/componentRuntime'
|
|
||||||
import { Loading } from '../shared'
|
|
||||||
import { useLocalization } from '@/utils/hooks/useLocalization'
|
|
||||||
|
|
||||||
export interface ComponentPreviewProps {
|
|
||||||
componentName?: string
|
|
||||||
className?: string
|
|
||||||
}
|
|
||||||
|
|
||||||
const ComponentPreview: React.FC<ComponentPreviewProps> = ({ componentName, className = '' }) => {
|
|
||||||
const { translate } = useLocalization()
|
|
||||||
const { components, loading } = useComponents()
|
|
||||||
|
|
||||||
// Referans olarak sabit tutulmalı: DynamicRenderer bunu effect bağımlılığı olarak kullanıyor.
|
|
||||||
const dependencies = useMemo(() => {
|
|
||||||
const component = components?.find((item) => item.name === componentName && item.isActive)
|
|
||||||
return parseComponentDependencies(component?.dependencies)
|
|
||||||
}, [components, componentName])
|
|
||||||
|
|
||||||
if (!componentName) {
|
|
||||||
return (
|
|
||||||
<div className="text-sm text-gray-500 dark:text-gray-400">
|
|
||||||
{translate('::App.DeveloperKitComponentPreview.NoComponentName')}
|
|
||||||
</div>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
if (loading || !Array.isArray(components)) {
|
|
||||||
return (
|
|
||||||
<div className="flex items-center justify-center min-h-screen bg-gray-50 dark:bg-gray-900">
|
|
||||||
<div className="text-center">
|
|
||||||
<Loading loading={true} />
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div className={`bg-white dark:bg-gray-900 ${className}`}>
|
|
||||||
<DynamicRenderer componentName={componentName} dependencies={dependencies} />
|
|
||||||
</div>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
export default ComponentPreview
|
|
||||||
|
|
@ -1,211 +0,0 @@
|
||||||
import React, { useCallback, useEffect, useRef, useState } from 'react'
|
|
||||||
import axios from 'axios'
|
|
||||||
import DOMPurify from 'dompurify'
|
|
||||||
import apiService from '@/services/api.service'
|
|
||||||
import * as UiKit from '@/components/ui'
|
|
||||||
import {
|
|
||||||
getComponentRuntimeCode,
|
|
||||||
parseComponentDependencies,
|
|
||||||
toErrorMessage,
|
|
||||||
type BabelLike,
|
|
||||||
} from '@/contexts/componentRuntime'
|
|
||||||
import { useLocalization } from '@/utils/hooks/useLocalization'
|
|
||||||
import { usePermission } from '@/utils/hooks/usePermission'
|
|
||||||
import { useComponents } from '../../contexts/ComponentContext'
|
|
||||||
import ErrorBoundary from './ErrorBoundary'
|
|
||||||
import PlatformIcon from './PlatformIcon'
|
|
||||||
import PlatformSelectComponent from './selectComponents'
|
|
||||||
import PlatformViewHost from './PlatformViewHost'
|
|
||||||
import { toast } from '../ui'
|
|
||||||
import { Loading } from '../shared'
|
|
||||||
|
|
||||||
type RuntimeModule = { exports: { default?: React.ComponentType<any> } }
|
|
||||||
|
|
||||||
const compileComponent = (
|
|
||||||
code: string,
|
|
||||||
scope: Record<string, any>,
|
|
||||||
Babel: BabelLike,
|
|
||||||
): React.ComponentType<any> | undefined => {
|
|
||||||
const transpiled = Babel.transform(code, {
|
|
||||||
filename: 'component.tsx',
|
|
||||||
presets: ['typescript', 'react'],
|
|
||||||
plugins: ['transform-modules-commonjs'],
|
|
||||||
}).code
|
|
||||||
|
|
||||||
if (!transpiled) throw new Error('Boş derleme çıktısı')
|
|
||||||
|
|
||||||
const module: RuntimeModule = { exports: {} }
|
|
||||||
const require = (moduleName: string) => {
|
|
||||||
if (moduleName === 'react') return React
|
|
||||||
if (moduleName === 'axios') return axios
|
|
||||||
if (moduleName === 'dompurify') return DOMPurify
|
|
||||||
if (moduleName === '@/services/api.service') return apiService
|
|
||||||
throw new Error(`Modül bulunamadı: ${moduleName}`)
|
|
||||||
}
|
|
||||||
|
|
||||||
const scopedEval = new Function('module', 'exports', 'require', ...Object.keys(scope), transpiled)
|
|
||||||
scopedEval(module, module.exports, require, ...Object.values(scope))
|
|
||||||
|
|
||||||
return module.exports.default
|
|
||||||
}
|
|
||||||
|
|
||||||
// The UI kit is spread as-is: every exported component is available to runtime
|
|
||||||
// code without an extra per-component entry here.
|
|
||||||
const staticComponents: Record<string, any> = {
|
|
||||||
...UiKit,
|
|
||||||
// Icon props travel as names, so generated code needs the same name resolver
|
|
||||||
// the designer canvas draws with.
|
|
||||||
PlatformIcon,
|
|
||||||
// Select.componentAs is stored as a name, so generated code needs the same
|
|
||||||
// resolver the designer canvas renders with.
|
|
||||||
PlatformSelectComponent,
|
|
||||||
PlatformViewHost,
|
|
||||||
toast,
|
|
||||||
apiService,
|
|
||||||
DOMPurify,
|
|
||||||
UiKit,
|
|
||||||
}
|
|
||||||
|
|
||||||
interface DynamicRendererProps {
|
|
||||||
componentName: string
|
|
||||||
dependencies?: string[]
|
|
||||||
}
|
|
||||||
|
|
||||||
const DynamicRenderer: React.FC<DynamicRendererProps> = ({
|
|
||||||
componentName,
|
|
||||||
dependencies: externalDeps,
|
|
||||||
}) => {
|
|
||||||
const [Component, setComponent] = useState<React.ComponentType<any> | null>(null)
|
|
||||||
const [error, setError] = useState<string | null>(null)
|
|
||||||
const { getComponentByName, components } = useComponents()
|
|
||||||
const { checkPermission } = usePermission()
|
|
||||||
const { translate } = useLocalization()
|
|
||||||
|
|
||||||
// Read through refs so the granted policies and the active language can change
|
|
||||||
// without recompiling the component; generated code calls both on every render.
|
|
||||||
const checkPermissionRef = useRef(checkPermission)
|
|
||||||
checkPermissionRef.current = checkPermission
|
|
||||||
const stableCheckPermission = useCallback(
|
|
||||||
(permission?: string) => checkPermissionRef.current(permission),
|
|
||||||
[],
|
|
||||||
)
|
|
||||||
const translateRef = useRef(translate)
|
|
||||||
translateRef.current = translate
|
|
||||||
const stableTranslate = useCallback(
|
|
||||||
(...args: Parameters<typeof translate>) => translateRef.current(...args),
|
|
||||||
[],
|
|
||||||
)
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
let cancelled = false
|
|
||||||
setComponent(null)
|
|
||||||
setError(null)
|
|
||||||
|
|
||||||
const storedComponent = getComponentByName(componentName)
|
|
||||||
if (!storedComponent) {
|
|
||||||
setError(`Component ${componentName} not found`)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
const build = async () => {
|
|
||||||
// Babel is several megabytes; load it only when something must be compiled.
|
|
||||||
const Babel = (await import('@babel/standalone')) as unknown as BabelLike
|
|
||||||
if (cancelled) return
|
|
||||||
|
|
||||||
const map = new Map(
|
|
||||||
components.map((component) => [
|
|
||||||
component.name,
|
|
||||||
{
|
|
||||||
code: getComponentRuntimeCode(component),
|
|
||||||
dependencies: parseComponentDependencies(component.dependencies),
|
|
||||||
},
|
|
||||||
]),
|
|
||||||
)
|
|
||||||
|
|
||||||
const compiled: Record<string, any> = {}
|
|
||||||
|
|
||||||
const compileWithDependencies = (name: string): any => {
|
|
||||||
if (compiled[name]) return compiled[name]
|
|
||||||
|
|
||||||
const entry = map.get(name)
|
|
||||||
if (!entry) {
|
|
||||||
if (staticComponents[name]) {
|
|
||||||
compiled[name] = staticComponents[name]
|
|
||||||
return staticComponents[name]
|
|
||||||
}
|
|
||||||
throw new Error(`Component ${name} not found`)
|
|
||||||
}
|
|
||||||
|
|
||||||
const depNames = name === componentName && externalDeps ? externalDeps : entry.dependencies
|
|
||||||
|
|
||||||
const deps: Record<string, any> = {}
|
|
||||||
for (const dep of depNames) {
|
|
||||||
deps[dep] = compileWithDependencies(dep)
|
|
||||||
}
|
|
||||||
|
|
||||||
const component = compileComponent(
|
|
||||||
entry.code,
|
|
||||||
{
|
|
||||||
React,
|
|
||||||
...staticComponents,
|
|
||||||
checkPermission: stableCheckPermission,
|
|
||||||
translate: stableTranslate,
|
|
||||||
...deps,
|
|
||||||
},
|
|
||||||
Babel,
|
|
||||||
)
|
|
||||||
if (!component) throw new Error(`Component ${name} bir default export döndürmüyor`)
|
|
||||||
|
|
||||||
compiled[name] = component
|
|
||||||
return component
|
|
||||||
}
|
|
||||||
|
|
||||||
try {
|
|
||||||
const RootComponent = compileWithDependencies(componentName)
|
|
||||||
if (!cancelled) setComponent(() => RootComponent)
|
|
||||||
} catch (err) {
|
|
||||||
console.error('Compilation error:', err)
|
|
||||||
if (!cancelled) setError(toErrorMessage(err))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
void build()
|
|
||||||
|
|
||||||
return () => {
|
|
||||||
cancelled = true
|
|
||||||
}
|
|
||||||
}, [
|
|
||||||
componentName,
|
|
||||||
externalDeps,
|
|
||||||
components,
|
|
||||||
getComponentByName,
|
|
||||||
stableCheckPermission,
|
|
||||||
stableTranslate,
|
|
||||||
])
|
|
||||||
|
|
||||||
if (error) {
|
|
||||||
return (
|
|
||||||
<div className="p-4 m-4 border-2 border-red-300 rounded-lg bg-red-50 text-red-700 dark:bg-red-950 dark:border-red-800 dark:text-red-300">
|
|
||||||
<div className="font-semibold text-sm">{componentName}</div>
|
|
||||||
<div className="text-sm whitespace-pre-wrap">{error}</div>
|
|
||||||
</div>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!Component)
|
|
||||||
return (
|
|
||||||
<div className="flex items-center justify-center min-h-screen bg-gray-50 dark:bg-gray-900">
|
|
||||||
<div className="text-center">
|
|
||||||
<Loading loading={true} />
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
)
|
|
||||||
|
|
||||||
return (
|
|
||||||
<ErrorBoundary key={componentName}>
|
|
||||||
<Component />
|
|
||||||
</ErrorBoundary>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
export default DynamicRenderer
|
|
||||||
|
|
@ -1,60 +0,0 @@
|
||||||
import { cloneElement } from 'react'
|
|
||||||
import Logo from '@/components/template/Logo'
|
|
||||||
import { APP_NAME } from '@/constants/app.constant'
|
|
||||||
import type { CommonProps } from '@/proxy/common'
|
|
||||||
import type { ReactNode, ReactElement } from 'react'
|
|
||||||
import { Avatar } from '@/components/ui'
|
|
||||||
import { useLocalization } from '@/utils/hooks/useLocalization'
|
|
||||||
import { ROUTES_ENUM } from '@/routes/route.constant'
|
|
||||||
import useDarkMode from '@/utils/hooks/useDarkmode'
|
|
||||||
|
|
||||||
interface CoverProps extends CommonProps {
|
|
||||||
content?: ReactNode
|
|
||||||
}
|
|
||||||
|
|
||||||
const Cover = ({ children, content, ...rest }: CoverProps) => {
|
|
||||||
const { translate } = useLocalization()
|
|
||||||
const [isDarkMode] = useDarkMode()
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div className="grid lg:grid-cols-3 h-full">
|
|
||||||
<div
|
|
||||||
className="col-span-2 bg-no-repeat bg-cover py-6 px-16 flex-col justify-between dark:bg-gray-800 hidden lg:flex relative"
|
|
||||||
style={{
|
|
||||||
backgroundImage: `url('/img/others/auth-cover-bg.jpg')`,
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
{/* Koyulaştırıcı katman */}
|
|
||||||
<div className="absolute inset-0 bg-black bg-opacity-50 z-0"></div>
|
|
||||||
|
|
||||||
<div className="relative z-10 flex flex-col h-full justify-between">
|
|
||||||
<Logo mode={isDarkMode ? 'dark' : 'light'} url={ROUTES_ENUM.authenticated.login} />
|
|
||||||
<div>
|
|
||||||
<div className="mb-6 flex items-center gap-4">
|
|
||||||
<Avatar className="border-2 border-white" shape="circle" src="/img/others/cto.png" />
|
|
||||||
<div className="text-white">
|
|
||||||
<div className="font-semibold text-base">Sedat ÖZTÜRK</div>
|
|
||||||
<span className="opacity-80">{translate('::App.Listform.ListformField.Founder')}</span>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<p className="text-lg text-white opacity-80">{translate('::App.LoginPanel.Message')}</p>
|
|
||||||
</div>
|
|
||||||
<span className="text-white">
|
|
||||||
Copyright © {`${new Date().getFullYear()}`}{' '}
|
|
||||||
<span className="font-semibold">{`${APP_NAME}`}</span>{' '}
|
|
||||||
</span>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div className="flex flex-col justify-center items-center bg-white dark:bg-gray-800">
|
|
||||||
<div className="w-full xl:max-w-[450px] px-8 max-w-[380px]">
|
|
||||||
<div className="mb-8">{content}</div>
|
|
||||||
{children ? cloneElement(children as ReactElement, { ...rest }) : null}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
export default Cover
|
|
||||||
|
|
@ -1,61 +0,0 @@
|
||||||
import React, { cloneElement } from 'react'
|
|
||||||
import Avatar from '@/components/ui/Avatar'
|
|
||||||
import Logo from '@/components/template/Logo'
|
|
||||||
import { APP_NAME } from '@/constants/app.constant'
|
|
||||||
import type { CommonProps } from '@/proxy/common'
|
|
||||||
import { useLocalization } from '@/utils/hooks/useLocalization'
|
|
||||||
import { ROUTES_ENUM } from '@/routes/route.constant'
|
|
||||||
import useDarkMode from '@/utils/hooks/useDarkmode'
|
|
||||||
|
|
||||||
interface SideProps extends CommonProps {
|
|
||||||
content?: React.ReactNode
|
|
||||||
}
|
|
||||||
|
|
||||||
const Side = ({ children, content, ...rest }: SideProps) => {
|
|
||||||
const { translate } = useLocalization()
|
|
||||||
const [isDarkMode] = useDarkMode()
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div className="grid lg:grid-cols-3 h-full">
|
|
||||||
<div
|
|
||||||
className="relative bg-no-repeat bg-cover py-6 px-16 flex-col justify-between hidden lg:flex"
|
|
||||||
style={{
|
|
||||||
backgroundImage: `url('/img/others/auth-side-bg.jpg')`,
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
<div className="absolute inset-0 bg-black bg-opacity-50 z-0"></div>
|
|
||||||
<div className="relative z-10 flex flex-col h-full justify-between">
|
|
||||||
<Logo mode={isDarkMode ? 'dark' : 'light'} className="drop-shadow-md" url={ROUTES_ENUM.authenticated.login} />
|
|
||||||
<div>
|
|
||||||
<div className="mb-6 flex items-center gap-4">
|
|
||||||
<Avatar className="border-2 border-white" shape="circle" src="/img/others/cto.png" />
|
|
||||||
<div className="text-white">
|
|
||||||
<div className="font-semibold text-base">Sedat ÖZTÜRK</div>
|
|
||||||
<span className="opacity-80">{ translate('::App.Listform.ListformField.Founder')}</span>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<p className="text-lg text-white opacity-80">
|
|
||||||
{ translate('::App.LoginPanel.Message')}
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
<span className="text-white">
|
|
||||||
Copyright © {`${new Date().getFullYear()}`}{' '}
|
|
||||||
<span className="font-semibold">{`${APP_NAME}`}</span>{' '}
|
|
||||||
</span>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div className="col-span-2 flex flex-col justify-start sm:justify-center items-center bg-white dark:bg-gray-800">
|
|
||||||
<div className="w-full xl:max-w-[450px] px-8 max-w-[380px]">
|
|
||||||
<div className="mb-8">{content}</div>
|
|
||||||
{children
|
|
||||||
? cloneElement(children as React.ReactElement, {
|
|
||||||
...rest,
|
|
||||||
})
|
|
||||||
: null}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
export default Side
|
|
||||||
|
|
@ -1,55 +0,0 @@
|
||||||
import { useEffect, useCallback } from 'react'
|
|
||||||
import { useStoreState, useStoreActions } from '@/store'
|
|
||||||
import { useLocation } from 'react-router-dom'
|
|
||||||
import type { LayoutType } from '@/proxy/theme/models'
|
|
||||||
import type { ComponentType } from 'react'
|
|
||||||
|
|
||||||
export type AppRouteProps<T> = {
|
|
||||||
component: ComponentType<T>
|
|
||||||
routeKey: string
|
|
||||||
layout?: LayoutType
|
|
||||||
}
|
|
||||||
|
|
||||||
const AppRoute = <T extends Record<string, unknown>>({
|
|
||||||
component: Component,
|
|
||||||
routeKey,
|
|
||||||
...props
|
|
||||||
}: AppRouteProps<T>) => {
|
|
||||||
const location = useLocation()
|
|
||||||
|
|
||||||
const { setCurrentRouteKey } = useStoreActions((actions) => actions.base.common)
|
|
||||||
const { setLayout, setPreviousLayout } = useStoreActions((actions) => actions.theme)
|
|
||||||
|
|
||||||
const layoutType = useStoreState((state) => state.theme.layout.type)
|
|
||||||
const previousLayout = useStoreState((state) => state.theme.layout.previousType)
|
|
||||||
|
|
||||||
const handleLayoutChange = useCallback(() => {
|
|
||||||
setCurrentRouteKey(routeKey)
|
|
||||||
|
|
||||||
if (props.layout && props.layout !== layoutType) {
|
|
||||||
setPreviousLayout(layoutType)
|
|
||||||
setLayout(props.layout)
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!props.layout && previousLayout && layoutType !== previousLayout) {
|
|
||||||
setLayout(previousLayout)
|
|
||||||
setPreviousLayout('')
|
|
||||||
}
|
|
||||||
}, [
|
|
||||||
layoutType,
|
|
||||||
previousLayout,
|
|
||||||
props.layout,
|
|
||||||
routeKey,
|
|
||||||
setCurrentRouteKey,
|
|
||||||
setLayout,
|
|
||||||
setPreviousLayout,
|
|
||||||
])
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
handleLayoutChange()
|
|
||||||
}, [location, handleLayoutChange])
|
|
||||||
|
|
||||||
return <Component {...(props as T)} />
|
|
||||||
}
|
|
||||||
|
|
||||||
export default AppRoute
|
|
||||||
|
|
@ -1,25 +0,0 @@
|
||||||
// AuthorityGuard.tsx
|
|
||||||
import { PropsWithChildren } from 'react'
|
|
||||||
import { Navigate, useLocation } from 'react-router-dom'
|
|
||||||
import useAuthority from '@/utils/hooks/useAuthority'
|
|
||||||
import { getAccessDeniedPath } from '@/utils/routing'
|
|
||||||
|
|
||||||
type AuthorityGuardProps = PropsWithChildren<{
|
|
||||||
userAuthority?: string[]
|
|
||||||
authority?: string[]
|
|
||||||
}>
|
|
||||||
|
|
||||||
const AuthorityGuard = (props: AuthorityGuardProps) => {
|
|
||||||
const { userAuthority = [], authority = [], children } = props
|
|
||||||
const roleMatched = useAuthority(userAuthority, authority)
|
|
||||||
const location = useLocation()
|
|
||||||
|
|
||||||
if (!roleMatched) {
|
|
||||||
const to = getAccessDeniedPath(location.pathname)
|
|
||||||
return <Navigate to={to} replace state={{ from: location }} />
|
|
||||||
}
|
|
||||||
|
|
||||||
return <>{children}</>
|
|
||||||
}
|
|
||||||
|
|
||||||
export default AuthorityGuard
|
|
||||||
|
|
@ -1,13 +0,0 @@
|
||||||
import { Navigate, Outlet } from 'react-router-dom'
|
|
||||||
import appConfig from '@/proxy/configs/app.config'
|
|
||||||
import useAuth from '@/utils/hooks/useAuth'
|
|
||||||
|
|
||||||
const { authenticatedEntryPath } = appConfig
|
|
||||||
|
|
||||||
const PublicRoute = () => {
|
|
||||||
const { authenticated } = useAuth()
|
|
||||||
|
|
||||||
return authenticated ? <Navigate to={authenticatedEntryPath} /> : <Outlet />
|
|
||||||
}
|
|
||||||
|
|
||||||
export default PublicRoute
|
|
||||||
|
|
@ -1,73 +0,0 @@
|
||||||
import React, { useState } from 'react'
|
|
||||||
import Dialog from './Dialog'
|
|
||||||
|
|
||||||
const DialogExample: React.FC = () => {
|
|
||||||
const [isOpen, setIsOpen] = useState(false)
|
|
||||||
|
|
||||||
const handleOpen = () => setIsOpen(true)
|
|
||||||
const handleClose = () => setIsOpen(false)
|
|
||||||
|
|
||||||
const handleMaximize = () => {
|
|
||||||
console.log('Dialog maximized - should cover full screen')
|
|
||||||
}
|
|
||||||
|
|
||||||
const handleRestore = () => {
|
|
||||||
console.log('Dialog restored - should return to normal size')
|
|
||||||
}
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div className="p-4">
|
|
||||||
<button
|
|
||||||
onClick={handleOpen}
|
|
||||||
className="px-4 py-2 bg-blue-500 text-white rounded hover:bg-blue-600"
|
|
||||||
>
|
|
||||||
Open Dialog with Window Controls
|
|
||||||
</button>
|
|
||||||
|
|
||||||
<Dialog
|
|
||||||
isOpen={isOpen}
|
|
||||||
onRequestClose={handleClose}
|
|
||||||
onClose={handleClose}
|
|
||||||
onMaximize={handleMaximize}
|
|
||||||
onRestore={handleRestore}
|
|
||||||
width={600}
|
|
||||||
height={400}
|
|
||||||
showWindowControls={true}
|
|
||||||
contentClassName="p-6"
|
|
||||||
>
|
|
||||||
<div className="h-full flex flex-col">
|
|
||||||
<h2 className="text-xl font-bold mb-4">Dialog with Window Controls</h2>
|
|
||||||
<div className="flex-1 overflow-auto">
|
|
||||||
<p className="mb-4">
|
|
||||||
This dialog has maximize and restore buttons in the top-right corner.
|
|
||||||
</p>
|
|
||||||
<ul className="list-disc ml-6 space-y-2 mb-6">
|
|
||||||
<li>Click the maximize button (□) to make the dialog fullscreen</li>
|
|
||||||
<li>Click the restore button (⧉) to return to normal size</li>
|
|
||||||
<li>Click the close button (×) to close the dialog</li>
|
|
||||||
</ul>
|
|
||||||
<div className="space-y-4">
|
|
||||||
<p className="text-gray-600">
|
|
||||||
Lorem ipsum dolor sit amet, consectetur adipiscing elit. Sed do eiusmod
|
|
||||||
tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim
|
|
||||||
veniam, quis nostrud exercitation ullamco laboris.
|
|
||||||
</p>
|
|
||||||
<p className="text-gray-600">
|
|
||||||
Duis aute irure dolor in reprehenderit in voluptate velit esse cillum
|
|
||||||
dolore eu fugiat nulla pariatur. Excepteur sint occaecat cupidatat non
|
|
||||||
proident, sunt in culpa qui officia deserunt mollit anim id est laborum.
|
|
||||||
</p>
|
|
||||||
<p className="text-gray-600">
|
|
||||||
Sed ut perspiciatis unde omnis iste natus error sit voluptatem accusantium
|
|
||||||
doloremque laudantium, totam rem aperiam, eaque ipsa quae ab illo inventore
|
|
||||||
veritatis et quasi architecto beatae vitae dicta sunt explicabo.
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</Dialog>
|
|
||||||
</div>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
export default DialogExample
|
|
||||||
|
|
@ -37,6 +37,7 @@ import {
|
||||||
isFormNode,
|
isFormNode,
|
||||||
normalizeDesignerKeyList,
|
normalizeDesignerKeyList,
|
||||||
readFormField,
|
readFormField,
|
||||||
|
DESIGNER_DROPDOWN_PLACEHOLDER_KEY,
|
||||||
resolveDesignerDropdownTitle,
|
resolveDesignerDropdownTitle,
|
||||||
resolveDesignerPreviewFilterValue,
|
resolveDesignerPreviewFilterValue,
|
||||||
resolveDesignerTabValue,
|
resolveDesignerTabValue,
|
||||||
|
|
@ -1334,6 +1335,7 @@ const renderElement = (
|
||||||
options,
|
options,
|
||||||
dropdownProps.activeKey,
|
dropdownProps.activeKey,
|
||||||
dropdownProps.title,
|
dropdownProps.title,
|
||||||
|
translate(DESIGNER_DROPDOWN_PLACEHOLDER_KEY),
|
||||||
)
|
)
|
||||||
return (
|
return (
|
||||||
<UiKit.Dropdown {...(dropdownProps as React.ComponentProps<typeof UiKit.Dropdown>)}>
|
<UiKit.Dropdown {...(dropdownProps as React.ComponentProps<typeof UiKit.Dropdown>)}>
|
||||||
|
|
|
||||||
|
|
@ -30,7 +30,7 @@ import {
|
||||||
DESIGNER_FILTER_DX_OPERATORS,
|
DESIGNER_FILTER_DX_OPERATORS,
|
||||||
toDesignerFilterParamName,
|
toDesignerFilterParamName,
|
||||||
normalizeDesignerKeyList,
|
normalizeDesignerKeyList,
|
||||||
DESIGNER_DROPDOWN_PLACEHOLDER,
|
DESIGNER_DROPDOWN_PLACEHOLDER_KEY,
|
||||||
FORM_SLOTS,
|
FORM_SLOTS,
|
||||||
SQL_DEFAULT_VALUE_PROP,
|
SQL_DEFAULT_VALUE_PROP,
|
||||||
type DesignerDataSource,
|
type DesignerDataSource,
|
||||||
|
|
@ -1656,6 +1656,29 @@ export const generateDesignerCode = (name: string, document: DesignerDocument) =
|
||||||
const hasDataSourceFilters = dataSources.some(
|
const hasDataSourceFilters = dataSources.some(
|
||||||
(source) => getDesignerDataSourceFilters(source).length > 0,
|
(source) => getDesignerDataSourceFilters(source).length > 0,
|
||||||
)
|
)
|
||||||
|
// Two designer sources can address the very same request — a Form Select and
|
||||||
|
// the Grid bound to the same list is the usual pair. They are grouped by
|
||||||
|
// request identity so the page issues one call instead of one per source; the
|
||||||
|
// duplicates read the canonical state through an alias, which keeps every
|
||||||
|
// `data_<sourceId>` binding in the document working untouched.
|
||||||
|
const firstSourceIdByRequest = new Map<string, string>()
|
||||||
|
const canonicalSourceId = new Map<string, string>()
|
||||||
|
dataSources.forEach((source) => {
|
||||||
|
// Filters belong to the identity: the same URL with different filters is a
|
||||||
|
// different request and must keep a state of its own.
|
||||||
|
const requestKey = [
|
||||||
|
source.method,
|
||||||
|
source.url.trim(),
|
||||||
|
source.responsePath || '',
|
||||||
|
filterEntriesExpression(source, sqlRecordByRef),
|
||||||
|
].join('|')
|
||||||
|
if (!firstSourceIdByRequest.has(requestKey)) firstSourceIdByRequest.set(requestKey, source.id)
|
||||||
|
canonicalSourceId.set(source.id, firstSourceIdByRequest.get(requestKey) as string)
|
||||||
|
})
|
||||||
|
/** Canonical id of the group a Form loads itself; a mount fetch would duplicate it. */
|
||||||
|
const formManagedCanonicalIds = new Set(
|
||||||
|
[...sqlManagedSelectSourceIds].map((id) => canonicalSourceId.get(id) ?? id),
|
||||||
|
)
|
||||||
const hasFilters = hasDataSourceFilters || platformFilterNodes.length > 0
|
const hasFilters = hasDataSourceFilters || platformFilterNodes.length > 0
|
||||||
const selectHelpers = hasSelect
|
const selectHelpers = hasSelect
|
||||||
? ` const toSelectOptions = (value, labelPath = "", valuePath = "") => {
|
? ` const toSelectOptions = (value, labelPath = "", valuePath = "") => {
|
||||||
|
|
@ -1686,7 +1709,7 @@ export const generateDesignerCode = (name: string, document: DesignerDocument) =
|
||||||
const key = activeKey === undefined || activeKey === null ? "" : String(activeKey)
|
const key = activeKey === undefined || activeKey === null ? "" : String(activeKey)
|
||||||
const selected = key ? options.find((option) => String(option?.value ?? "") === key) : undefined
|
const selected = key ? options.find((option) => String(option?.value ?? "") === key) : undefined
|
||||||
if (selected) return String(selected.label ?? selected.value ?? key)
|
if (selected) return String(selected.label ?? selected.value ?? key)
|
||||||
return String(fallbackTitle ?? "") || ${JSON.stringify(DESIGNER_DROPDOWN_PLACEHOLDER)}
|
return String(fallbackTitle ?? "") || ${translateExpression(DESIGNER_DROPDOWN_PLACEHOLDER_KEY)}
|
||||||
}`
|
}`
|
||||||
: ''
|
: ''
|
||||||
// Falls back to the first tab while an endpoint driven tab list is still empty
|
// Falls back to the first tab while an endpoint driven tab list is still empty
|
||||||
|
|
@ -1715,19 +1738,29 @@ export const generateDesignerCode = (name: string, document: DesignerDocument) =
|
||||||
}`
|
}`
|
||||||
: ''
|
: ''
|
||||||
// Grid cells print raw endpoint columns, so dates and decimals are localised
|
// Grid cells print raw endpoint columns, so dates and decimals are localised
|
||||||
// here — the culture is published on `<html lang>` because a runtime compiled
|
// here. The culture is injected by the component runtime; `<html lang>` is the
|
||||||
// component has neither hooks nor imports to reach the store.
|
// fallback for an older host, and an empty result hands the browser default to
|
||||||
|
// Intl rather than pinning the page to a language chosen in this file.
|
||||||
const localeHelpers = hasDataTable
|
const localeHelpers = hasDataTable
|
||||||
? ` const localeCulture = () => (typeof document !== "undefined" && document.documentElement.lang) || "en"
|
? ` const localeCulture = () => {
|
||||||
const localeDateOptions = { year: "numeric", month: "2-digit", day: "2-digit" }
|
const injected = typeof getCulture === "function" ? getCulture() : ""
|
||||||
const localeTimeOptions = { year: "numeric", month: "2-digit", day: "2-digit", hour: "2-digit", minute: "2-digit" }
|
if (injected) return injected
|
||||||
|
if (typeof document !== "undefined" && document.documentElement.lang) return document.documentElement.lang
|
||||||
|
return undefined
|
||||||
|
}
|
||||||
|
// Patterns come from the culture itself; a fixed day/month/year order would
|
||||||
|
// print the same layout in every language.
|
||||||
|
const localeDateOptions = { dateStyle: "short" }
|
||||||
|
const localeTimeOptions = { dateStyle: "short", timeStyle: "short" }
|
||||||
const localeIsoPattern = /^(\\d{4})-(\\d{2})-(\\d{2})(?:[T ](\\d{2}):(\\d{2})(?::(\\d{2})(?:\\.\\d+)?)?(Z|[+-]\\d{2}:?\\d{2})?)?$/
|
const localeIsoPattern = /^(\\d{4})-(\\d{2})-(\\d{2})(?:[T ](\\d{2}):(\\d{2})(?::(\\d{2})(?:\\.\\d+)?)?(Z|[+-]\\d{2}:?\\d{2})?)?$/
|
||||||
const formatLocaleValue = (value) => {
|
const formatLocaleValue = (value) => {
|
||||||
if (value === null || value === undefined) return ""
|
if (value === null || value === undefined) return ""
|
||||||
const culture = localeCulture()
|
const culture = localeCulture()
|
||||||
if (typeof value === "number") {
|
if (typeof value === "number") {
|
||||||
if (!Number.isFinite(value)) return String(value)
|
if (!Number.isFinite(value)) return String(value)
|
||||||
return new Intl.NumberFormat(culture, { useGrouping: false, maximumFractionDigits: 20 }).format(value)
|
// Grouping and the decimal separator are the culture's decision; only the
|
||||||
|
// precision guard stays, so a long decimal is not rounded away.
|
||||||
|
return new Intl.NumberFormat(culture, { maximumFractionDigits: 20 }).format(value)
|
||||||
}
|
}
|
||||||
if (value instanceof Date) {
|
if (value instanceof Date) {
|
||||||
return Number.isNaN(value.getTime()) ? "" : new Intl.DateTimeFormat(culture, localeTimeOptions).format(value)
|
return Number.isNaN(value.getTime()) ? "" : new Intl.DateTimeFormat(culture, localeTimeOptions).format(value)
|
||||||
|
|
@ -1992,7 +2025,18 @@ export const generateDesignerCode = (name: string, document: DesignerDocument) =
|
||||||
const identifier = safeIdentifier(source.id)
|
const identifier = safeIdentifier(source.id)
|
||||||
// POST/PUT/DELETE sources are only ever invoked by a Form command;
|
// POST/PUT/DELETE sources are only ever invoked by a Form command;
|
||||||
// they hold no readable state and must never be written to.
|
// they hold no readable state and must never be written to.
|
||||||
if (source.method !== 'GET' || !source.url.trim().startsWith('/api/')) {
|
const isReadable = source.method === 'GET' && source.url.trim().startsWith('/api/')
|
||||||
|
const canonicalId = canonicalSourceId.get(source.id) ?? source.id
|
||||||
|
// A duplicate declares no state of its own: it aliases the canonical
|
||||||
|
// source, so both bindings always read the one response that was fetched.
|
||||||
|
if (canonicalId !== source.id) {
|
||||||
|
const canonical = safeIdentifier(canonicalId)
|
||||||
|
return isReadable
|
||||||
|
? ` const data_${identifier} = data_${canonical}
|
||||||
|
const setData_${identifier} = setData_${canonical}`
|
||||||
|
: ` const data_${identifier} = data_${canonical}`
|
||||||
|
}
|
||||||
|
if (!isReadable) {
|
||||||
return ` const [data_${identifier}] = React.useState(null)`
|
return ` const [data_${identifier}] = React.useState(null)`
|
||||||
}
|
}
|
||||||
return ` const [data_${identifier}, setData_${identifier}] = React.useState(null)`
|
return ` const [data_${identifier}, setData_${identifier}] = React.useState(null)`
|
||||||
|
|
@ -2002,10 +2046,13 @@ export const generateDesignerCode = (name: string, document: DesignerDocument) =
|
||||||
.map((source) => {
|
.map((source) => {
|
||||||
const identifier = safeIdentifier(source.id)
|
const identifier = safeIdentifier(source.id)
|
||||||
if (source.method !== 'GET' || !source.url.trim().startsWith('/api/')) return ''
|
if (source.method !== 'GET' || !source.url.trim().startsWith('/api/')) return ''
|
||||||
|
// A duplicate is only an alias of an earlier source; the canonical one
|
||||||
|
// already owns the single fetch for this request.
|
||||||
|
if ((canonicalSourceId.get(source.id) ?? source.id) !== source.id) return ''
|
||||||
// A GetById style URL is not fetched on mount — requesting a literal `{id}`
|
// A GetById style URL is not fetched on mount — requesting a literal `{id}`
|
||||||
// is a guaranteed 400 — and neither is a source the owning Form
|
// is a guaranteed 400 — and neither is a source the owning Form
|
||||||
// loads itself, through the setter declared above.
|
// loads itself, through the setter declared above.
|
||||||
if (hasFormUrlParams(source.url) || sqlManagedSelectSourceIds.has(source.id)) {
|
if (hasFormUrlParams(source.url) || formManagedCanonicalIds.has(source.id)) {
|
||||||
return ''
|
return ''
|
||||||
}
|
}
|
||||||
const entries = filterEntriesExpression(source, sqlRecordByRef)
|
const entries = filterEntriesExpression(source, sqlRecordByRef)
|
||||||
|
|
|
||||||
|
|
@ -504,7 +504,8 @@ export const resolveDesignerTabValue = (
|
||||||
return options.length ? String(options[0]?.value ?? '') : ''
|
return options.length ? String(options[0]?.value ?? '') : ''
|
||||||
}
|
}
|
||||||
|
|
||||||
export const DESIGNER_DROPDOWN_PLACEHOLDER = 'Seçiniz'
|
/** Localization key of the toggle label; the text itself never lives in the code. */
|
||||||
|
export const DESIGNER_DROPDOWN_PLACEHOLDER_KEY = '::App.Platform.Select'
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Dropdown renders its toggle from `title` only; `activeKey` merely highlights an
|
* Dropdown renders its toggle from `title` only; `activeKey` merely highlights an
|
||||||
|
|
@ -515,12 +516,13 @@ export const resolveDesignerDropdownTitle = (
|
||||||
options: Array<Record<string, unknown>>,
|
options: Array<Record<string, unknown>>,
|
||||||
activeKey: unknown,
|
activeKey: unknown,
|
||||||
fallbackTitle: unknown,
|
fallbackTitle: unknown,
|
||||||
|
placeholder: string,
|
||||||
) => {
|
) => {
|
||||||
const key = activeKey === undefined || activeKey === null ? '' : String(activeKey)
|
const key = activeKey === undefined || activeKey === null ? '' : String(activeKey)
|
||||||
const selected = key ? options.find((option) => String(option?.value ?? '') === key) : undefined
|
const selected = key ? options.find((option) => String(option?.value ?? '') === key) : undefined
|
||||||
if (selected) return String(selected.label ?? selected.value ?? key)
|
if (selected) return String(selected.label ?? selected.value ?? key)
|
||||||
const title = fallbackTitle === undefined || fallbackTitle === null ? '' : String(fallbackTitle)
|
const title = fallbackTitle === undefined || fallbackTitle === null ? '' : String(fallbackTitle)
|
||||||
return title || DESIGNER_DROPDOWN_PLACEHOLDER
|
return title || placeholder
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface DesignerPropertyInfo extends PropertyInfo {
|
export interface DesignerPropertyInfo extends PropertyInfo {
|
||||||
|
|
|
||||||
|
|
@ -1,38 +0,0 @@
|
||||||
import { TW_COLORS } from '@/utils/tailwind'
|
|
||||||
const C = TW_COLORS as any
|
|
||||||
|
|
||||||
export const COLOR_1 = C.indigo[600]
|
|
||||||
export const COLOR_2 = C.blue[500]
|
|
||||||
export const COLOR_3 = C.emerald[500]
|
|
||||||
export const COLOR_4 = C.amber[500]
|
|
||||||
export const COLOR_5 = C.red[500]
|
|
||||||
export const COLOR_6 = C.purple[500]
|
|
||||||
export const COLOR_7 = C.cyan[500]
|
|
||||||
|
|
||||||
export const COLOR_1_LIGHT = C.indigo[100]
|
|
||||||
export const COLOR_2_LIGHT = C.blue[100]
|
|
||||||
export const COLOR_3_LIGHT = C.emerald[100]
|
|
||||||
export const COLOR_4_LIGHT = C.amber[100]
|
|
||||||
export const COLOR_5_LIGHT = C.red[100]
|
|
||||||
export const COLOR_6_LIGHT = C.purple[100]
|
|
||||||
export const COLOR_7_LIGHT = C.cyan[100]
|
|
||||||
|
|
||||||
export const COLORS = [
|
|
||||||
COLOR_1,
|
|
||||||
COLOR_2,
|
|
||||||
COLOR_3,
|
|
||||||
COLOR_4,
|
|
||||||
COLOR_5,
|
|
||||||
COLOR_6,
|
|
||||||
COLOR_7,
|
|
||||||
]
|
|
||||||
|
|
||||||
export const COLORS_LIGHT = [
|
|
||||||
COLOR_1_LIGHT,
|
|
||||||
COLOR_2_LIGHT,
|
|
||||||
COLOR_3_LIGHT,
|
|
||||||
COLOR_4_LIGHT,
|
|
||||||
COLOR_5_LIGHT,
|
|
||||||
COLOR_6_LIGHT,
|
|
||||||
COLOR_7_LIGHT,
|
|
||||||
]
|
|
||||||
|
|
@ -99,6 +99,7 @@ export const ComponentProvider: React.FC<{ children: React.ReactNode }> = ({ chi
|
||||||
const { translate } = useLocalization()
|
const { translate } = useLocalization()
|
||||||
const { checkPermission } = usePermission()
|
const { checkPermission } = usePermission()
|
||||||
const applicationConfig = useStoreState((state) => state.abpConfig?.config)
|
const applicationConfig = useStoreState((state) => state.abpConfig?.config)
|
||||||
|
const currentLang = useStoreState((state) => state.locale.currentLang)
|
||||||
const extraProperties = applicationConfig?.extraProperties
|
const extraProperties = applicationConfig?.extraProperties
|
||||||
|
|
||||||
const [components, setComponents] = useState<CustomComponent[]>([])
|
const [components, setComponents] = useState<CustomComponent[]>([])
|
||||||
|
|
@ -130,6 +131,16 @@ export const ComponentProvider: React.FC<{ children: React.ReactNode }> = ({ chi
|
||||||
[],
|
[],
|
||||||
)
|
)
|
||||||
|
|
||||||
|
// Number and date formatting inside a runtime component follows the active
|
||||||
|
// culture. It is read through a ref for the same reason as the two above: a
|
||||||
|
// language change must reach the compiled bundle without recompiling it.
|
||||||
|
const culture =
|
||||||
|
useStoreState((state) => state.abpConfig?.config?.localization?.currentCulture?.cultureName) ??
|
||||||
|
currentLang
|
||||||
|
const cultureRef = useRef(culture)
|
||||||
|
cultureRef.current = culture
|
||||||
|
const stableGetCulture = useCallback(() => cultureRef.current, [])
|
||||||
|
|
||||||
const refreshComponents = useCallback(async () => {
|
const refreshComponents = useCallback(async () => {
|
||||||
if (!applicationConfig) return
|
if (!applicationConfig) return
|
||||||
|
|
||||||
|
|
@ -309,6 +320,7 @@ export const ComponentProvider: React.FC<{ children: React.ReactNode }> = ({ chi
|
||||||
axios,
|
axios,
|
||||||
translate: stableTranslate,
|
translate: stableTranslate,
|
||||||
checkPermission: stableCheckPermission,
|
checkPermission: stableCheckPermission,
|
||||||
|
getCulture: stableGetCulture,
|
||||||
})
|
})
|
||||||
|
|
||||||
const { registry, errors } = compileComponentBundle(
|
const { registry, errors } = compileComponentBundle(
|
||||||
|
|
@ -336,7 +348,7 @@ export const ComponentProvider: React.FC<{ children: React.ReactNode }> = ({ chi
|
||||||
return () => {
|
return () => {
|
||||||
cancelled = true
|
cancelled = true
|
||||||
}
|
}
|
||||||
}, [compilationSignature, stableCheckPermission, stableTranslate])
|
}, [compilationSignature, stableCheckPermission, stableTranslate, stableGetCulture])
|
||||||
|
|
||||||
const renderComponent = useCallback(
|
const renderComponent = useCallback(
|
||||||
(name: string, props: ComponentProps = {}) => {
|
(name: string, props: ComponentProps = {}) => {
|
||||||
|
|
|
||||||
|
|
@ -1,21 +0,0 @@
|
||||||
export interface DashboardWidgetsRequestDto {
|
|
||||||
tarih1: string
|
|
||||||
tarih2: string
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface DashboardWidgetsDto {
|
|
||||||
id: number
|
|
||||||
label: string
|
|
||||||
dataValue: number
|
|
||||||
dataGrowShrink: number
|
|
||||||
valuePrefix: string
|
|
||||||
date: Date
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface WidgetsData {
|
|
||||||
label: string
|
|
||||||
datavalue: number
|
|
||||||
datagrowShrink: number
|
|
||||||
valuePrefix: string
|
|
||||||
date: Date
|
|
||||||
}
|
|
||||||
|
|
@ -1,9 +0,0 @@
|
||||||
import apiService from '../../services/api.service'
|
|
||||||
import { DashboardWidgetsRequestDto, DashboardWidgetsDto } from './models'
|
|
||||||
|
|
||||||
export const getDashboardWidgetList = (input: DashboardWidgetsRequestDto) => {
|
|
||||||
return apiService.fetchData<DashboardWidgetsDto[]>({
|
|
||||||
method: 'GET',
|
|
||||||
url: `/api/app/dashboard/widgets?Tarih1=${input.tarih1}&Tarih2=${input.tarih2}`,
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
@ -1,106 +0,0 @@
|
||||||
export type ReportParameterType = 'text' | 'number' | 'date' | 'select' | 'checkbox'
|
|
||||||
|
|
||||||
export interface ReportCategoryDto {
|
|
||||||
id: string
|
|
||||||
name: string
|
|
||||||
description?: string
|
|
||||||
icon?: string
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface ReportParameterDto {
|
|
||||||
id: string
|
|
||||||
templateId: string
|
|
||||||
name: string
|
|
||||||
placeholder?: string
|
|
||||||
type: ReportParameterType
|
|
||||||
defaultValue?: string
|
|
||||||
required: boolean
|
|
||||||
description?: string
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface ReportTemplateDto {
|
|
||||||
id: string
|
|
||||||
name: string
|
|
||||||
description?: string
|
|
||||||
htmlContent: string
|
|
||||||
categoryId?: string
|
|
||||||
tags: string[]
|
|
||||||
parameters: ReportParameterDto[]
|
|
||||||
|
|
||||||
// FullAuditedEntityDto alanları
|
|
||||||
creationTime: string // ISO
|
|
||||||
lastModificationTime?: string // ISO | undefined
|
|
||||||
creatorId?: string
|
|
||||||
lastModifierId?: string
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface ReportGeneratedDto {
|
|
||||||
id: string
|
|
||||||
templateId?: string | null
|
|
||||||
templateName: string
|
|
||||||
generatedContent: string
|
|
||||||
parameters: Record<string, string>
|
|
||||||
|
|
||||||
// FullAuditedEntityDto alanları
|
|
||||||
creationTime: string // ISO
|
|
||||||
lastModificationTime?: string // ISO | undefined
|
|
||||||
creatorId?: string
|
|
||||||
lastModifierId?: string
|
|
||||||
|
|
||||||
template?: ReportTemplateDto // dolu gelebilir
|
|
||||||
}
|
|
||||||
|
|
||||||
/** Create / Update input’ları */
|
|
||||||
export interface CreateReportParameterDto {
|
|
||||||
name: string
|
|
||||||
placeholder?: string
|
|
||||||
type: ReportParameterType
|
|
||||||
defaultValue?: string
|
|
||||||
required: boolean
|
|
||||||
description?: string
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface UpdateReportParameterDto extends CreateReportParameterDto {
|
|
||||||
id?: string // opsiyonel
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface CreateReportTemplateDto {
|
|
||||||
name: string
|
|
||||||
description?: string
|
|
||||||
htmlContent: string
|
|
||||||
categoryId?: string
|
|
||||||
tags?: string[]
|
|
||||||
parameters: CreateReportParameterDto[]
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface UpdateReportTemplateDto {
|
|
||||||
name: string
|
|
||||||
description?: string
|
|
||||||
htmlContent: string
|
|
||||||
categoryId?: string
|
|
||||||
tags?: string[]
|
|
||||||
parameters: UpdateReportParameterDto[]
|
|
||||||
}
|
|
||||||
|
|
||||||
/** Generate input’u */
|
|
||||||
export interface ReportGenerateDto {
|
|
||||||
templateId: string
|
|
||||||
parameters: Record<string, string>
|
|
||||||
}
|
|
||||||
|
|
||||||
/** List input’ları (query string) */
|
|
||||||
export interface GetReportTemplatesInput {
|
|
||||||
skipCount?: number
|
|
||||||
maxResultCount?: number
|
|
||||||
sorting?: string
|
|
||||||
filter?: string
|
|
||||||
categoryId?: string
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface GetReportsGeneratedInput {
|
|
||||||
skipCount?: number
|
|
||||||
maxResultCount?: number
|
|
||||||
sorting?: string
|
|
||||||
filter?: string
|
|
||||||
templateId?: string
|
|
||||||
}
|
|
||||||
|
|
@ -13,6 +13,18 @@ const physicalComponentCache = new Map<
|
||||||
React.LazyExoticComponent<React.ComponentType<any>>
|
React.LazyExoticComponent<React.ComponentType<any>>
|
||||||
>()
|
>()
|
||||||
|
|
||||||
|
// Runtime component wrappers are cached by name so their identity survives every
|
||||||
|
// re-render of the router. Without this, each render produced a brand new
|
||||||
|
// function type and React remounted the whole page — every data hook inside the
|
||||||
|
// component fired again (twice on a language change, once per configuration
|
||||||
|
// refresh).
|
||||||
|
const runtimeComponentCache = new Map<string, React.ComponentType<any>>()
|
||||||
|
// The wrapper must not close over `renderComponent`: it changes after the
|
||||||
|
// asynchronous bundle compilation finishes, and an old closure would render an
|
||||||
|
// empty page. The latest renderer is kept beside the wrapper and read at render
|
||||||
|
// time instead.
|
||||||
|
const runtimeRenderers = new Map<string, (name: string, props?: any) => React.ReactNode>()
|
||||||
|
|
||||||
// Fiziksel komponent yükleme (mevcut mantık)
|
// Fiziksel komponent yükleme (mevcut mantık)
|
||||||
function loadPhysicalComponent(componentPath: string) {
|
function loadPhysicalComponent(componentPath: string) {
|
||||||
const cleanedPath = componentPath.replace(/^@\//, '')
|
const cleanedPath = componentPath.replace(/^@\//, '')
|
||||||
|
|
@ -50,19 +62,21 @@ function loadDynamicComponent(
|
||||||
renderComponent &&
|
renderComponent &&
|
||||||
isComponentRegistered(componentPath)
|
isComponentRegistered(componentPath)
|
||||||
) {
|
) {
|
||||||
DynamicComponent = (props: any) => renderComponent(componentPath, props) as React.ReactElement
|
runtimeRenderers.set(componentPath, renderComponent)
|
||||||
|
let cached = runtimeComponentCache.get(componentPath)
|
||||||
|
if (!cached) {
|
||||||
|
cached = (props: any) =>
|
||||||
|
runtimeRenderers.get(componentPath)?.(componentPath, props) as React.ReactElement
|
||||||
|
cached.displayName = `RuntimeComponent(${componentPath})`
|
||||||
|
runtimeComponentCache.set(componentPath, cached)
|
||||||
|
}
|
||||||
|
DynamicComponent = cached
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!DynamicComponent) {
|
if (!DynamicComponent) {
|
||||||
if (isComponentRegistered) {
|
|
||||||
console.log('Database component registry available - checking...')
|
|
||||||
}
|
|
||||||
throw new Error(`Dynamic component not found: ${componentPath}`)
|
throw new Error(`Dynamic component not found: ${componentPath}`)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Do not put this wrapper in a module-level cache. renderComponent changes
|
|
||||||
// after the asynchronous database bundle compilation finishes. Caching this
|
|
||||||
// function would keep the pre-compilation closure and render an empty page.
|
|
||||||
return DynamicComponent as React.ComponentType<any>
|
return DynamicComponent as React.ComponentType<any>
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -1,48 +0,0 @@
|
||||||
import { PagedResultDto } from '@/proxy'
|
|
||||||
import apiService from './api.service'
|
|
||||||
import { QuestionDto } from '@/types/coordinator'
|
|
||||||
|
|
||||||
class QuestionService {
|
|
||||||
async getQuestions(): Promise<PagedResultDto<QuestionDto>> {
|
|
||||||
const response = await apiService.fetchData<PagedResultDto<QuestionDto>>({
|
|
||||||
url: '/api/app/question',
|
|
||||||
method: 'GET',
|
|
||||||
})
|
|
||||||
return response.data
|
|
||||||
}
|
|
||||||
|
|
||||||
async getQuestion(id: string): Promise<QuestionDto> {
|
|
||||||
const response = await apiService.fetchData<QuestionDto>({
|
|
||||||
url: `/api/app/question/${id}`,
|
|
||||||
method: 'GET',
|
|
||||||
})
|
|
||||||
return response.data
|
|
||||||
}
|
|
||||||
|
|
||||||
async updateQuestion(id: string, input: QuestionDto) {
|
|
||||||
const response = await apiService.fetchData<QuestionDto>({
|
|
||||||
url: `/api/app/question/${id}`,
|
|
||||||
method: 'PUT',
|
|
||||||
data: input as any,
|
|
||||||
})
|
|
||||||
return response.data
|
|
||||||
}
|
|
||||||
|
|
||||||
async createQuestion(input: QuestionDto) {
|
|
||||||
const response = await apiService.fetchData<QuestionDto>({
|
|
||||||
method: 'POST',
|
|
||||||
url: '/api/app/question',
|
|
||||||
data: input as any,
|
|
||||||
})
|
|
||||||
return response.data
|
|
||||||
}
|
|
||||||
|
|
||||||
async deleteQuestion(id: string) {
|
|
||||||
await apiService.fetchData<void>({
|
|
||||||
method: 'DELETE',
|
|
||||||
url: `/api/app/question/${id}`,
|
|
||||||
})
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
export const questionService = new QuestionService()
|
|
||||||
|
|
@ -1,139 +0,0 @@
|
||||||
import {
|
|
||||||
ReportTemplateDto,
|
|
||||||
ReportGeneratedDto,
|
|
||||||
CreateReportTemplateDto,
|
|
||||||
UpdateReportTemplateDto,
|
|
||||||
ReportGenerateDto,
|
|
||||||
ReportCategoryDto,
|
|
||||||
GetReportTemplatesInput, // backend'deki GenerateReportDto (templateId + parameters)
|
|
||||||
} from '@/proxy/reports/models'
|
|
||||||
import apiService from './api.service'
|
|
||||||
import { PagedAndSortedResultRequestDto, PagedResultDto } from '@/proxy'
|
|
||||||
|
|
||||||
export interface ReportsData {
|
|
||||||
templates: ReportTemplateDto[]
|
|
||||||
generatedReports: ReportGeneratedDto[]
|
|
||||||
}
|
|
||||||
|
|
||||||
export class ReportsService {
|
|
||||||
apiName = 'Default'
|
|
||||||
|
|
||||||
getCategories = () =>
|
|
||||||
apiService.fetchData<ReportCategoryDto[]>(
|
|
||||||
{
|
|
||||||
method: 'GET',
|
|
||||||
url: '/api/app/report/categories',
|
|
||||||
},
|
|
||||||
{ apiName: this.apiName },
|
|
||||||
)
|
|
||||||
|
|
||||||
// TEMPLATES
|
|
||||||
getTemplates = (input: GetReportTemplatesInput) =>
|
|
||||||
apiService.fetchData<PagedResultDto<ReportTemplateDto>, PagedAndSortedResultRequestDto>(
|
|
||||||
{
|
|
||||||
method: 'GET',
|
|
||||||
url: '/api/app/report/templates', // ✔ Swagger: GET /api/app/report/templates
|
|
||||||
params: {
|
|
||||||
sorting: input.sorting,
|
|
||||||
skipCount: input.skipCount,
|
|
||||||
maxResultCount: input.maxResultCount,
|
|
||||||
filter: input.filter,
|
|
||||||
categoryId: input.categoryId,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
{ apiName: this.apiName },
|
|
||||||
)
|
|
||||||
|
|
||||||
getTemplateById = (id: string) =>
|
|
||||||
apiService.fetchData<ReportTemplateDto>(
|
|
||||||
{
|
|
||||||
method: 'GET',
|
|
||||||
url: `/api/app/report/${id}/template`, // ✔ Swagger: GET /api/app/report/{id}/template
|
|
||||||
},
|
|
||||||
{ apiName: this.apiName },
|
|
||||||
)
|
|
||||||
|
|
||||||
createTemplate = (input: CreateReportTemplateDto) =>
|
|
||||||
apiService.fetchData<ReportTemplateDto, CreateReportTemplateDto>(
|
|
||||||
{
|
|
||||||
method: 'POST',
|
|
||||||
url: '/api/app/report/template', // ✔ Swagger: POST /api/app/report/template
|
|
||||||
data: input,
|
|
||||||
},
|
|
||||||
{ apiName: this.apiName },
|
|
||||||
)
|
|
||||||
|
|
||||||
updateTemplate = (id: string, input: UpdateReportTemplateDto) =>
|
|
||||||
apiService.fetchData<ReportTemplateDto, UpdateReportTemplateDto>(
|
|
||||||
{
|
|
||||||
method: 'PUT',
|
|
||||||
url: `/api/app/report/${id}/template`, // ✔ Swagger: PUT /api/app/report/{id}/template
|
|
||||||
data: input,
|
|
||||||
},
|
|
||||||
{ apiName: this.apiName },
|
|
||||||
)
|
|
||||||
|
|
||||||
deleteTemplate = (id: string) =>
|
|
||||||
apiService.fetchData(
|
|
||||||
{
|
|
||||||
method: 'DELETE',
|
|
||||||
url: `/api/app/report/${id}/template`, // ✔ Swagger: DELETE /api/app/report/{id}/template
|
|
||||||
},
|
|
||||||
{ apiName: this.apiName },
|
|
||||||
)
|
|
||||||
|
|
||||||
// GENERATED REPORTS
|
|
||||||
getGeneratedReports = (input: PagedAndSortedResultRequestDto) =>
|
|
||||||
apiService.fetchData<PagedResultDto<ReportGeneratedDto>, PagedAndSortedResultRequestDto>(
|
|
||||||
{
|
|
||||||
method: 'GET',
|
|
||||||
url: '/api/app/report/generated-reports', // ✔ Swagger: GET /api/app/report/generated-reports
|
|
||||||
params: {
|
|
||||||
sorting: input.sorting,
|
|
||||||
skipCount: input.skipCount,
|
|
||||||
maxResultCount: input.maxResultCount,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
{ apiName: this.apiName },
|
|
||||||
)
|
|
||||||
|
|
||||||
getGeneratedReportById = (id: string) =>
|
|
||||||
apiService.fetchData<ReportGeneratedDto>(
|
|
||||||
{
|
|
||||||
method: 'GET',
|
|
||||||
url: `/api/app/report/${id}/generated-report`, // ✔ Swagger: GET /api/app/report/{id}/generated-report
|
|
||||||
},
|
|
||||||
{ apiName: this.apiName },
|
|
||||||
)
|
|
||||||
|
|
||||||
generateReport = (input: ReportGenerateDto) =>
|
|
||||||
apiService.fetchData<ReportGeneratedDto, ReportGenerateDto>(
|
|
||||||
{
|
|
||||||
method: 'POST',
|
|
||||||
url: '/api/app/report/generate-report', // ✔ Swagger: POST /api/app/report/generate-report
|
|
||||||
data: input,
|
|
||||||
},
|
|
||||||
{ apiName: this.apiName },
|
|
||||||
)
|
|
||||||
|
|
||||||
deleteGeneratedReport = (id: string) =>
|
|
||||||
apiService.fetchData(
|
|
||||||
{
|
|
||||||
method: 'DELETE',
|
|
||||||
url: `/api/app/report/${id}/generated-report`, // ✔ Swagger: DELETE /api/app/report/{id}/generated-report
|
|
||||||
},
|
|
||||||
{ apiName: this.apiName },
|
|
||||||
)
|
|
||||||
|
|
||||||
// BULK
|
|
||||||
getAllData = () =>
|
|
||||||
apiService.fetchData<ReportsData>(
|
|
||||||
{
|
|
||||||
method: 'GET',
|
|
||||||
url: '/api/app/report/data', // ✔ Swagger: GET /api/app/report/data
|
|
||||||
},
|
|
||||||
{ apiName: this.apiName },
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
export default ReportsService
|
|
||||||
|
|
@ -1,18 +0,0 @@
|
||||||
import { RouteDto } from '@/proxy/routes/models'
|
|
||||||
import apiService, { Config } from '@/services/api.service'
|
|
||||||
|
|
||||||
export class RouteService {
|
|
||||||
apiName = 'Default'
|
|
||||||
|
|
||||||
getRoutes = (config?: Partial<Config>) =>
|
|
||||||
apiService.fetchData<RouteDto[]>(
|
|
||||||
{
|
|
||||||
method: 'GET',
|
|
||||||
url: '/api/app/route',
|
|
||||||
},
|
|
||||||
{ apiName: this.apiName, ...config },
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
const routeService = new RouteService()
|
|
||||||
export default routeService
|
|
||||||
|
|
@ -1,22 +0,0 @@
|
||||||
export interface WidgetGroupDto {
|
|
||||||
// Widget Grubu
|
|
||||||
colGap?: number
|
|
||||||
colSpan?: number
|
|
||||||
className?: string
|
|
||||||
items: WidgetEditDto[]
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface WidgetEditDto {
|
|
||||||
// Widget Düzenleme
|
|
||||||
colGap: number
|
|
||||||
colSpan: number
|
|
||||||
sqlQuery?: string
|
|
||||||
className?: string
|
|
||||||
title: string
|
|
||||||
value: string
|
|
||||||
valueClassName: string
|
|
||||||
color: string
|
|
||||||
icon: string
|
|
||||||
subTitle: string
|
|
||||||
onClick: string
|
|
||||||
}
|
|
||||||
|
|
@ -1,134 +0,0 @@
|
||||||
import { FullAuditedEntityDto } from "@/proxy";
|
|
||||||
|
|
||||||
export type QuestionType =
|
|
||||||
| 'multiple-choice'
|
|
||||||
| 'fill-blank'
|
|
||||||
| 'multiple-answer'
|
|
||||||
| 'matching'
|
|
||||||
| 'ordering'
|
|
||||||
| 'open-ended'
|
|
||||||
| 'true-false'
|
|
||||||
| 'calculation'
|
|
||||||
|
|
||||||
export const QUESTION_TYPE_LABELS: Record<QuestionType, string> = {
|
|
||||||
'multiple-choice': 'Multiple Choice',
|
|
||||||
'fill-blank': 'Fill in the Blank',
|
|
||||||
'multiple-answer': 'Multiple Answer',
|
|
||||||
'matching': 'Matching',
|
|
||||||
'ordering': 'Ordering',
|
|
||||||
'open-ended': 'Open Ended',
|
|
||||||
'true-false': 'True / False',
|
|
||||||
'calculation': 'Calculation',
|
|
||||||
}
|
|
||||||
|
|
||||||
export type ExamType = "exam" | "assignment" | "test";
|
|
||||||
export type TestType = 'pdf' | 'image';
|
|
||||||
export type MediaType = 'image' | 'video';
|
|
||||||
export type QuestionDifficulty = 'easy' | 'medium' | 'hard';
|
|
||||||
export type ExamSessionStatus = 'in-progress' | 'completed' | 'submitted';
|
|
||||||
|
|
||||||
export interface QuestionPoolDto extends FullAuditedEntityDto {
|
|
||||||
name: string;
|
|
||||||
description: string;
|
|
||||||
questions: QuestionDto[];
|
|
||||||
tags: string[];
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface QuestionDto extends FullAuditedEntityDto {
|
|
||||||
questionType: QuestionType;
|
|
||||||
title: string;
|
|
||||||
content: string;
|
|
||||||
mediaUrl?: string;
|
|
||||||
mediaType?: MediaType;
|
|
||||||
options?: QuestionOptionDto[];
|
|
||||||
correctAnswer?: string | string[];
|
|
||||||
points: number;
|
|
||||||
timeLimit?: number;
|
|
||||||
explanation?: string;
|
|
||||||
difficulty: QuestionDifficulty;
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface QuestionOptionDto extends FullAuditedEntityDto {
|
|
||||||
text: string;
|
|
||||||
isCorrect: boolean;
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface Exam {
|
|
||||||
id: string;
|
|
||||||
title: string;
|
|
||||||
description: string;
|
|
||||||
type: ExamType;
|
|
||||||
testDocument?: {
|
|
||||||
url: string;
|
|
||||||
type: TestType;
|
|
||||||
name: string;
|
|
||||||
};
|
|
||||||
answerKeyTemplate?: AnswerKeyItem[];
|
|
||||||
questions: QuestionDto[];
|
|
||||||
timeLimit: number;
|
|
||||||
totalPoints: number;
|
|
||||||
passingScore: number;
|
|
||||||
allowReview: boolean;
|
|
||||||
randomizeQuestions: boolean;
|
|
||||||
showResults: boolean;
|
|
||||||
maxAttempts: number;
|
|
||||||
startTime?: Date;
|
|
||||||
endTime?: Date;
|
|
||||||
isActive: boolean;
|
|
||||||
creationTime: Date;
|
|
||||||
lastModificationTime: Date;
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface AnswerKeyItem {
|
|
||||||
id: string;
|
|
||||||
questionNumber: number;
|
|
||||||
type: QuestionType;
|
|
||||||
options?: string[];
|
|
||||||
points: number;
|
|
||||||
correctAnswer?: string | string[];
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface StudentAnswer {
|
|
||||||
questionId: string;
|
|
||||||
answer: string | string[];
|
|
||||||
timeSpent: number;
|
|
||||||
isCorrect?: boolean;
|
|
||||||
points?: number;
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface ExamSession {
|
|
||||||
id: string;
|
|
||||||
examId: string;
|
|
||||||
studentId: string;
|
|
||||||
startTime: Date;
|
|
||||||
endTime?: Date;
|
|
||||||
answers: StudentAnswer[];
|
|
||||||
totalScore?: number;
|
|
||||||
status: ExamSessionStatus;
|
|
||||||
timeRemaining: number;
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface TagItem {
|
|
||||||
id: string;
|
|
||||||
name: string;
|
|
||||||
description: string;
|
|
||||||
color: string;
|
|
||||||
usageCount: number;
|
|
||||||
creationTime: Date;
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface NavigationItem {
|
|
||||||
id: string;
|
|
||||||
title: string;
|
|
||||||
icon: string;
|
|
||||||
path: string;
|
|
||||||
description?: string;
|
|
||||||
badge?: number;
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface AdminRoute {
|
|
||||||
path: string;
|
|
||||||
component: string;
|
|
||||||
title: string;
|
|
||||||
breadcrumb: string[];
|
|
||||||
}
|
|
||||||
|
|
@ -1,36 +0,0 @@
|
||||||
export enum LeaveStatusEnum {
|
|
||||||
// İzin Durumu
|
|
||||||
Pending = 'Pending', // Beklemede
|
|
||||||
Approved = 'Approved', // Onaylandı
|
|
||||||
Rejected = 'Rejected', // Reddedildi
|
|
||||||
Cancelled = 'Cancelled', // İptal edildi
|
|
||||||
}
|
|
||||||
|
|
||||||
export enum LeaveTypeEnum {
|
|
||||||
// İzin Türü
|
|
||||||
Annual = 'Annual', // Yıllık
|
|
||||||
Sick = 'Sick', // Hastalık
|
|
||||||
Maternity = 'Maternity', // Doğum
|
|
||||||
Paternity = 'Paternity', // Babalık
|
|
||||||
Personal = 'Personal', // Kişisel
|
|
||||||
Emergency = 'Emergency', // Acil
|
|
||||||
Study = 'Study', // Eğitim
|
|
||||||
Unpaid = 'Unpaid', // Ücretsiz
|
|
||||||
}
|
|
||||||
|
|
||||||
export enum PriorityEnum {
|
|
||||||
// Öncelik
|
|
||||||
Low = 'LOW', // Düşük
|
|
||||||
Normal = 'NORMAL', // Normal
|
|
||||||
High = 'HIGH', // Yüksek
|
|
||||||
Urgent = 'URGENT', // Acil
|
|
||||||
}
|
|
||||||
|
|
||||||
export enum TaskStatusEnum {
|
|
||||||
// Görev Durumu
|
|
||||||
NotStarted = 'NOT_STARTED', // Başlanmadı
|
|
||||||
InProgress = 'IN_PROGRESS', // Devam Ediyor
|
|
||||||
Completed = 'COMPLETED', // Tamamlandı
|
|
||||||
OnHold = 'ON_HOLD', // Beklemede
|
|
||||||
Cancelled = 'CANCELLED', // İptal Edildi
|
|
||||||
}
|
|
||||||
|
|
@ -1,32 +0,0 @@
|
||||||
const isNumString = (str: string): boolean => !isNaN(Number(str))
|
|
||||||
|
|
||||||
type JsonObject = { [key: string]: unknown }
|
|
||||||
type JsonArray = Array<unknown>
|
|
||||||
type Json = JsonObject | JsonArray | string | number | boolean | null
|
|
||||||
|
|
||||||
function deepParseJson(jsonString: Json): Json {
|
|
||||||
if (typeof jsonString === 'string') {
|
|
||||||
if (isNumString(jsonString)) {
|
|
||||||
return jsonString
|
|
||||||
}
|
|
||||||
try {
|
|
||||||
return deepParseJson(JSON.parse(jsonString))
|
|
||||||
} catch {
|
|
||||||
return jsonString
|
|
||||||
}
|
|
||||||
} else if (Array.isArray(jsonString)) {
|
|
||||||
return jsonString.map((val) => deepParseJson(val as JsonArray))
|
|
||||||
} else if (typeof jsonString === 'object' && jsonString !== null) {
|
|
||||||
return Object.keys(jsonString).reduce<JsonObject>((obj, key) => {
|
|
||||||
const val = jsonString[key]
|
|
||||||
obj[key] = isNumString(val as string)
|
|
||||||
? val
|
|
||||||
: deepParseJson(val as number)
|
|
||||||
return obj
|
|
||||||
}, {})
|
|
||||||
} else {
|
|
||||||
return jsonString
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
export default deepParseJson
|
|
||||||
|
|
@ -1,54 +0,0 @@
|
||||||
import { Role, RoleState, VideoroomDto } from '@/proxy/videoroom/models'
|
|
||||||
import { useStoreActions, useStoreState } from '@/store/store'
|
|
||||||
import { useState } from 'react'
|
|
||||||
|
|
||||||
export function useVideoroomLogic() {
|
|
||||||
const { user } = useStoreState((state) => state.auth)
|
|
||||||
const { setUser } = useStoreActions((actions) => actions.auth.user)
|
|
||||||
|
|
||||||
const [roleState, setRoleState] = useState<RoleState>('role-selection')
|
|
||||||
const [currentClass, setCurrentClass] = useState<VideoroomDto | null>(null)
|
|
||||||
const [allClasses, setAllClasses] = useState<VideoroomDto[]>([])
|
|
||||||
|
|
||||||
const handleRoleSelect = (role: Role) => {
|
|
||||||
setUser({
|
|
||||||
...user,
|
|
||||||
role,
|
|
||||||
})
|
|
||||||
setRoleState('dashboard')
|
|
||||||
}
|
|
||||||
|
|
||||||
const handleCreateClass = (classData: Partial<VideoroomDto>) => {
|
|
||||||
const newClass = {
|
|
||||||
...classData,
|
|
||||||
id: crypto.randomUUID(),
|
|
||||||
teacherId: '',
|
|
||||||
teacherName: '',
|
|
||||||
isActive: false,
|
|
||||||
isScheduled: true,
|
|
||||||
participantCount: 0,
|
|
||||||
} as VideoroomDto
|
|
||||||
setAllClasses((prev) => [...prev, newClass])
|
|
||||||
}
|
|
||||||
|
|
||||||
const handleEditClass = (classId: string, classData: Partial<VideoroomDto>) => {
|
|
||||||
setAllClasses((prev) => prev.map((c) => (c.id === classId ? { ...c, ...classData } : c)))
|
|
||||||
}
|
|
||||||
|
|
||||||
const handleDeleteClass = (classId: string) => {
|
|
||||||
setAllClasses((prev) => prev.filter((c) => c.id !== classId))
|
|
||||||
}
|
|
||||||
|
|
||||||
return {
|
|
||||||
roleState,
|
|
||||||
setRoleState,
|
|
||||||
currentClass,
|
|
||||||
setCurrentClass,
|
|
||||||
allClasses,
|
|
||||||
setAllClasses,
|
|
||||||
handleRoleSelect,
|
|
||||||
handleCreateClass,
|
|
||||||
handleEditClass,
|
|
||||||
handleDeleteClass,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
@ -1,130 +0,0 @@
|
||||||
import { QuestionPoolDto, Exam, TagItem, ExamSession } from "@/types/coordinator";
|
|
||||||
import { useState } from "react";
|
|
||||||
|
|
||||||
export function useCoordinator() {
|
|
||||||
const [currentPath, setCurrentPath] = useState("/admin/dashboard");
|
|
||||||
const [pools, setPools] = useState<QuestionPoolDto[]>([]);
|
|
||||||
const [exams, setExams] = useState<Exam[]>([]);
|
|
||||||
const [tags, setTags] = useState<TagItem[]>([]);
|
|
||||||
const [currentExam, setCurrentExam] = useState<Exam | null>(null);
|
|
||||||
|
|
||||||
const handleUpdateTest = (updatedTest: Exam) => {
|
|
||||||
setExams((prev) =>
|
|
||||||
prev.map((exam) => (exam.id === updatedTest.id ? updatedTest : exam))
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
const handleDeleteTest = (testId: string) => {
|
|
||||||
setExams((prev) => prev.filter((exam) => exam.id !== testId));
|
|
||||||
};
|
|
||||||
|
|
||||||
const handleCreatePool = (
|
|
||||||
poolData: Omit<QuestionPoolDto, "id" | "creationTime">
|
|
||||||
) => {
|
|
||||||
const newPool: QuestionPoolDto = {
|
|
||||||
...poolData,
|
|
||||||
id: `pool-${Date.now()}`,
|
|
||||||
creationTime: new Date(),
|
|
||||||
};
|
|
||||||
setPools((prev) => [...prev, newPool]);
|
|
||||||
};
|
|
||||||
|
|
||||||
const handleUpdatePool = (updatedPool: QuestionPoolDto) => {
|
|
||||||
setPools((prev) =>
|
|
||||||
prev.map((pool) => (pool.id === updatedPool.id ? updatedPool : pool))
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
const handleDeletePool = (poolId: string) => {
|
|
||||||
setPools((prev) => prev.filter((pool) => pool.id !== poolId));
|
|
||||||
};
|
|
||||||
|
|
||||||
const handleCreateExam = (
|
|
||||||
examData: Omit<Exam, "id" | "creationTime" | "lastModificationTime">
|
|
||||||
) => {
|
|
||||||
const newExam: Exam = {
|
|
||||||
...examData,
|
|
||||||
id: `exam-${Date.now()}`,
|
|
||||||
creationTime: new Date(),
|
|
||||||
lastModificationTime: new Date(),
|
|
||||||
};
|
|
||||||
setExams((prev) => [...prev, newExam]);
|
|
||||||
setCurrentPath("/admin/exams");
|
|
||||||
};
|
|
||||||
|
|
||||||
const handleSaveExam = (exam: Exam) => {
|
|
||||||
setExams((prev) => prev.map((e) => (e.id === exam.id ? exam : e)));
|
|
||||||
setCurrentPath("/admin/exams");
|
|
||||||
};
|
|
||||||
|
|
||||||
const handleCreateTest = (
|
|
||||||
testData: Omit<Exam, "id" | "creationTime" | "lastModificationTime">
|
|
||||||
) => {
|
|
||||||
const newTest: Exam = {
|
|
||||||
...testData,
|
|
||||||
id: `test-${Date.now()}`,
|
|
||||||
creationTime: new Date(),
|
|
||||||
lastModificationTime: new Date(),
|
|
||||||
};
|
|
||||||
setExams((prev) => [...prev, newTest]);
|
|
||||||
setCurrentPath("/admin/tests");
|
|
||||||
};
|
|
||||||
|
|
||||||
const handleCreateTag = (
|
|
||||||
tagData: Omit<TagItem, "id" | "usageCount" | "creationTime">
|
|
||||||
) => {
|
|
||||||
const newTag: TagItem = {
|
|
||||||
...tagData,
|
|
||||||
id: `tag-${Date.now()}`,
|
|
||||||
usageCount: 0,
|
|
||||||
creationTime: new Date(),
|
|
||||||
};
|
|
||||||
setTags((prev) => [...prev, newTag]);
|
|
||||||
};
|
|
||||||
|
|
||||||
const handleUpdateTag = (updatedTag: TagItem) => {
|
|
||||||
setTags((prev) =>
|
|
||||||
prev.map((tag) => (tag.id === updatedTag.id ? updatedTag : tag))
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
const handleDeleteTag = (tagId: string) => {
|
|
||||||
setTags((prev) => prev.filter((tag) => tag.id !== tagId));
|
|
||||||
};
|
|
||||||
|
|
||||||
const handleExamComplete = (session: ExamSession) => {
|
|
||||||
alert("Assessment completed successfully!");
|
|
||||||
setCurrentPath("/admin/dashboard");
|
|
||||||
setCurrentExam(null);
|
|
||||||
};
|
|
||||||
|
|
||||||
const startExam = (exam: Exam) => {
|
|
||||||
setCurrentExam(exam);
|
|
||||||
};
|
|
||||||
|
|
||||||
return {
|
|
||||||
currentPath,
|
|
||||||
setCurrentPath,
|
|
||||||
pools,
|
|
||||||
setPools,
|
|
||||||
exams,
|
|
||||||
setExams,
|
|
||||||
tags,
|
|
||||||
setTags,
|
|
||||||
currentExam,
|
|
||||||
setCurrentExam,
|
|
||||||
handleUpdateTest,
|
|
||||||
handleDeleteTest,
|
|
||||||
handleCreatePool,
|
|
||||||
handleUpdatePool,
|
|
||||||
handleDeletePool,
|
|
||||||
handleCreateExam,
|
|
||||||
handleSaveExam,
|
|
||||||
handleCreateTest,
|
|
||||||
handleCreateTag,
|
|
||||||
handleUpdateTag,
|
|
||||||
handleDeleteTag,
|
|
||||||
handleExamComplete,
|
|
||||||
startExam,
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
@ -1,95 +0,0 @@
|
||||||
import { useEffect, useCallback } from 'react';
|
|
||||||
|
|
||||||
interface SecurityConfig {
|
|
||||||
disableRightClick: boolean;
|
|
||||||
disableCopyPaste: boolean;
|
|
||||||
disableDevTools: boolean;
|
|
||||||
fullScreenMode: boolean;
|
|
||||||
preventTabSwitch: boolean;
|
|
||||||
}
|
|
||||||
|
|
||||||
export const useExamSecurity = (config: SecurityConfig, isActive: boolean = true) => {
|
|
||||||
const handleRightClick = useCallback((e: MouseEvent) => {
|
|
||||||
if (config.disableRightClick && isActive) {
|
|
||||||
e.preventDefault();
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
}, [config.disableRightClick, isActive]);
|
|
||||||
|
|
||||||
const handleKeyDown = useCallback((e: KeyboardEvent) => {
|
|
||||||
if (!isActive) return;
|
|
||||||
|
|
||||||
// Disable copy/paste shortcuts
|
|
||||||
if (config.disableCopyPaste) {
|
|
||||||
if (e.ctrlKey && (e.key === 'c' || e.key === 'v' || e.key === 'x' || e.key === 'a')) {
|
|
||||||
e.preventDefault();
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Disable developer tools
|
|
||||||
if (config.disableDevTools) {
|
|
||||||
if (e.key === 'F12' || (e.ctrlKey && e.shiftKey && e.key === 'I')) {
|
|
||||||
e.preventDefault();
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Disable Alt+Tab for tab switching
|
|
||||||
if (config.preventTabSwitch && e.altKey && e.key === 'Tab') {
|
|
||||||
e.preventDefault();
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
}, [config, isActive]);
|
|
||||||
|
|
||||||
const handleVisibilityChange = useCallback(() => {
|
|
||||||
if (config.preventTabSwitch && isActive && document.hidden) {
|
|
||||||
// Log tab switch attempt - in real app, this would call an API
|
|
||||||
console.warn('Tab switch detected during exam');
|
|
||||||
}
|
|
||||||
}, [config.preventTabSwitch, isActive]);
|
|
||||||
|
|
||||||
const enterFullScreen = useCallback(async () => {
|
|
||||||
if (config.fullScreenMode && isActive) {
|
|
||||||
try {
|
|
||||||
await document.documentElement.requestFullscreen();
|
|
||||||
} catch (error) {
|
|
||||||
console.error('Failed to enter fullscreen:', error);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}, [config.fullScreenMode, isActive]);
|
|
||||||
|
|
||||||
const exitFullScreen = useCallback(async () => {
|
|
||||||
if (document.fullscreenElement) {
|
|
||||||
try {
|
|
||||||
await document.exitFullscreen();
|
|
||||||
} catch (error) {
|
|
||||||
console.error('Failed to exit fullscreen:', error);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}, []);
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
if (isActive) {
|
|
||||||
document.addEventListener('contextmenu', handleRightClick);
|
|
||||||
document.addEventListener('keydown', handleKeyDown);
|
|
||||||
document.addEventListener('visibilitychange', handleVisibilityChange);
|
|
||||||
|
|
||||||
return () => {
|
|
||||||
document.removeEventListener('contextmenu', handleRightClick);
|
|
||||||
document.removeEventListener('keydown', handleKeyDown);
|
|
||||||
document.removeEventListener('visibilitychange', handleVisibilityChange);
|
|
||||||
|
|
||||||
if (config.fullScreenMode) {
|
|
||||||
exitFullScreen();
|
|
||||||
}
|
|
||||||
};
|
|
||||||
}
|
|
||||||
}, [handleRightClick, handleKeyDown, handleVisibilityChange, exitFullScreen, isActive, config.fullScreenMode]);
|
|
||||||
|
|
||||||
return {
|
|
||||||
enterFullScreen,
|
|
||||||
exitFullScreen,
|
|
||||||
isFullScreen: !!document.fullscreenElement
|
|
||||||
};
|
|
||||||
};
|
|
||||||
|
|
@ -1,94 +0,0 @@
|
||||||
import { useState, useEffect, useCallback } from 'react';
|
|
||||||
|
|
||||||
interface UseExamTimerProps {
|
|
||||||
initialTime: number; // in seconds
|
|
||||||
onTimeUp?: () => void;
|
|
||||||
onTick?: (timeRemaining: number) => void;
|
|
||||||
autoStart?: boolean;
|
|
||||||
}
|
|
||||||
|
|
||||||
export const useExamTimer = ({
|
|
||||||
initialTime,
|
|
||||||
onTimeUp,
|
|
||||||
onTick,
|
|
||||||
autoStart = false
|
|
||||||
}: UseExamTimerProps) => {
|
|
||||||
const [timeRemaining, setTimeRemaining] = useState(initialTime);
|
|
||||||
const [isRunning, setIsRunning] = useState(autoStart);
|
|
||||||
const [isPaused, setIsPaused] = useState(false);
|
|
||||||
|
|
||||||
const start = useCallback(() => {
|
|
||||||
setIsRunning(true);
|
|
||||||
setIsPaused(false);
|
|
||||||
}, []);
|
|
||||||
|
|
||||||
const pause = useCallback(() => {
|
|
||||||
setIsPaused(true);
|
|
||||||
}, []);
|
|
||||||
|
|
||||||
const resume = useCallback(() => {
|
|
||||||
setIsPaused(false);
|
|
||||||
}, []);
|
|
||||||
|
|
||||||
const reset = useCallback(() => {
|
|
||||||
setTimeRemaining(initialTime);
|
|
||||||
setIsRunning(false);
|
|
||||||
setIsPaused(false);
|
|
||||||
}, [initialTime]);
|
|
||||||
|
|
||||||
const stop = useCallback(() => {
|
|
||||||
setIsRunning(false);
|
|
||||||
setIsPaused(false);
|
|
||||||
}, []);
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
let interval: ReturnType<typeof setInterval>;
|
|
||||||
|
|
||||||
if (isRunning && !isPaused && timeRemaining > 0) {
|
|
||||||
interval = setInterval(() => {
|
|
||||||
setTimeRemaining((prev) => {
|
|
||||||
const newTime = prev - 1;
|
|
||||||
onTick?.(newTime);
|
|
||||||
|
|
||||||
if (newTime <= 0) {
|
|
||||||
setIsRunning(false);
|
|
||||||
onTimeUp?.();
|
|
||||||
return 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
return newTime;
|
|
||||||
});
|
|
||||||
}, 1000);
|
|
||||||
}
|
|
||||||
|
|
||||||
return () => {
|
|
||||||
if (interval) {
|
|
||||||
clearInterval(interval);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
}, [isRunning, isPaused, timeRemaining, onTimeUp, onTick]);
|
|
||||||
|
|
||||||
const formatTime = useCallback((seconds: number) => {
|
|
||||||
const hours = Math.floor(seconds / 3600);
|
|
||||||
const minutes = Math.floor((seconds % 3600) / 60);
|
|
||||||
const secs = seconds % 60;
|
|
||||||
|
|
||||||
if (hours > 0) {
|
|
||||||
return `${hours.toString().padStart(2, '0')}:${minutes.toString().padStart(2, '0')}:${secs.toString().padStart(2, '0')}`;
|
|
||||||
}
|
|
||||||
return `${minutes.toString().padStart(2, '0')}:${secs.toString().padStart(2, '0')}`;
|
|
||||||
}, []);
|
|
||||||
|
|
||||||
return {
|
|
||||||
timeRemaining,
|
|
||||||
formattedTime: formatTime(timeRemaining),
|
|
||||||
isRunning,
|
|
||||||
isPaused,
|
|
||||||
start,
|
|
||||||
pause,
|
|
||||||
resume,
|
|
||||||
reset,
|
|
||||||
stop,
|
|
||||||
progress: ((initialTime - timeRemaining) / initialTime) * 100
|
|
||||||
};
|
|
||||||
};
|
|
||||||
|
|
@ -1,27 +0,0 @@
|
||||||
import { Product } from '@/proxy/order/models'
|
|
||||||
import { useState, useEffect } from 'react'
|
|
||||||
|
|
||||||
export const useOrders = () => {
|
|
||||||
const [products, setProducts] = useState<Product[]>([])
|
|
||||||
const [loading, setLoading] = useState(true)
|
|
||||||
const [error, setError] = useState<string | null>(null)
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
fetchProducts()
|
|
||||||
}, [])
|
|
||||||
|
|
||||||
const fetchProducts = async () => {
|
|
||||||
try {
|
|
||||||
setLoading(true)
|
|
||||||
setProducts(products)
|
|
||||||
setError(null)
|
|
||||||
} catch (err) {
|
|
||||||
console.error('Products fetch error:', err)
|
|
||||||
setError('App.Orders.LoadProductsError')
|
|
||||||
} finally {
|
|
||||||
setLoading(false)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return { products, loading, error, refetch: fetchProducts }
|
|
||||||
}
|
|
||||||
|
|
@ -1,227 +0,0 @@
|
||||||
import { ReportGeneratedDto, ReportTemplateDto, ReportCategoryDto } from '@/proxy/reports/models'
|
|
||||||
import ReportsService from '@/services/reports.service'
|
|
||||||
import { useState, useCallback, useEffect } from 'react'
|
|
||||||
|
|
||||||
const reportsService = new ReportsService()
|
|
||||||
|
|
||||||
interface ReportData {
|
|
||||||
templates: ReportTemplateDto[]
|
|
||||||
generatedReports: ReportGeneratedDto[]
|
|
||||||
categories: ReportCategoryDto[]
|
|
||||||
}
|
|
||||||
|
|
||||||
export const useReports = () => {
|
|
||||||
const [data, setData] = useState<ReportData>({
|
|
||||||
templates: [],
|
|
||||||
generatedReports: [],
|
|
||||||
categories: [],
|
|
||||||
})
|
|
||||||
const [isLoading, setIsLoading] = useState(false)
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
const loadData = async () => {
|
|
||||||
setIsLoading(true)
|
|
||||||
try {
|
|
||||||
await new Promise((resolve) => setTimeout(resolve, 200))
|
|
||||||
|
|
||||||
const [templatesResponse, generatedReportsResponse, categoriesResponse] = await Promise.all(
|
|
||||||
[
|
|
||||||
reportsService.getTemplates({
|
|
||||||
sorting: '',
|
|
||||||
skipCount: 0,
|
|
||||||
maxResultCount: 1000,
|
|
||||||
}),
|
|
||||||
reportsService.getGeneratedReports({
|
|
||||||
sorting: '',
|
|
||||||
skipCount: 0,
|
|
||||||
maxResultCount: 1000,
|
|
||||||
}),
|
|
||||||
reportsService.getCategories(),
|
|
||||||
],
|
|
||||||
)
|
|
||||||
|
|
||||||
setData({
|
|
||||||
templates: templatesResponse.data.items || [],
|
|
||||||
generatedReports: generatedReportsResponse.data.items || [],
|
|
||||||
categories: categoriesResponse.data || [],
|
|
||||||
})
|
|
||||||
} catch (error) {
|
|
||||||
console.error('Error loading data:', error)
|
|
||||||
// Fallback to default data
|
|
||||||
setData({
|
|
||||||
templates: [],
|
|
||||||
generatedReports: [],
|
|
||||||
categories: [],
|
|
||||||
})
|
|
||||||
} finally {
|
|
||||||
setIsLoading(false)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
loadData()
|
|
||||||
}, [])
|
|
||||||
|
|
||||||
const createTemplate = useCallback(
|
|
||||||
async (template: ReportTemplateDto) => {
|
|
||||||
setIsLoading(true)
|
|
||||||
try {
|
|
||||||
const response = await reportsService.createTemplate(template as ReportTemplateDto)
|
|
||||||
const newTemplate = response.data as ReportTemplateDto
|
|
||||||
|
|
||||||
// Update local state
|
|
||||||
setData((prevData) => ({
|
|
||||||
...prevData,
|
|
||||||
templates: [...prevData.templates, newTemplate],
|
|
||||||
}))
|
|
||||||
|
|
||||||
return newTemplate
|
|
||||||
} catch (error) {
|
|
||||||
console.error('Error creating template:', error)
|
|
||||||
throw error
|
|
||||||
} finally {
|
|
||||||
setIsLoading(false)
|
|
||||||
}
|
|
||||||
},
|
|
||||||
[data],
|
|
||||||
)
|
|
||||||
|
|
||||||
const updateTemplate = useCallback(async (id: string, updates: Partial<ReportTemplateDto>) => {
|
|
||||||
setIsLoading(true)
|
|
||||||
try {
|
|
||||||
// First get the current template to merge with updates
|
|
||||||
const currentTemplateResponse = await reportsService.getTemplateById(id)
|
|
||||||
const currentTemplate = currentTemplateResponse.data as ReportTemplateDto
|
|
||||||
|
|
||||||
const updatedTemplate = { ...currentTemplate, ...updates }
|
|
||||||
await reportsService.updateTemplate(id, updatedTemplate)
|
|
||||||
|
|
||||||
// Update local state
|
|
||||||
setData((prevData) => ({
|
|
||||||
...prevData,
|
|
||||||
templates: prevData.templates.map((template) =>
|
|
||||||
template.id === id
|
|
||||||
? { ...template, ...updates, lastModificationTime: new Date().toISOString() }
|
|
||||||
: template,
|
|
||||||
),
|
|
||||||
}))
|
|
||||||
} catch (error) {
|
|
||||||
console.error('Error updating template:', error)
|
|
||||||
throw error
|
|
||||||
} finally {
|
|
||||||
setIsLoading(false)
|
|
||||||
}
|
|
||||||
}, [])
|
|
||||||
|
|
||||||
const deleteTemplate = useCallback(async (id: string) => {
|
|
||||||
setIsLoading(true)
|
|
||||||
try {
|
|
||||||
await reportsService.deleteTemplate(id)
|
|
||||||
|
|
||||||
// Update local state
|
|
||||||
setData((prevData) => ({
|
|
||||||
...prevData,
|
|
||||||
templates: prevData.templates.filter((template) => template.id !== id),
|
|
||||||
}))
|
|
||||||
} catch (error) {
|
|
||||||
console.error('Error deleting template:', error)
|
|
||||||
throw error
|
|
||||||
} finally {
|
|
||||||
setIsLoading(false)
|
|
||||||
}
|
|
||||||
}, [])
|
|
||||||
|
|
||||||
const generateReport = useCallback(
|
|
||||||
async (templateId: string, parameterValues: Record<string, string>) => {
|
|
||||||
setIsLoading(true)
|
|
||||||
try {
|
|
||||||
const reportData = {
|
|
||||||
templateId,
|
|
||||||
parameters: parameterValues,
|
|
||||||
}
|
|
||||||
|
|
||||||
const response = await reportsService.generateReport(reportData)
|
|
||||||
const report = response.data as ReportGeneratedDto
|
|
||||||
|
|
||||||
if (report) {
|
|
||||||
// Update local state
|
|
||||||
setData((prevData) => ({
|
|
||||||
...prevData,
|
|
||||||
generatedReports: [...prevData.generatedReports, report],
|
|
||||||
}))
|
|
||||||
}
|
|
||||||
|
|
||||||
return report
|
|
||||||
} catch (error) {
|
|
||||||
console.error('Error generating report:', error)
|
|
||||||
throw error
|
|
||||||
} finally {
|
|
||||||
setIsLoading(false)
|
|
||||||
}
|
|
||||||
},
|
|
||||||
[],
|
|
||||||
)
|
|
||||||
|
|
||||||
const getReportById = useCallback(
|
|
||||||
async (reportId: string) => {
|
|
||||||
try {
|
|
||||||
const response = await reportsService.getGeneratedReportById(reportId)
|
|
||||||
return response.data as ReportGeneratedDto
|
|
||||||
} catch (error) {
|
|
||||||
console.error('Error getting report by id:', error)
|
|
||||||
// Fallback to local data
|
|
||||||
return data.generatedReports.find((report) => report.id === reportId)
|
|
||||||
}
|
|
||||||
},
|
|
||||||
[data.generatedReports],
|
|
||||||
)
|
|
||||||
|
|
||||||
const getTemplateById = useCallback(
|
|
||||||
async (templateId: string) => {
|
|
||||||
try {
|
|
||||||
const response = await reportsService.getTemplateById(templateId)
|
|
||||||
return response.data as ReportTemplateDto
|
|
||||||
} catch (error) {
|
|
||||||
console.error('Error getting template by id:', error)
|
|
||||||
// Fallback to local data
|
|
||||||
return data.templates.find((template) => template.id === templateId)
|
|
||||||
}
|
|
||||||
},
|
|
||||||
[data.templates],
|
|
||||||
)
|
|
||||||
|
|
||||||
const loadTemplatesByCategory = useCallback(async (categoryId?: string) => {
|
|
||||||
setIsLoading(true)
|
|
||||||
try {
|
|
||||||
const templatesResponse = await reportsService.getTemplates({
|
|
||||||
sorting: '',
|
|
||||||
skipCount: 0,
|
|
||||||
maxResultCount: 1000,
|
|
||||||
categoryId: categoryId && categoryId !== 'Tümü' ? categoryId : undefined,
|
|
||||||
})
|
|
||||||
|
|
||||||
setData((prevData) => ({
|
|
||||||
...prevData,
|
|
||||||
templates: templatesResponse.data.items || [],
|
|
||||||
}))
|
|
||||||
} catch (error) {
|
|
||||||
console.error('Error loading templates by category:', error)
|
|
||||||
} finally {
|
|
||||||
setIsLoading(false)
|
|
||||||
}
|
|
||||||
}, [])
|
|
||||||
|
|
||||||
return {
|
|
||||||
templates: data.templates,
|
|
||||||
generatedReports: data.generatedReports,
|
|
||||||
categories: data.categories,
|
|
||||||
isLoading,
|
|
||||||
setIsLoading,
|
|
||||||
createTemplate,
|
|
||||||
updateTemplate,
|
|
||||||
deleteTemplate,
|
|
||||||
generateReport,
|
|
||||||
getReportById,
|
|
||||||
getTemplateById,
|
|
||||||
loadTemplatesByCategory,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
@ -1,6 +0,0 @@
|
||||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
||||||
const paginate = (array: Array<any>, pageSize: number, pageNumber: number) => {
|
|
||||||
return array.slice((pageNumber - 1) * pageSize, pageNumber * pageSize)
|
|
||||||
}
|
|
||||||
|
|
||||||
export default paginate
|
|
||||||
|
|
@ -1,11 +0,0 @@
|
||||||
/* eslint-disable @typescript-eslint/no-explicit-any */
|
|
||||||
export default function requiredFieldValidation(
|
|
||||||
value: any,
|
|
||||||
message: string
|
|
||||||
): string {
|
|
||||||
let validationMessage = ''
|
|
||||||
if (!value) {
|
|
||||||
validationMessage = message || 'Required'
|
|
||||||
}
|
|
||||||
return validationMessage
|
|
||||||
}
|
|
||||||
|
|
@ -1,20 +0,0 @@
|
||||||
const shadeColor = (color: string, percent: number) => {
|
|
||||||
let R = parseInt(color.substring(1, 3), 16)
|
|
||||||
let G = parseInt(color.substring(3, 5), 16)
|
|
||||||
let B = parseInt(color.substring(5, 7), 16)
|
|
||||||
R = (R * (100 + percent)) / 100
|
|
||||||
G = (G * (100 + percent)) / 100
|
|
||||||
B = (B * (100 + percent)) / 100
|
|
||||||
R = R < 255 ? R : 255
|
|
||||||
G = G < 255 ? G : 255
|
|
||||||
B = B < 255 ? B : 255
|
|
||||||
const RR =
|
|
||||||
R.toString(16).length === 1 ? `0${R.toString(16)}` : R.toString(16)
|
|
||||||
const GG =
|
|
||||||
G.toString(16).length === 1 ? `0${G.toString(16)}` : G.toString(16)
|
|
||||||
const BB =
|
|
||||||
B.toString(16).length === 1 ? `0${B.toString(16)}` : B.toString(16)
|
|
||||||
return `#${RR}${GG}${BB}`
|
|
||||||
}
|
|
||||||
|
|
||||||
export default shadeColor
|
|
||||||
|
|
@ -1,28 +0,0 @@
|
||||||
type Primitive = string | number | boolean
|
|
||||||
|
|
||||||
export type Primer = (value: Primitive) => Primitive
|
|
||||||
|
|
||||||
const sortBy = <T extends Record<string, Primitive>>(
|
|
||||||
field: keyof T,
|
|
||||||
reverse: boolean,
|
|
||||||
primer?: (value: Primitive) => Primitive
|
|
||||||
) => {
|
|
||||||
const key = primer
|
|
||||||
? function (x: T) {
|
|
||||||
return primer(x[field])
|
|
||||||
}
|
|
||||||
: function (x: T) {
|
|
||||||
return x[field]
|
|
||||||
}
|
|
||||||
const isReverse = !reverse ? 1 : -1
|
|
||||||
return function (a: T, b: T) {
|
|
||||||
const valueA = key(a)
|
|
||||||
const valueB = key(b)
|
|
||||||
if (typeof valueA === 'string' && typeof valueB === 'string') {
|
|
||||||
return isReverse * valueA.localeCompare(valueB)
|
|
||||||
}
|
|
||||||
return isReverse * (valueA > valueB ? 1 : valueB > valueA ? -1 : 0)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
export default sortBy
|
|
||||||
|
|
@ -1,24 +0,0 @@
|
||||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
||||||
export default function wildCardSearch(
|
|
||||||
list: Array<Record<string, string | number>>,
|
|
||||||
input: string,
|
|
||||||
specifyKey?: string
|
|
||||||
) {
|
|
||||||
const searchText = (item: Record<string, string | number>) => {
|
|
||||||
for (const key in item) {
|
|
||||||
if (item[specifyKey ? specifyKey : key] == null) {
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
if (
|
|
||||||
item[specifyKey ? specifyKey : key]
|
|
||||||
.toString()
|
|
||||||
.toUpperCase()
|
|
||||||
.indexOf(input.toString().toUpperCase()) !== -1
|
|
||||||
) {
|
|
||||||
return true
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
const result = list.filter((value) => searchText(value))
|
|
||||||
return result
|
|
||||||
}
|
|
||||||
|
|
@ -1,3 +0,0 @@
|
||||||
import Views from './Views'
|
|
||||||
|
|
||||||
export default Views
|
|
||||||
Loading…
Reference in a new issue