Genel düzeltmeler Responsive ve Seeder
This commit is contained in:
parent
a70d8650f1
commit
871ee34536
9 changed files with 248 additions and 78 deletions
|
|
@ -16733,8 +16733,8 @@
|
||||||
{
|
{
|
||||||
"resourceName": "Platform",
|
"resourceName": "Platform",
|
||||||
"key": "ListForms.Wizard.MenuInfo",
|
"key": "ListForms.Wizard.MenuInfo",
|
||||||
"en": "Menu Information",
|
"en": "Menu",
|
||||||
"tr": "Menü Bilgileri"
|
"tr": "Menü"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"resourceName": "Platform",
|
"resourceName": "Platform",
|
||||||
|
|
@ -16751,14 +16751,14 @@
|
||||||
{
|
{
|
||||||
"resourceName": "Platform",
|
"resourceName": "Platform",
|
||||||
"key": "ListForms.Wizard.ListFormSettings",
|
"key": "ListForms.Wizard.ListFormSettings",
|
||||||
"en": "List Form Settings",
|
"en": "Settings",
|
||||||
"tr": "Liste Formu Ayarları"
|
"tr": "Ayarlar"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"resourceName": "Platform",
|
"resourceName": "Platform",
|
||||||
"key": "ListForms.Wizard.ListFormFields",
|
"key": "ListForms.Wizard.ListFormFields",
|
||||||
"en": "List Form Fields",
|
"en": "Fields",
|
||||||
"tr": "Liste Formu Alanları"
|
"tr": "Alanlar"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"resourceName": "Platform",
|
"resourceName": "Platform",
|
||||||
|
|
|
||||||
|
|
@ -840,10 +840,10 @@ public class ListFormSeeder_Administration : IDataSeedContributor, ITransientDep
|
||||||
ColSpan = 4,
|
ColSpan = 4,
|
||||||
SqlQuery = @"
|
SqlQuery = @"
|
||||||
SELECT
|
SELECT
|
||||||
'Aktif' AS ""Title"",
|
N'Aktif' AS ""Title"",
|
||||||
COUNT(""Id"") AS ""Value"",
|
COUNT(""Id"") AS ""Value"",
|
||||||
'blue' AS ""Color"",
|
'blue' AS ""Color"",
|
||||||
'Aktif Kullanıcılar' AS ""SubTitle"",
|
N'Aktif Kullanıcılar' AS ""SubTitle"",
|
||||||
'FaUserCheck' AS ""Icon""
|
'FaUserCheck' AS ""Icon""
|
||||||
FROM ""AbpUsers""
|
FROM ""AbpUsers""
|
||||||
WHERE ""IsActive"" = 'true'
|
WHERE ""IsActive"" = 'true'
|
||||||
|
|
@ -851,10 +851,10 @@ public class ListFormSeeder_Administration : IDataSeedContributor, ITransientDep
|
||||||
UNION ALL
|
UNION ALL
|
||||||
|
|
||||||
SELECT
|
SELECT
|
||||||
'Pasif' AS ""Title"",
|
N'Pasif' AS ""Title"",
|
||||||
COUNT(""Id"") AS ""Value"",
|
COUNT(""Id"") AS ""Value"",
|
||||||
'green' AS ""Color"",
|
'green' AS ""Color"",
|
||||||
'Pasif Kullanıcılar' AS ""SubTitle"",
|
N'Pasif Kullanıcılar' AS ""SubTitle"",
|
||||||
'FaUserSlash' AS ""Icon""
|
'FaUserSlash' AS ""Icon""
|
||||||
FROM ""AbpUsers""
|
FROM ""AbpUsers""
|
||||||
WHERE ""IsActive"" = 'false'
|
WHERE ""IsActive"" = 'false'
|
||||||
|
|
@ -862,10 +862,10 @@ public class ListFormSeeder_Administration : IDataSeedContributor, ITransientDep
|
||||||
UNION ALL
|
UNION ALL
|
||||||
|
|
||||||
SELECT
|
SELECT
|
||||||
'Doğrulama' AS ""Title"",
|
N'Doğrulama' AS ""Title"",
|
||||||
COUNT(""Id"") AS ""Value"",
|
COUNT(""Id"") AS ""Value"",
|
||||||
'purple' AS ""Color"",
|
'purple' AS ""Color"",
|
||||||
'Yönetici Doğrulaması bekleyenler' AS ""SubTitle"",
|
N'Doğrulama bekleyenler' AS ""SubTitle"",
|
||||||
'FaUserClock' AS ""Icon""
|
'FaUserClock' AS ""Icon""
|
||||||
FROM ""AbpUsers""
|
FROM ""AbpUsers""
|
||||||
WHERE ""IsVerified"" = 'false';
|
WHERE ""IsVerified"" = 'false';
|
||||||
|
|
|
||||||
|
|
@ -77,7 +77,7 @@ export default function Widget({
|
||||||
>
|
>
|
||||||
<div className="flex items-center justify-between">
|
<div className="flex items-center justify-between">
|
||||||
<div>
|
<div>
|
||||||
<p className="text-sm font-semibold text-gray-600 uppercase tracking-wide">{title}</p>
|
<p className="text-sm font-semibold text-gray-600">{title}</p>
|
||||||
<p className={`${valueClassName} font-bold mt-1 ${colorMap[safeColor].text}`}>{value}</p>
|
<p className={`${valueClassName} font-bold mt-1 ${colorMap[safeColor].text}`}>{value}</p>
|
||||||
<p className="text-sm text-gray-500 mt-1">{subTitle}</p>
|
<p className="text-sm text-gray-500 mt-1">{subTitle}</p>
|
||||||
</div>
|
</div>
|
||||||
|
|
|
||||||
|
|
@ -8,13 +8,25 @@ export default function WidgetGroup({ widgetGroups }: { widgetGroups: WidgetGrou
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div>
|
<div>
|
||||||
|
{/* Mobile responsive override: below sm breakpoint each widget spans 6/12 (2 per row) */}
|
||||||
|
<style>{`
|
||||||
|
@media (max-width: 639px) {
|
||||||
|
.widget-item { grid-column: span 6 / span 6 !important; }
|
||||||
|
}
|
||||||
|
`}</style>
|
||||||
|
|
||||||
{widgetGroups.map((group, gIdx) => (
|
{widgetGroups.map((group, gIdx) => (
|
||||||
<div
|
<div
|
||||||
key={gIdx}
|
key={gIdx}
|
||||||
className={classNames(`grid grid-cols-12 gap-${group.colGap} ${group.className || ''}`)}
|
className={classNames(`grid gap-${group.colGap} ${group.className || ''}`)}
|
||||||
|
style={{ gridTemplateColumns: 'repeat(12, minmax(0, 1fr))' }}
|
||||||
>
|
>
|
||||||
{group.items.map((item: WidgetEditDto, order: number) => (
|
{group.items.map((item: WidgetEditDto, order: number) => (
|
||||||
<div key={`${gIdx}-${order}`} className={classNames(`col-span-${group.colSpan}`)}>
|
<div
|
||||||
|
key={`${gIdx}-${order}`}
|
||||||
|
className="widget-item min-w-0"
|
||||||
|
style={{ gridColumn: `span ${group.colSpan} / span ${group.colSpan}` }}
|
||||||
|
>
|
||||||
<Widget
|
<Widget
|
||||||
title={item.title}
|
title={item.title}
|
||||||
value={item.value}
|
value={item.value}
|
||||||
|
|
@ -36,3 +48,4 @@ export default function WidgetGroup({ widgetGroups }: { widgetGroups: WidgetGrou
|
||||||
</div>
|
</div>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -452,7 +452,7 @@ function OrgChartNode({
|
||||||
data-card=""
|
data-card=""
|
||||||
data-depth={depth}
|
data-depth={depth}
|
||||||
data-users-visible={showUsers ? 'true' : 'false'}
|
data-users-visible={showUsers ? 'true' : 'false'}
|
||||||
className={`relative bg-white border ${borderColor} rounded-xl shadow-sm w-52 hover:shadow-md transition-shadow`}
|
className={`relative bg-white border ${borderColor} rounded-xl shadow-sm w-36 sm:w-44 md:w-52 hover:shadow-md transition-shadow`}
|
||||||
style={{ cursor: dragging ? 'grabbing' : 'grab' }}
|
style={{ cursor: dragging ? 'grabbing' : 'grab' }}
|
||||||
>
|
>
|
||||||
{/* Header bar */}
|
{/* Header bar */}
|
||||||
|
|
@ -522,7 +522,7 @@ function OrgChartNode({
|
||||||
|
|
||||||
{/* Children */}
|
{/* Children */}
|
||||||
{hasChildren && !collapsed && (
|
{hasChildren && !collapsed && (
|
||||||
<div className="mt-10 flex items-start gap-10">
|
<div className="mt-6 sm:mt-8 md:mt-10 flex items-start gap-4 sm:gap-6 md:gap-10">
|
||||||
{node.children.map((child, idx) => {
|
{node.children.map((child, idx) => {
|
||||||
const childPosition = positions[child.id] ?? { x: 0, y: 0 }
|
const childPosition = positions[child.id] ?? { x: 0, y: 0 }
|
||||||
|
|
||||||
|
|
@ -695,7 +695,7 @@ function OrgChartTree({
|
||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div ref={containerRef} className="relative p-6">
|
<div ref={containerRef} className="relative p-3 sm:p-4 md:p-6">
|
||||||
<svg className="pointer-events-none absolute inset-0 w-full h-full overflow-visible">
|
<svg className="pointer-events-none absolute inset-0 w-full h-full overflow-visible">
|
||||||
{paths.map((path, index) => (
|
{paths.map((path, index) => (
|
||||||
<path
|
<path
|
||||||
|
|
@ -710,7 +710,7 @@ function OrgChartTree({
|
||||||
))}
|
))}
|
||||||
</svg>
|
</svg>
|
||||||
|
|
||||||
<div className="relative z-10 flex gap-8 justify-center flex-wrap items-start">
|
<div className="relative z-10 flex gap-4 sm:gap-6 md:gap-8 justify-center flex-wrap items-start">
|
||||||
{nodes.map((root) => (
|
{nodes.map((root) => (
|
||||||
<OrgChartNode
|
<OrgChartNode
|
||||||
key={root.id}
|
key={root.id}
|
||||||
|
|
@ -804,73 +804,73 @@ const OrgChart = () => {
|
||||||
|
|
||||||
<div className="flex flex-col h-full">
|
<div className="flex flex-col h-full">
|
||||||
{/* Toolbar */}
|
{/* Toolbar */}
|
||||||
<div className="flex items-center justify-between pb-1 border-b">
|
<div className="flex flex-col sm:flex-row sm:items-center sm:justify-between gap-2 pb-1 border-b">
|
||||||
<div className="flex items-center gap-3">
|
<div className="flex items-center gap-3 min-w-0">
|
||||||
{MenuIcon}
|
{MenuIcon}
|
||||||
<h4 className="text-sm font-medium">
|
<h4 className="text-sm font-medium truncate">
|
||||||
{translate('::App.Definitions.OrgChart') || 'Organizasyon Şeması'}
|
{translate('::App.Definitions.OrgChart') || 'Organizasyon Şeması'}
|
||||||
</h4>
|
</h4>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="flex items-center gap-2">
|
<div className="flex flex-wrap items-center gap-2">
|
||||||
<div className="flex items-center bg-slate-100 rounded-lg p-1 gap-1">
|
<div className="flex items-center bg-slate-100 rounded-lg p-1 gap-1">
|
||||||
<button
|
<button
|
||||||
onClick={() => setMode('department')}
|
onClick={() => setMode('department')}
|
||||||
className={`flex items-center gap-2 px-3 py-1.5 rounded-md text-sm font-medium transition-colors ${
|
className={`flex items-center gap-1.5 px-2 py-1.5 rounded-md text-xs sm:text-sm font-medium transition-colors ${
|
||||||
mode === 'department'
|
mode === 'department'
|
||||||
? 'bg-blue-600 text-white shadow-sm'
|
? 'bg-blue-600 text-white shadow-sm'
|
||||||
: 'text-slate-600 hover:text-slate-800'
|
: 'text-slate-600 hover:text-slate-800'
|
||||||
}`}
|
}`}
|
||||||
>
|
>
|
||||||
<FaBuilding className="w-3.5 h-3.5" />
|
<FaBuilding className="w-3.5 h-3.5 flex-shrink-0" />
|
||||||
{translate('::App.Hr.Department') || 'Departman'}
|
<span className="hidden sm:inline">{translate('::App.Hr.Department') || 'Departman'}</span>
|
||||||
</button>
|
</button>
|
||||||
<button
|
<button
|
||||||
onClick={() => setMode('jobPosition')}
|
onClick={() => setMode('jobPosition')}
|
||||||
className={`flex items-center gap-2 px-3 py-1.5 rounded-md text-sm font-medium transition-colors ${
|
className={`flex items-center gap-1.5 px-2 py-1.5 rounded-md text-xs sm:text-sm font-medium transition-colors ${
|
||||||
mode === 'jobPosition'
|
mode === 'jobPosition'
|
||||||
? 'bg-purple-600 text-white shadow-sm'
|
? 'bg-purple-600 text-white shadow-sm'
|
||||||
: 'text-slate-600 hover:text-slate-800'
|
: 'text-slate-600 hover:text-slate-800'
|
||||||
}`}
|
}`}
|
||||||
>
|
>
|
||||||
<FaBriefcase className="w-3.5 h-3.5" />
|
<FaBriefcase className="w-3.5 h-3.5 flex-shrink-0" />
|
||||||
{translate('::App.Hr.JobPosition') || 'Pozisyon'}
|
<span className="hidden sm:inline">{translate('::App.Hr.JobPosition') || 'Pozisyon'}</span>
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="flex items-center bg-slate-100 rounded-lg p-1 gap-1">
|
<div className="flex items-center bg-slate-100 rounded-lg p-1 gap-1">
|
||||||
<label className="flex items-center gap-2 px-3 py-1.5 rounded-md text-sm text-slate-600 select-none">
|
<label className="flex items-center gap-2 px-2 py-1.5 rounded-md text-xs sm:text-sm text-slate-600 select-none cursor-pointer">
|
||||||
<input
|
<input
|
||||||
type="checkbox"
|
type="checkbox"
|
||||||
checked={showUsers}
|
checked={showUsers}
|
||||||
onChange={(e) => setShowUsers(e.target.checked)}
|
onChange={(e) => setShowUsers(e.target.checked)}
|
||||||
className="h-4 w-4 rounded border-slate-300 text-blue-600 focus:ring-blue-500"
|
className="h-4 w-4 rounded border-slate-300 text-blue-600 focus:ring-blue-500"
|
||||||
/>
|
/>
|
||||||
<span>{translate('::App.Definitions.OrgChart.ShowUsers') || 'Kullanıcılar'}</span>
|
<span className="hidden sm:inline">{translate('::App.Definitions.OrgChart.ShowUsers') || 'Kullanıcılar'}</span>
|
||||||
</label>
|
</label>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="flex items-center bg-slate-100 rounded-lg p-1 gap-1">
|
<div className="flex items-center bg-slate-100 rounded-lg p-1 gap-1">
|
||||||
<button
|
<button
|
||||||
onClick={handleZoomOut}
|
onClick={handleZoomOut}
|
||||||
className="p-2 rounded-md text-slate-600 hover:text-slate-800 hover:bg-white"
|
className="p-1.5 sm:p-2 rounded-md text-slate-600 hover:text-slate-800 hover:bg-white"
|
||||||
title="Zoom Out"
|
title="Zoom Out"
|
||||||
>
|
>
|
||||||
<FaSearchMinus className="w-3.5 h-3.5" />
|
<FaSearchMinus className="w-3.5 h-3.5" />
|
||||||
</button>
|
</button>
|
||||||
<span className="text-xs font-medium text-slate-600 px-1 min-w-[46px] text-center">
|
<span className="text-xs font-medium text-slate-600 px-1 min-w-[36px] sm:min-w-[46px] text-center">
|
||||||
{Math.round(zoom * 100)}%
|
{Math.round(zoom * 100)}%
|
||||||
</span>
|
</span>
|
||||||
<button
|
<button
|
||||||
onClick={handleZoomIn}
|
onClick={handleZoomIn}
|
||||||
className="p-2 rounded-md text-slate-600 hover:text-slate-800 hover:bg-white"
|
className="p-1.5 sm:p-2 rounded-md text-slate-600 hover:text-slate-800 hover:bg-white"
|
||||||
title="Zoom In"
|
title="Zoom In"
|
||||||
>
|
>
|
||||||
<FaSearchPlus className="w-3.5 h-3.5" />
|
<FaSearchPlus className="w-3.5 h-3.5" />
|
||||||
</button>
|
</button>
|
||||||
<button
|
<button
|
||||||
onClick={handleZoomReset}
|
onClick={handleZoomReset}
|
||||||
className="p-2 rounded-md text-slate-600 hover:text-slate-800 hover:bg-white"
|
className="p-1.5 sm:p-2 rounded-md text-slate-600 hover:text-slate-800 hover:bg-white"
|
||||||
title="Reset Zoom"
|
title="Reset Zoom"
|
||||||
>
|
>
|
||||||
<FaUndo className="w-3.5 h-3.5" />
|
<FaUndo className="w-3.5 h-3.5" />
|
||||||
|
|
@ -880,11 +880,11 @@ const OrgChart = () => {
|
||||||
<button
|
<button
|
||||||
onClick={handleExportJpg}
|
onClick={handleExportJpg}
|
||||||
disabled={exporting || loading}
|
disabled={exporting || loading}
|
||||||
className="flex items-center gap-2 px-3 py-1.5 rounded-lg text-sm font-medium bg-slate-100 text-slate-600 hover:bg-slate-200 disabled:opacity-50 transition-colors"
|
className="flex items-center gap-1.5 px-2 py-1.5 rounded-lg text-xs sm:text-sm font-medium bg-slate-100 text-slate-600 hover:bg-slate-200 disabled:opacity-50 transition-colors"
|
||||||
title="JPG olarak indir"
|
title="JPG olarak indir"
|
||||||
>
|
>
|
||||||
<FaFileImage className="w-3.5 h-3.5" />
|
<FaFileImage className="w-3.5 h-3.5 flex-shrink-0" />
|
||||||
{exporting ? 'İşleniyor…' : 'Export'}
|
<span className="hidden sm:inline">{exporting ? 'İşleniyor…' : 'Export'}</span>
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
|
||||||
|
|
@ -109,21 +109,23 @@ const WizardFileManager = () => {
|
||||||
{/* ── Header ─────────────────────────────────────────────── */}
|
{/* ── Header ─────────────────────────────────────────────── */}
|
||||||
<div
|
<div
|
||||||
className={classNames(
|
className={classNames(
|
||||||
'flex items-center gap-2 pb-1 border-b',
|
'flex flex-col sm:flex-row sm:items-center gap-2 pb-1 border-b',
|
||||||
mode === 'light' ? 'border-gray-200' : 'border-neutral-700',
|
mode === 'light' ? 'border-gray-200' : 'border-neutral-700',
|
||||||
)}
|
)}
|
||||||
>
|
>
|
||||||
{MenuIcon}
|
<div className="flex items-center gap-2 min-w-0">
|
||||||
<h4 className="text-sm font-medium">
|
{MenuIcon}
|
||||||
{translate('::App.Listforms.WizardManager') || 'Wizard Seed Dosyaları'}
|
<h4 className="text-sm font-medium truncate">
|
||||||
</h4>
|
{translate('::App.Listforms.WizardManager') || 'Wizard Seed Dosyaları'}
|
||||||
|
</h4>
|
||||||
|
</div>
|
||||||
|
|
||||||
<div className="flex gap-1 ml-auto items-center">
|
<div className="flex flex-wrap gap-1 sm:ml-auto items-center">
|
||||||
<div className="relative">
|
<div className="relative">
|
||||||
<FaSearch className="absolute left-2 top-1/2 -translate-y-1/2 text-gray-400 text-xs pointer-events-none" />
|
<FaSearch className="absolute left-2 top-1/2 -translate-y-1/2 text-gray-400 text-xs pointer-events-none" />
|
||||||
<Input
|
<Input
|
||||||
size="sm"
|
size="sm"
|
||||||
className="pl-6 w-44"
|
className="pl-6 w-36 sm:w-44"
|
||||||
placeholder={translate('::App.Platform.Search') || 'Search...'}
|
placeholder={translate('::App.Platform.Search') || 'Search...'}
|
||||||
value={search}
|
value={search}
|
||||||
onChange={(e) => setSearch(e.target.value)}
|
onChange={(e) => setSearch(e.target.value)}
|
||||||
|
|
@ -145,7 +147,7 @@ const WizardFileManager = () => {
|
||||||
onClick={() => setShowDbMigrateDialog(true)}
|
onClick={() => setShowDbMigrateDialog(true)}
|
||||||
title={translate('::App.DbMigrate.StartMessage') || 'Run DB Migration'}
|
title={translate('::App.DbMigrate.StartMessage') || 'Run DB Migration'}
|
||||||
>
|
>
|
||||||
{translate('::ListForms.ListForm.DbMigrate') || 'DB Migrate'}
|
<span className="hidden sm:inline">{translate('::ListForms.ListForm.DbMigrate') || 'DB Migrate'}</span>
|
||||||
</Button>
|
</Button>
|
||||||
<Button
|
<Button
|
||||||
size="sm"
|
size="sm"
|
||||||
|
|
@ -154,7 +156,7 @@ const WizardFileManager = () => {
|
||||||
className="flex items-center"
|
className="flex items-center"
|
||||||
>
|
>
|
||||||
<FaPlus className="mr-1" />
|
<FaPlus className="mr-1" />
|
||||||
{translate('::ListForms.Wizard.AddNewRecord') || 'Add New Record'}
|
<span className="hidden sm:inline">{translate('::ListForms.Wizard.AddNewRecord') || 'Add New Record'}</span>
|
||||||
</Button>
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
@ -181,7 +183,7 @@ const WizardFileManager = () => {
|
||||||
return (
|
return (
|
||||||
<div
|
<div
|
||||||
key={f.fileName}
|
key={f.fileName}
|
||||||
className="flex items-center justify-between p-3 rounded-lg border border-gray-200 dark:border-gray-700 bg-gray-50 dark:bg-gray-800"
|
className="flex flex-col sm:flex-row sm:items-center sm:justify-between gap-2 p-3 rounded-lg border border-gray-200 dark:border-gray-700 bg-gray-50 dark:bg-gray-800"
|
||||||
>
|
>
|
||||||
<div className="flex items-center gap-3 min-w-0">
|
<div className="flex items-center gap-3 min-w-0">
|
||||||
<Icon className="text-indigo-400 shrink-0 text-3xl" />
|
<Icon className="text-indigo-400 shrink-0 text-3xl" />
|
||||||
|
|
@ -197,7 +199,7 @@ const WizardFileManager = () => {
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="flex items-center gap-2 shrink-0 ml-3">
|
<div className="flex items-center gap-2 shrink-0 sm:ml-3">
|
||||||
{!f.hasInsertedRecords && (
|
{!f.hasInsertedRecords && (
|
||||||
<span
|
<span
|
||||||
title="Bu dosyada izlenen kayıt bilgisi yok. Eski format olabilir."
|
title="Bu dosyada izlenen kayıt bilgisi yok. Eski format olabilir."
|
||||||
|
|
|
||||||
|
|
@ -1,7 +1,7 @@
|
||||||
import React, { useEffect } from 'react'
|
import React, { useEffect, useState } from 'react'
|
||||||
import { useLocalization } from '@/utils/hooks/useLocalization'
|
import { useLocalization } from '@/utils/hooks/useLocalization'
|
||||||
import { motion } from 'framer-motion'
|
import { motion, AnimatePresence } from 'framer-motion'
|
||||||
import { FaTimes, FaEye, FaClipboard } from 'react-icons/fa'
|
import { FaTimes, FaEye, FaClipboard, FaChevronLeft, FaChevronRight } from 'react-icons/fa'
|
||||||
import { AnnouncementDto } from '@/proxy/intranet/models'
|
import { AnnouncementDto } from '@/proxy/intranet/models'
|
||||||
import useLocale from '@/utils/hooks/useLocale'
|
import useLocale from '@/utils/hooks/useLocale'
|
||||||
import { currentLocalDate } from '@/utils/dateUtils'
|
import { currentLocalDate } from '@/utils/dateUtils'
|
||||||
|
|
@ -17,11 +17,34 @@ interface AnnouncementModalProps {
|
||||||
const AnnouncementModal: React.FC<AnnouncementModalProps> = ({ announcement, onClose }) => {
|
const AnnouncementModal: React.FC<AnnouncementModalProps> = ({ announcement, onClose }) => {
|
||||||
const { translate } = useLocalization()
|
const { translate } = useLocalization()
|
||||||
const currentLocale = useLocale()
|
const currentLocale = useLocale()
|
||||||
|
const [lightboxOpen, setLightboxOpen] = useState(false)
|
||||||
|
const [lightboxIndex, setLightboxIndex] = useState(0)
|
||||||
|
|
||||||
|
const openLightbox = (idx: number) => {
|
||||||
|
setLightboxIndex(idx)
|
||||||
|
setLightboxOpen(true)
|
||||||
|
}
|
||||||
|
const closeLightbox = () => setLightboxOpen(false)
|
||||||
|
const prevLightbox = (e: React.MouseEvent) => {
|
||||||
|
e.stopPropagation()
|
||||||
|
setLightboxIndex((i) => (i - 1 + images.length) % images.length)
|
||||||
|
}
|
||||||
|
const nextLightbox = (e: React.MouseEvent) => {
|
||||||
|
e.stopPropagation()
|
||||||
|
setLightboxIndex((i) => (i + 1) % images.length)
|
||||||
|
}
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
intranetService.incrementAnnouncementViewCount(announcement.id)
|
intranetService.incrementAnnouncementViewCount(announcement.id)
|
||||||
}, [announcement.id])
|
}, [announcement.id])
|
||||||
|
|
||||||
|
const images = announcement.imageUrl ? announcement.imageUrl.split('|').filter(Boolean) : []
|
||||||
|
|
||||||
|
const imgSrc = (img: string) =>
|
||||||
|
img.startsWith('data:') || img.startsWith('http://') || img.startsWith('https://') || img.startsWith('/')
|
||||||
|
? img
|
||||||
|
: `data:image/jpeg;base64,${img}`
|
||||||
|
|
||||||
const getCategoryColor = (category: string) => {
|
const getCategoryColor = (category: string) => {
|
||||||
const colors: Record<string, string> = {
|
const colors: Record<string, string> = {
|
||||||
general: 'bg-blue-100 dark:bg-blue-900/30 text-blue-700 dark:text-blue-300',
|
general: 'bg-blue-100 dark:bg-blue-900/30 text-blue-700 dark:text-blue-300',
|
||||||
|
|
@ -114,10 +137,8 @@ const AnnouncementModal: React.FC<AnnouncementModalProps> = ({ announcement, onC
|
||||||
{/* Content */}
|
{/* Content */}
|
||||||
<div className="p-6 max-h-[60vh] overflow-y-auto">
|
<div className="p-6 max-h-[60vh] overflow-y-auto">
|
||||||
{/* Images if exist */}
|
{/* Images if exist */}
|
||||||
{announcement.imageUrl &&
|
{images.length > 0 &&
|
||||||
(() => {
|
(() => {
|
||||||
const images = announcement.imageUrl.split('|').filter(Boolean)
|
|
||||||
if (images.length === 0) return null
|
|
||||||
const getGridClass = (count: number) => {
|
const getGridClass = (count: number) => {
|
||||||
if (count === 1) return ''
|
if (count === 1) return ''
|
||||||
if (count === 2) return 'grid grid-cols-2 gap-2'
|
if (count === 2) return 'grid grid-cols-2 gap-2'
|
||||||
|
|
@ -135,16 +156,10 @@ const AnnouncementModal: React.FC<AnnouncementModalProps> = ({ announcement, onC
|
||||||
{images.map((img, idx) => (
|
{images.map((img, idx) => (
|
||||||
<img
|
<img
|
||||||
key={idx}
|
key={idx}
|
||||||
src={
|
src={imgSrc(img)}
|
||||||
img.startsWith('data:') ||
|
|
||||||
img.startsWith('http://') ||
|
|
||||||
img.startsWith('https://') ||
|
|
||||||
img.startsWith('/')
|
|
||||||
? img
|
|
||||||
: `data:image/jpeg;base64,${img}`
|
|
||||||
}
|
|
||||||
alt={`${announcement.title} ${images.length > 1 ? idx + 1 : ''}`.trim()}
|
alt={`${announcement.title} ${images.length > 1 ? idx + 1 : ''}`.trim()}
|
||||||
className={getImgClass(images.length, idx)}
|
className={`${getImgClass(images.length, idx)} cursor-zoom-in hover:opacity-90 transition-opacity`}
|
||||||
|
onClick={() => openLightbox(idx)}
|
||||||
/>
|
/>
|
||||||
))}
|
))}
|
||||||
</div>
|
</div>
|
||||||
|
|
@ -236,6 +251,71 @@ const AnnouncementModal: React.FC<AnnouncementModalProps> = ({ announcement, onC
|
||||||
</div>
|
</div>
|
||||||
</motion.div>
|
</motion.div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
{/* Lightbox */}
|
||||||
|
<AnimatePresence>
|
||||||
|
{lightboxOpen && (
|
||||||
|
<>
|
||||||
|
<motion.div
|
||||||
|
initial={{ opacity: 0 }}
|
||||||
|
animate={{ opacity: 1 }}
|
||||||
|
exit={{ opacity: 0 }}
|
||||||
|
className="fixed inset-0 bg-black/95 z-[60]"
|
||||||
|
onClick={closeLightbox}
|
||||||
|
/>
|
||||||
|
<div className="fixed inset-0 z-[70] flex items-center justify-center">
|
||||||
|
<button
|
||||||
|
onClick={closeLightbox}
|
||||||
|
className="absolute top-4 right-4 p-2 text-white hover:text-gray-300 transition-colors z-10"
|
||||||
|
>
|
||||||
|
<FaTimes className="w-8 h-8" />
|
||||||
|
</button>
|
||||||
|
{images.length > 1 && (
|
||||||
|
<>
|
||||||
|
<button
|
||||||
|
onClick={prevLightbox}
|
||||||
|
className="absolute left-4 top-1/2 -translate-y-1/2 p-3 bg-black/50 hover:bg-black/70 text-white rounded-full transition-colors z-10"
|
||||||
|
>
|
||||||
|
<FaChevronLeft className="w-6 h-6" />
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
onClick={nextLightbox}
|
||||||
|
className="absolute right-4 top-1/2 -translate-y-1/2 p-3 bg-black/50 hover:bg-black/70 text-white rounded-full transition-colors z-10"
|
||||||
|
>
|
||||||
|
<FaChevronRight className="w-6 h-6" />
|
||||||
|
</button>
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
<motion.img
|
||||||
|
key={lightboxIndex}
|
||||||
|
initial={{ opacity: 0, scale: 0.9 }}
|
||||||
|
animate={{ opacity: 1, scale: 1 }}
|
||||||
|
exit={{ opacity: 0 }}
|
||||||
|
src={imgSrc(images[lightboxIndex])}
|
||||||
|
alt={`${announcement.title} ${lightboxIndex + 1}`}
|
||||||
|
className="max-w-[90vw] max-h-[90vh] object-contain rounded-lg"
|
||||||
|
onClick={(e) => e.stopPropagation()}
|
||||||
|
/>
|
||||||
|
{images.length > 1 && (
|
||||||
|
<div className="absolute bottom-4 left-1/2 -translate-x-1/2 flex gap-2">
|
||||||
|
{images.map((_, idx) => (
|
||||||
|
<button
|
||||||
|
key={idx}
|
||||||
|
onClick={(e) => {
|
||||||
|
e.stopPropagation()
|
||||||
|
setLightboxIndex(idx)
|
||||||
|
}}
|
||||||
|
className={`w-2.5 h-2.5 rounded-full transition-all ${
|
||||||
|
idx === lightboxIndex ? 'bg-white' : 'bg-white/40 hover:bg-white/70'
|
||||||
|
}`}
|
||||||
|
/>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
</AnimatePresence>
|
||||||
</>
|
</>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -65,6 +65,73 @@ const cellTemplateMultiValue = (
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Hover preview overlay — singleton, tüm grid hücreleri tarafından paylaşılır
|
||||||
|
let __imgPreviewEl: HTMLDivElement | null = null
|
||||||
|
|
||||||
|
function getImgPreview(): HTMLDivElement {
|
||||||
|
if (!__imgPreviewEl) {
|
||||||
|
const el = document.createElement('div')
|
||||||
|
el.id = '__cellImgPreview'
|
||||||
|
el.style.cssText = [
|
||||||
|
'position:fixed',
|
||||||
|
'z-index:99999',
|
||||||
|
'display:none',
|
||||||
|
'pointer-events:none',
|
||||||
|
'background:#fff',
|
||||||
|
'border:1px solid #d1d5db',
|
||||||
|
'border-radius:8px',
|
||||||
|
'box-shadow:0 8px 32px rgba(0,0,0,0.22)',
|
||||||
|
'padding:4px',
|
||||||
|
'max-width:320px',
|
||||||
|
'max-height:320px',
|
||||||
|
'overflow:hidden',
|
||||||
|
'transition:opacity 0.15s ease',
|
||||||
|
'opacity:0',
|
||||||
|
].join(';')
|
||||||
|
const img = document.createElement('img')
|
||||||
|
img.style.cssText =
|
||||||
|
'display:block;max-width:312px;max-height:312px;object-fit:contain;border-radius:4px;'
|
||||||
|
el.appendChild(img)
|
||||||
|
document.body.appendChild(el)
|
||||||
|
__imgPreviewEl = el
|
||||||
|
}
|
||||||
|
return __imgPreviewEl
|
||||||
|
}
|
||||||
|
|
||||||
|
function showImgPreview(src: string, e: MouseEvent) {
|
||||||
|
const el = getImgPreview()
|
||||||
|
const imgEl = el.querySelector('img') as HTMLImageElement
|
||||||
|
if (imgEl.src !== src) imgEl.src = src
|
||||||
|
|
||||||
|
const GAP = 12
|
||||||
|
const vw = window.innerWidth
|
||||||
|
const vh = window.innerHeight
|
||||||
|
|
||||||
|
el.style.opacity = '0'
|
||||||
|
el.style.display = 'block'
|
||||||
|
|
||||||
|
const pw = el.offsetWidth || 320
|
||||||
|
const ph = el.offsetHeight || 320
|
||||||
|
let left = e.clientX + GAP
|
||||||
|
let top = e.clientY + GAP
|
||||||
|
|
||||||
|
if (left + pw > vw - 8) left = e.clientX - pw - GAP
|
||||||
|
if (top + ph > vh - 8) top = e.clientY - ph - GAP
|
||||||
|
if (left < 8) left = 8
|
||||||
|
if (top < 8) top = 8
|
||||||
|
|
||||||
|
el.style.left = `${left}px`
|
||||||
|
el.style.top = `${top}px`
|
||||||
|
el.style.opacity = '1'
|
||||||
|
}
|
||||||
|
|
||||||
|
function hideImgPreview() {
|
||||||
|
if (__imgPreviewEl) {
|
||||||
|
__imgPreviewEl.style.opacity = '0'
|
||||||
|
__imgPreviewEl.style.display = 'none'
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
const cellTemplateImage = (
|
const cellTemplateImage = (
|
||||||
cellElement: HTMLElement,
|
cellElement: HTMLElement,
|
||||||
cellInfo: DataGridTypes.ColumnCellTemplateData<any, any>,
|
cellInfo: DataGridTypes.ColumnCellTemplateData<any, any>,
|
||||||
|
|
@ -74,21 +141,29 @@ const cellTemplateImage = (
|
||||||
? cellInfo.value.filter(Boolean)
|
? cellInfo.value.filter(Boolean)
|
||||||
: [cellInfo.value].filter(Boolean)
|
: [cellInfo.value].filter(Boolean)
|
||||||
|
|
||||||
const col = cellInfo.column as any
|
//const col = cellInfo.column as any
|
||||||
const imgOptions = col?.extras?.imageUploadOptions ?? {}
|
//const imgOptions = col?.extras?.imageUploadOptions ?? {}
|
||||||
const w: number = imgOptions.width ?? 40
|
//const w: number = imgOptions.width ?? 40
|
||||||
const h: number = imgOptions.height ?? 40
|
//const h: number = imgOptions.height ?? 40
|
||||||
|
const w: number = 40
|
||||||
const imgs = urls
|
const h: number = 40
|
||||||
.map(
|
|
||||||
(url) =>
|
|
||||||
`<img src="${url}" alt="" style="width:${w}px;height:${h}px;object-fit:cover;border-radius:4px;border:1px solid #ddd;margin:2px;vertical-align:middle;display:inline-block;" />`,
|
|
||||||
)
|
|
||||||
.join('')
|
|
||||||
|
|
||||||
cellElement.style.cssText += 'display:flex;flex-wrap:wrap;align-items:center;gap:4px;'
|
cellElement.style.cssText += 'display:flex;flex-wrap:wrap;align-items:center;gap:4px;'
|
||||||
cellElement.innerHTML = imgs
|
cellElement.innerHTML = ''
|
||||||
cellElement.title = urls.join(', ')
|
//cellElement.title = urls.join(', ')
|
||||||
|
|
||||||
|
urls.forEach((url) => {
|
||||||
|
const img = document.createElement('img')
|
||||||
|
img.src = url
|
||||||
|
img.alt = ''
|
||||||
|
img.style.cssText = `width:${w}px;height:${h}px;object-fit:cover;border-radius:4px;border:1px solid #ddd;margin:2px;vertical-align:middle;display:inline-block;cursor:zoom-in;`
|
||||||
|
|
||||||
|
img.addEventListener('mouseenter', (e) => showImgPreview(url, e as MouseEvent))
|
||||||
|
img.addEventListener('mousemove', (e) => showImgPreview(url, e as MouseEvent))
|
||||||
|
img.addEventListener('mouseleave', hideImgPreview)
|
||||||
|
|
||||||
|
cellElement.appendChild(img)
|
||||||
|
})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -211,7 +211,7 @@ export function MenuAddDialog({
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Dialog isOpen={isOpen} onClose={onClose} onRequestClose={onClose} width={680}>
|
<Dialog isOpen={isOpen} onClose={onClose} onRequestClose={onClose} width={680}>
|
||||||
<div className="flex flex-col gap-5 p-5">
|
<div className="flex flex-col gap-5 p-1">
|
||||||
{/* Header */}
|
{/* Header */}
|
||||||
<div className="flex items-center gap-2 pb-1 border-b border-gray-100 dark:border-gray-700">
|
<div className="flex items-center gap-2 pb-1 border-b border-gray-100 dark:border-gray-700">
|
||||||
<FaPlus className="text-green-500 text-sm" />
|
<FaPlus className="text-green-500 text-sm" />
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue