Genel düzeltmeler Responsive ve Seeder

This commit is contained in:
Sedat Öztürk 2026-05-08 23:29:06 +03:00
parent a70d8650f1
commit 871ee34536
9 changed files with 248 additions and 78 deletions

View file

@ -16733,8 +16733,8 @@
{
"resourceName": "Platform",
"key": "ListForms.Wizard.MenuInfo",
"en": "Menu Information",
"tr": "Menü Bilgileri"
"en": "Menu",
"tr": "Menü"
},
{
"resourceName": "Platform",
@ -16751,14 +16751,14 @@
{
"resourceName": "Platform",
"key": "ListForms.Wizard.ListFormSettings",
"en": "List Form Settings",
"tr": "Liste Formu Ayarları"
"en": "Settings",
"tr": "Ayarlar"
},
{
"resourceName": "Platform",
"key": "ListForms.Wizard.ListFormFields",
"en": "List Form Fields",
"tr": "Liste Formu Alanları"
"en": "Fields",
"tr": "Alanlar"
},
{
"resourceName": "Platform",

View file

@ -840,10 +840,10 @@ public class ListFormSeeder_Administration : IDataSeedContributor, ITransientDep
ColSpan = 4,
SqlQuery = @"
SELECT
'Aktif' AS ""Title"",
N'Aktif' AS ""Title"",
COUNT(""Id"") AS ""Value"",
'blue' AS ""Color"",
'Aktif Kullanıcılar' AS ""SubTitle"",
N'Aktif Kullanıcılar' AS ""SubTitle"",
'FaUserCheck' AS ""Icon""
FROM ""AbpUsers""
WHERE ""IsActive"" = 'true'
@ -851,10 +851,10 @@ public class ListFormSeeder_Administration : IDataSeedContributor, ITransientDep
UNION ALL
SELECT
'Pasif' AS ""Title"",
N'Pasif' AS ""Title"",
COUNT(""Id"") AS ""Value"",
'green' AS ""Color"",
'Pasif Kullanıcılar' AS ""SubTitle"",
N'Pasif Kullanıcılar' AS ""SubTitle"",
'FaUserSlash' AS ""Icon""
FROM ""AbpUsers""
WHERE ""IsActive"" = 'false'
@ -862,10 +862,10 @@ public class ListFormSeeder_Administration : IDataSeedContributor, ITransientDep
UNION ALL
SELECT
'Doğrulama' AS ""Title"",
N'Doğrulama' AS ""Title"",
COUNT(""Id"") AS ""Value"",
'purple' AS ""Color"",
'Yönetici Doğrulaması bekleyenler' AS ""SubTitle"",
N'Doğrulama bekleyenler' AS ""SubTitle"",
'FaUserClock' AS ""Icon""
FROM ""AbpUsers""
WHERE ""IsVerified"" = 'false';

View file

@ -77,7 +77,7 @@ export default function Widget({
>
<div className="flex items-center justify-between">
<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="text-sm text-gray-500 mt-1">{subTitle}</p>
</div>

View file

@ -8,13 +8,25 @@ export default function WidgetGroup({ widgetGroups }: { widgetGroups: WidgetGrou
return (
<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) => (
<div
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) => (
<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
title={item.title}
value={item.value}
@ -36,3 +48,4 @@ export default function WidgetGroup({ widgetGroups }: { widgetGroups: WidgetGrou
</div>
)
}

View file

@ -452,7 +452,7 @@ function OrgChartNode({
data-card=""
data-depth={depth}
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' }}
>
{/* Header bar */}
@ -522,7 +522,7 @@ function OrgChartNode({
{/* Children */}
{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) => {
const childPosition = positions[child.id] ?? { x: 0, y: 0 }
@ -695,7 +695,7 @@ function OrgChartTree({
}
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">
{paths.map((path, index) => (
<path
@ -710,7 +710,7 @@ function OrgChartTree({
))}
</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) => (
<OrgChartNode
key={root.id}
@ -804,73 +804,73 @@ const OrgChart = () => {
<div className="flex flex-col h-full">
{/* Toolbar */}
<div className="flex items-center justify-between pb-1 border-b">
<div className="flex items-center gap-3">
<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 min-w-0">
{MenuIcon}
<h4 className="text-sm font-medium">
<h4 className="text-sm font-medium truncate">
{translate('::App.Definitions.OrgChart') || 'Organizasyon Şeması'}
</h4>
</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">
<button
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'
? 'bg-blue-600 text-white shadow-sm'
: 'text-slate-600 hover:text-slate-800'
}`}
>
<FaBuilding className="w-3.5 h-3.5" />
{translate('::App.Hr.Department') || 'Departman'}
<FaBuilding className="w-3.5 h-3.5 flex-shrink-0" />
<span className="hidden sm:inline">{translate('::App.Hr.Department') || 'Departman'}</span>
</button>
<button
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'
? 'bg-purple-600 text-white shadow-sm'
: 'text-slate-600 hover:text-slate-800'
}`}
>
<FaBriefcase className="w-3.5 h-3.5" />
{translate('::App.Hr.JobPosition') || 'Pozisyon'}
<FaBriefcase className="w-3.5 h-3.5 flex-shrink-0" />
<span className="hidden sm:inline">{translate('::App.Hr.JobPosition') || 'Pozisyon'}</span>
</button>
</div>
<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
type="checkbox"
checked={showUsers}
onChange={(e) => setShowUsers(e.target.checked)}
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>
</div>
<div className="flex items-center bg-slate-100 rounded-lg p-1 gap-1">
<button
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"
>
<FaSearchMinus className="w-3.5 h-3.5" />
</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)}%
</span>
<button
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"
>
<FaSearchPlus className="w-3.5 h-3.5" />
</button>
<button
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"
>
<FaUndo className="w-3.5 h-3.5" />
@ -880,11 +880,11 @@ const OrgChart = () => {
<button
onClick={handleExportJpg}
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"
>
<FaFileImage className="w-3.5 h-3.5" />
{exporting ? 'İşleniyor…' : 'Export'}
<FaFileImage className="w-3.5 h-3.5 flex-shrink-0" />
<span className="hidden sm:inline">{exporting ? 'İşleniyor…' : 'Export'}</span>
</button>
</div>
</div>

View file

@ -109,21 +109,23 @@ const WizardFileManager = () => {
{/* ── Header ─────────────────────────────────────────────── */}
<div
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',
)}
>
{MenuIcon}
<h4 className="text-sm font-medium">
{translate('::App.Listforms.WizardManager') || 'Wizard Seed Dosyaları'}
</h4>
<div className="flex items-center gap-2 min-w-0">
{MenuIcon}
<h4 className="text-sm font-medium truncate">
{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">
<FaSearch className="absolute left-2 top-1/2 -translate-y-1/2 text-gray-400 text-xs pointer-events-none" />
<Input
size="sm"
className="pl-6 w-44"
className="pl-6 w-36 sm:w-44"
placeholder={translate('::App.Platform.Search') || 'Search...'}
value={search}
onChange={(e) => setSearch(e.target.value)}
@ -145,7 +147,7 @@ const WizardFileManager = () => {
onClick={() => setShowDbMigrateDialog(true)}
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
size="sm"
@ -154,7 +156,7 @@ const WizardFileManager = () => {
className="flex items-center"
>
<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>
</div>
</div>
@ -181,7 +183,7 @@ const WizardFileManager = () => {
return (
<div
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">
<Icon className="text-indigo-400 shrink-0 text-3xl" />
@ -197,7 +199,7 @@ const WizardFileManager = () => {
</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 && (
<span
title="Bu dosyada izlenen kayıt bilgisi yok. Eski format olabilir."

View file

@ -1,7 +1,7 @@
import React, { useEffect } from 'react'
import React, { useEffect, useState } from 'react'
import { useLocalization } from '@/utils/hooks/useLocalization'
import { motion } from 'framer-motion'
import { FaTimes, FaEye, FaClipboard } from 'react-icons/fa'
import { motion, AnimatePresence } from 'framer-motion'
import { FaTimes, FaEye, FaClipboard, FaChevronLeft, FaChevronRight } from 'react-icons/fa'
import { AnnouncementDto } from '@/proxy/intranet/models'
import useLocale from '@/utils/hooks/useLocale'
import { currentLocalDate } from '@/utils/dateUtils'
@ -17,11 +17,34 @@ interface AnnouncementModalProps {
const AnnouncementModal: React.FC<AnnouncementModalProps> = ({ announcement, onClose }) => {
const { translate } = useLocalization()
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(() => {
intranetService.incrementAnnouncementViewCount(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 colors: Record<string, string> = {
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 */}
<div className="p-6 max-h-[60vh] overflow-y-auto">
{/* 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) => {
if (count === 1) return ''
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) => (
<img
key={idx}
src={
img.startsWith('data:') ||
img.startsWith('http://') ||
img.startsWith('https://') ||
img.startsWith('/')
? img
: `data:image/jpeg;base64,${img}`
}
src={imgSrc(img)}
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>
@ -236,6 +251,71 @@ const AnnouncementModal: React.FC<AnnouncementModalProps> = ({ announcement, onC
</div>
</motion.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>
</>
)
}

View file

@ -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 = (
cellElement: HTMLElement,
cellInfo: DataGridTypes.ColumnCellTemplateData<any, any>,
@ -74,21 +141,29 @@ const cellTemplateImage = (
? cellInfo.value.filter(Boolean)
: [cellInfo.value].filter(Boolean)
const col = cellInfo.column as any
const imgOptions = col?.extras?.imageUploadOptions ?? {}
const w: number = imgOptions.width ?? 40
const h: number = imgOptions.height ?? 40
const imgs = urls
.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('')
//const col = cellInfo.column as any
//const imgOptions = col?.extras?.imageUploadOptions ?? {}
//const w: number = imgOptions.width ?? 40
//const h: number = imgOptions.height ?? 40
const w: number = 40
const h: number = 40
cellElement.style.cssText += 'display:flex;flex-wrap:wrap;align-items:center;gap:4px;'
cellElement.innerHTML = imgs
cellElement.title = urls.join(', ')
cellElement.innerHTML = ''
//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)
})
}
}

View file

@ -211,7 +211,7 @@ export function MenuAddDialog({
return (
<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 */}
<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" />