Compare commits

...

9 Commits

Author SHA1 Message Date
67815aec2d update the ideas page 2025-10-04 01:55:24 +03:30
e10c25fc3e refactor: the component ,feat: add tables in componente 2025-10-04 01:55:21 +03:30
mahmoodsht
b4b023ec32 جزییات گراف 2025-10-02 20:41:00 +03:30
MehrdadAdabi
9d0fd5968b fix: design bugs 2025-09-30 10:26:24 +03:30
MehrdadAdabi
cacf40938f fix: change adaption rate bg color and logic 2025-09-29 18:41:28 +03:30
MehrdadAdabi
ef96cb4778 fix: ui bugs 2025-09-28 22:41:04 +03:30
MehrdadAdabi
d4fd97daaa Merge branch 'main' of http://git.sepehrdata.com/Saeed0920/inogen 2025-09-28 22:32:45 +03:30
MehrdadAdabi
b60216c71d feat: add Compliance rate 2025-09-28 22:31:52 +03:30
mahmoodsht
921afe42fa جزییات 2025-09-28 08:11:19 +03:30
14 changed files with 1850 additions and 1161 deletions

View File

@ -2,51 +2,51 @@
@import "tailwindcss";
@theme {
/* Teal color scale */
--color-teal-50: #f0fdfa;
--color-teal-100: #ccfbf1;
--color-teal-200: #99f6e4;
--color-teal-300: #5eead4;
--color-teal-400: #2dd4bf;
--color-teal-500: #14b8a6;
--color-teal-600: #0d9488;
--color-teal-700: #0f766e;
--color-teal-800: #115e59;
--color-teal-900: #134e4a;
--color-teal-950: #042f2e;
/* Teal color scale */
--color-teal-50: #f0fdfa;
--color-teal-100: #ccfbf1;
--color-teal-200: #99f6e4;
--color-teal-300: #5eead4;
--color-teal-400: #2dd4bf;
--color-teal-500: #14b8a6;
--color-teal-600: #0d9488;
--color-teal-700: #0f766e;
--color-teal-800: #115e59;
--color-teal-900: #134e4a;
--color-teal-950: #042f2e;
/* Slate color scale */
--color-slate-50: #f8fafc;
--color-slate-100: #f1f5f9;
--color-slate-200: #e2e8f0;
--color-slate-300: #cbd5e1;
--color-slate-400: #94a3b8;
--color-slate-500: #64748b;
--color-slate-600: #475569;
--color-slate-700: #334155;
--color-slate-800: #1e293b;
--color-slate-900: #0f172a;
--color-slate-950: #020617;
/* Slate color scale */
--color-slate-50: #f8fafc;
--color-slate-100: #f1f5f9;
--color-slate-200: #e2e8f0;
--color-slate-300: #cbd5e1;
--color-slate-400: #94a3b8;
--color-slate-500: #64748b;
--color-slate-600: #475569;
--color-slate-700: #334155;
--color-slate-800: #1e293b;
--color-slate-900: #0f172a;
--color-slate-950: #020617;
--color-pr-green: #3aea83;
--color-pr-blue: #69c8ea;
--color-pr-red: #f76276;
--color-pr-gray: #3f415a;
--color-pr-green: #3aea83;
--color-pr-blue: #69c8ea;
--color-pr-red: #f76276;
--color-pr-gray: #3f415a;
}
html,
body {
@apply bg-background text-foreground;
@media (prefers-color-scheme: dark) {
color-scheme: dark;
}
@apply bg-background text-foreground;
@media (prefers-color-scheme: dark) {
color-scheme: dark;
}
}
body {
font-family: IRANYekanX;
direction: rtl;
background-color: #cdcdcd;
margin: 0;
font-family: IRANYekanX;
direction: rtl;
background-color: #cdcdcd;
margin: 0;
}
h1,
h2,
@ -56,378 +56,380 @@ h5,
h6,
input,
textarea {
font-family: IRANYekanX;
font-family: IRANYekanX;
}
/* RTL Support */
html[dir="rtl"] {
direction: rtl;
direction: rtl;
}
html[dir="rtl"] body {
text-align: right;
text-align: right;
}
@theme inline {
--radius-sm: calc(var(--radius) - 4px);
--radius-md: calc(var(--radius) - 2px);
--radius-lg: var(--radius);
--radius-xl: calc(var(--radius) + 4px);
--color-background: var(--background);
--color-foreground: var(--foreground);
--color-card: var(--card);
--color-card-foreground: var(--card-foreground);
--color-popover: var(--popover);
--color-popover-foreground: var(--popover-foreground);
--color-primary: var(--primary);
--color-primary-foreground: var(--primary-foreground);
--color-secondary: var(--secondary);
--color-secondary-foreground: var(--secondary-foreground);
--color-muted: var(--muted);
--color-muted-foreground: var(--muted-foreground);
--color-accent: var(--accent);
--color-accent-foreground: var(--accent-foreground);
--color-destructive: var(--destructive);
--color-destructive-foreground: var(--destructive-foreground);
--color-border: var(--border);
--color-input: var(--input);
--color-ring: var(--ring);
--radius-sm: calc(var(--radius) - 4px);
--radius-md: calc(var(--radius) - 2px);
--radius-lg: var(--radius);
--radius-xl: calc(var(--radius) + 4px);
--color-background: var(--background);
--color-foreground: var(--foreground);
--color-card: var(--card);
--color-card-foreground: var(--card-foreground);
--color-popover: var(--popover);
--color-dark-blue: var(--dark-blue);
--color-popover-foreground: var(--popover-foreground);
--color-primary: var(--primary);
--color-primary-foreground: var(--primary-foreground);
--color-secondary: var(--secondary);
--color-secondary-foreground: var(--secondary-foreground);
--color-muted: var(--muted);
--color-muted-foreground: var(--muted-foreground);
--color-accent: var(--accent);
--color-accent-foreground: var(--accent-foreground);
--color-destructive: var(--destructive);
--color-destructive-foreground: var(--destructive-foreground);
--color-border: var(--border);
--color-input: var(--input);
--color-ring: var(--ring);
}
:root {
--radius: 0.5rem;
--radius: 0.5rem;
--color-green: #3aea83;
--color-blue: #69c8ea;
--color-red: #f76276;
--color-green: #3aea83;
--color-blue: #69c8ea;
--color-red: #f76276;
/* primary colors */
--color-pr-gray: #3f415a;
--color-pr-green: var(--color-green);
/* primary colors */
--color-pr-gray: #3f415a;
--color-pr-green: var(--color-green);
/* Light theme colors */
--background: #ffffff;
--foreground: #0a0a0a;
--card: #ffffff;
--card-foreground: #0a0a0a;
--popover: #ffffff;
--popover-foreground: #0a0a0a;
--primary: #22c55e;
--primary-foreground: #ffffff;
--secondary: #f5f5f5;
--secondary-foreground: #0a0a0a;
--muted: #f5f5f5;
--muted-foreground: #737373;
--accent: #f5f5f5;
--accent-foreground: #0a0a0a;
--destructive: #ef4444;
--destructive-foreground: #ffffff;
--border: #e5e5e5;
--input: #e5e5e5;
--ring: #22c55e;
/* Light theme colors */
--background: #ffffff;
--foreground: #0a0a0a;
--card: #ffffff;
--card-foreground: #0a0a0a;
--popover: #ffffff;
--popover-foreground: #0a0a0a;
--primary: #22c55e;
--primary-foreground: #ffffff;
--secondary: #f5f5f5;
--secondary-foreground: #0a0a0a;
--muted: #f5f5f5;
--muted-foreground: #737373;
--accent: #f5f5f5;
--accent-foreground: #0a0a0a;
--destructive: #ef4444;
--destructive-foreground: #ffffff;
--border: #e5e5e5;
--input: #e5e5e5;
--ring: #22c55e;
--dark-blue: #33364d;
/* Primary color scale */
--color-primary-50: #f0fdf4;
--color-primary-100: #dcfce7;
--color-primary-200: #bbf7d0;
--color-primary-300: #86efac;
--color-primary-400: #4ade80;
--color-primary-500: #22c55e;
--color-primary-600: #16a34a;
--color-primary-700: #15803d;
--color-primary-800: #166534;
--color-primary-900: #14532d;
--color-primary-950: #052e16;
/* Primary color scale */
--color-primary-50: #f0fdf4;
--color-primary-100: #dcfce7;
--color-primary-200: #bbf7d0;
--color-primary-300: #86efac;
--color-primary-400: #4ade80;
--color-primary-500: #22c55e;
--color-primary-600: #16a34a;
--color-primary-700: #15803d;
--color-primary-800: #166534;
--color-primary-900: #14532d;
--color-primary-950: #052e16;
/* Secondary color scale (Blue) */
--color-secondary-50: #eff6ff;
--color-secondary-100: #dbeafe;
--color-secondary-200: #bfdbfe;
--color-secondary-300: #93c5fd;
--color-secondary-400: #60a5fa;
--color-secondary-500: #3b82f6;
--color-secondary-600: #2563eb;
--color-secondary-700: #1d4ed8;
--color-secondary-800: #1e40af;
--color-secondary-900: #1e3a8a;
--color-secondary-950: #172554;
/* Secondary color scale (Blue) */
--color-secondary-50: #eff6ff;
--color-secondary-100: #dbeafe;
--color-secondary-200: #bfdbfe;
--color-secondary-300: #93c5fd;
--color-secondary-400: #60a5fa;
--color-secondary-500: #3b82f6;
--color-secondary-600: #2563eb;
--color-secondary-700: #1d4ed8;
--color-secondary-800: #1e40af;
--color-secondary-900: #1e3a8a;
--color-secondary-950: #172554;
/* Neutral color scale */
--color-neutral-50: #fafafa;
--color-neutral-100: #f5f5f5;
--color-neutral-200: #e5e5e5;
--color-neutral-300: #d4d4d4;
--color-neutral-400: #a3a3a3;
--color-neutral-500: #737373;
--color-neutral-600: #525252;
--color-neutral-700: #404040;
--color-neutral-800: #262626;
--color-neutral-900: #171717;
--color-neutral-950: #0a0a0a;
/* Neutral color scale */
--color-neutral-50: #fafafa;
--color-neutral-100: #f5f5f5;
--color-neutral-200: #e5e5e5;
--color-neutral-300: #d4d4d4;
--color-neutral-400: #a3a3a3;
--color-neutral-500: #737373;
--color-neutral-600: #525252;
--color-neutral-700: #404040;
--color-neutral-800: #262626;
--color-neutral-900: #171717;
--color-neutral-950: #0a0a0a;
/* Status colors */
--color-success-50: #f0fdf4;
--color-success-100: #dcfce7;
--color-success-500: #22c55e;
--color-success-600: #16a34a;
--color-success-700: #15803d;
--color-success-900: #14532d;
/* Status colors */
--color-success-50: #f0fdf4;
--color-success-100: #dcfce7;
--color-success-500: #22c55e;
--color-success-600: #16a34a;
--color-success-700: #15803d;
--color-success-900: #14532d;
--color-error-50: #fef2f2;
--color-error-100: #fee2e2;
--color-error-500: #ef4444;
--color-error-600: #dc2626;
--color-error-700: #b91c1c;
--color-error-900: #7f1d1d;
--color-error-50: #fef2f2;
--color-error-100: #fee2e2;
--color-error-500: #ef4444;
--color-error-600: #dc2626;
--color-error-700: #b91c1c;
--color-error-900: #7f1d1d;
--color-warning-50: #fffbeb;
--color-warning-100: #fef3c7;
--color-warning-500: #f59e0b;
--color-warning-600: #d97706;
--color-warning-700: #b45309;
--color-warning-900: #78350f;
--color-warning-50: #fffbeb;
--color-warning-100: #fef3c7;
--color-warning-500: #f59e0b;
--color-warning-600: #d97706;
--color-warning-700: #b45309;
--color-warning-900: #78350f;
--color-info-50: #eff6ff;
--color-info-100: #dbeafe;
--color-info-500: #3b82f6;
--color-info-600: #2563eb;
--color-info-700: #1d4ed8;
--color-info-900: #1e3a8a;
--color-info-50: #eff6ff;
--color-info-100: #dbeafe;
--color-info-500: #3b82f6;
--color-info-600: #2563eb;
--color-info-700: #1d4ed8;
--color-info-900: #1e3a8a;
/* Teal colors */
--color-teal-50: #f0fdfa;
--color-teal-100: #ccfbf1;
--color-teal-200: #99f6e4;
--color-teal-300: #5eead4;
--color-teal-400: #2dd4bf;
--color-teal-500: #14b8a6;
--color-teal-600: #0d9488;
--color-teal-700: #0f766e;
--color-teal-800: #115e59;
--color-teal-900: #134e4a;
/* Teal colors */
--color-teal-50: #f0fdfa;
--color-teal-100: #ccfbf1;
--color-teal-200: #99f6e4;
--color-teal-300: #5eead4;
--color-teal-400: #2dd4bf;
--color-teal-500: #14b8a6;
--color-teal-600: #0d9488;
--color-teal-700: #0f766e;
--color-teal-800: #115e59;
--color-teal-900: #134e4a;
/* Dark colors */
--color-dark-50: #f8fafc;
--color-dark-100: #f1f5f9;
--color-dark-200: #e2e8f0;
--color-dark-300: #cbd5e1;
--color-dark-400: #94a3b8;
--color-dark-500: #64748b;
--color-dark-600: #475569;
--color-dark-700: #334155;
--color-dark-800: #1e293b;
--color-dark-900: #0f172a;
--color-dark-950: #020617;
/* Dark colors */
--color-dark-50: #f8fafc;
--color-dark-100: #f1f5f9;
--color-dark-200: #e2e8f0;
--color-dark-300: #cbd5e1;
--color-dark-400: #94a3b8;
--color-dark-500: #64748b;
--color-dark-600: #475569;
--color-dark-700: #334155;
--color-dark-800: #1e293b;
--color-dark-900: #0f172a;
--color-dark-950: #020617;
/* Login specific colors */
--color-login-primary: var(--color-green);
--color-login-dark-start: #464861;
--color-login-dark-end: #111628;
/* Login specific colors */
--color-login-primary: var(--color-green);
--color-login-dark-start: #464861;
--color-login-dark-end: #111628;
}
.dark {
/* Dark theme colors */
--background: #020617;
--foreground: #f8fafc;
--card: #0f172a;
--card-foreground: #f8fafc;
--popover: #0f172a;
--popover-foreground: #f8fafc;
--primary: #22c55e;
--primary-foreground: #0a0a0a;
--secondary: #1e293b;
--secondary-foreground: #f8fafc;
--muted: #1e293b;
--muted-foreground: #94a3b8;
--accent: #1e293b;
--accent-foreground: #f8fafc;
--destructive: #ef4444;
--destructive-foreground: #f8fafc;
--border: #1e293b;
--input: #1e293b;
--ring: #22c55e;
/* Dark theme colors */
--background: #020617;
--foreground: #f8fafc;
--card: #0f172a;
--card-foreground: #f8fafc;
--popover: #0f172a;
--popover-foreground: #f8fafc;
--primary: #22c55e;
--primary-foreground: #0a0a0a;
--secondary: #1e293b;
--secondary-foreground: #f8fafc;
--muted: #1e293b;
--muted-foreground: #94a3b8;
--accent: #1e293b;
--accent-foreground: #f8fafc;
--destructive: #ef4444;
--destructive-foreground: #f8fafc;
--border: #1e293b;
--input: #1e293b;
--ring: #22c55e;
}
@layer base {
* {
@apply border-border;
}
* {
@apply border-border;
}
body {
@apply bg-background text-foreground;
}
body {
@apply bg-background text-foreground;
}
}
/* Persian/Farsi font class */
.font-persian {
font-family: "IRANYekanX";
font-family: "IRANYekanX";
}
/* Custom utility classes */
.gradient-primary {
background: linear-gradient(
135deg,
var(--color-primary-500) 0%,
var(--color-primary-600) 100%
);
background: linear-gradient(
135deg,
var(--color-primary-500) 0%,
var(--color-primary-600) 100%
);
}
.gradient-secondary {
background: linear-gradient(
135deg,
var(--color-secondary-500) 0%,
var(--color-secondary-600) 100%
);
background: linear-gradient(
135deg,
var(--color-secondary-500) 0%,
var(--color-secondary-600) 100%
);
}
.gradient-background {
background: linear-gradient(
135deg,
var(--color-neutral-50) 0%,
var(--color-neutral-100) 100%
);
background: linear-gradient(
135deg,
var(--color-neutral-50) 0%,
var(--color-neutral-100) 100%
);
}
.dark .gradient-background {
background: linear-gradient(
135deg,
var(--color-neutral-900) 0%,
var(--color-neutral-800) 100%
);
background: linear-gradient(
135deg,
var(--color-neutral-900) 0%,
var(--color-neutral-800) 100%
);
}
/* Login page specific styles */
.login-page {
background: linear-gradient(
135deg,
var(--color-login-dark-start) 0%,
var(--color-login-dark-end) 100%
);
background: linear-gradient(
135deg,
var(--color-login-dark-start) 0%,
var(--color-login-dark-end) 100%
);
}
.login-sidebar {
background: var(--color-login-primary);
background: var(--color-login-primary);
}
/* Animation classes */
.animate-fade-in {
animation: fadeIn 0.3s ease-in-out;
animation: fadeIn 0.3s ease-in-out;
}
.animate-slide-up {
animation: slideUp 0.3s ease-out;
animation: slideUp 0.3s ease-out;
}
.animate-slide-down {
animation: slideDown 0.3s ease-out;
animation: slideDown 0.3s ease-out;
}
@keyframes fadeIn {
from {
opacity: 0;
}
to {
opacity: 1;
}
from {
opacity: 0;
}
to {
opacity: 1;
}
}
@keyframes slideUp {
from {
transform: translateY(10px);
opacity: 0;
}
to {
transform: translateY(0);
opacity: 1;
}
from {
transform: translateY(10px);
opacity: 0;
}
to {
transform: translateY(0);
opacity: 1;
}
}
@keyframes slideDown {
from {
transform: translateY(-10px);
opacity: 0;
}
to {
transform: translateY(0);
opacity: 1;
}
from {
transform: translateY(-10px);
opacity: 0;
}
to {
transform: translateY(0);
opacity: 1;
}
}
/* Toast customization for RTL */
.Toaster__toast {
direction: rtl;
text-align: right;
direction: rtl;
text-align: right;
}
/* Form focus styles */
.form-input:focus-within {
@apply ring-2 ring-primary/20 border-primary;
@apply ring-2 ring-primary/20 border-primary;
}
/* Button hover effects */
.btn-hover-scale:hover {
transform: scale(1.02);
transition: transform 0.2s ease-in-out;
transform: scale(1.02);
transition: transform 0.2s ease-in-out;
}
/* Custom shadows */
.shadow-primary {
box-shadow:
0 4px 6px -1px rgb(34 197 94 / 0.1),
0 2px 4px -2px rgb(34 197 94 / 0.1);
box-shadow:
0 4px 6px -1px rgb(34 197 94 / 0.1),
0 2px 4px -2px rgb(34 197 94 / 0.1);
}
.shadow-error {
box-shadow:
0 4px 6px -1px rgb(239 68 68 / 0.1),
0 2px 4px -2px rgb(239 68 68 / 0.1);
box-shadow:
0 4px 6px -1px rgb(239 68 68 / 0.1),
0 2px 4px -2px rgb(239 68 68 / 0.1);
}
/* Loading states */
.loading-shimmer {
background: linear-gradient(90deg, #f0f0f0 25%, #e0e0e0 50%, #f0f0f0 75%);
background-size: 200% 100%;
animation: shimmer 1.5s infinite;
background: linear-gradient(90deg, #f0f0f0 25%, #e0e0e0 50%, #f0f0f0 75%);
background-size: 200% 100%;
animation: shimmer 1.5s infinite;
}
.dark .loading-shimmer {
background: linear-gradient(90deg, #2a2a2a 25%, #3a3a3a 50%, #2a2a2a 75%);
background-size: 200% 100%;
background: linear-gradient(90deg, #2a2a2a 25%, #3a3a3a 50%, #2a2a2a 75%);
background-size: 200% 100%;
}
@keyframes shimmer {
0% {
background-position: -200% 0;
}
100% {
background-position: 200% 0;
}
0% {
background-position: -200% 0;
}
100% {
background-position: 200% 0;
}
}
/* Table/container specific custom dark scrollbar */
.custom-scrollbar {
scrollbar-width: thin; /* Firefox */
scrollbar-color: rgba(100, 116, 139, 0.6) transparent; /* thumb track */
scrollbar-width: thin; /* Firefox */
scrollbar-color: rgba(100, 116, 139, 0.6) transparent; /* thumb track */
}
.custom-scrollbar::-webkit-scrollbar {
width: 2px;
height: 2px;
width: 2px;
height: 2px;
}
.custom-scrollbar::-webkit-scrollbar-track {
background: rgba(241, 245, 249, 0.6); /* slate-100 */
background: rgba(241, 245, 249, 0.6); /* slate-100 */
}
.custom-scrollbar::-webkit-scrollbar-thumb {
background: linear-gradient(
to bottom,
rgba(16, 185, 129, 0.6),
rgba(16, 185, 129, 0.9)
); /* emerald */
border-radius: 9999px;
border: 0.5px solid transparent;
background-clip: padding-box;
background: linear-gradient(
to bottom,
rgba(16, 185, 129, 0.6),
rgba(16, 185, 129, 0.9)
); /* emerald */
border-radius: 9999px;
border: 0.5px solid transparent;
background-clip: padding-box;
}
.custom-scrollbar:hover::-webkit-scrollbar-thumb {
@ -443,50 +445,50 @@ html[dir="rtl"] body {
}
:root {
--form-control-color: #3f415a;
--form-control-disabled: ##5f6284;
--form-background: #3aea83;
--form-control-color: #3f415a;
--form-control-disabled: ##5f6284;
--form-background: #3aea83;
}
input[type="checkbox"] {
-webkit-appearance: none;
appearance: none;
margin: 0;
font: inherit;
color: #5f6284;
background-color: transparent;
width: 1.15em;
height: 1.15em;
border: 1px solid #5f6284;
border-radius: 0.15em;
transform: translateY(-0.075em);
display: grid;
place-content: center;
cursor: pointer;
-webkit-appearance: none;
appearance: none;
margin: 0;
font: inherit;
color: #5f6284;
background-color: transparent;
width: 1.15em;
height: 1.15em;
border: 1px solid #5f6284;
border-radius: 0.15em;
transform: translateY(-0.075em);
display: grid;
place-content: center;
cursor: pointer;
}
input[type="checkbox"]::before {
content: "";
width: 0.65em;
height: 0.65em;
clip-path: polygon(14% 44%, 0 65%, 50% 100%, 100% 16%, 80% 0%, 43% 62%);
transform: scale(0);
transform-origin: bottom left;
transition: 120ms transform ease-in-out;
box-shadow: inset 1em 1em var(--form-control-color);
content: "";
width: 0.65em;
height: 0.65em;
clip-path: polygon(14% 44%, 0 65%, 50% 100%, 100% 16%, 80% 0%, 43% 62%);
transform: scale(0);
transform-origin: bottom left;
transition: 120ms transform ease-in-out;
box-shadow: inset 1em 1em var(--form-control-color);
}
input[type="checkbox"]:checked::before {
transform: scale(1);
transform: scale(1);
}
input[type="checkbox"]:checked {
background-color: #3aea83;
border: 1px solid transparent;
background-color: #3aea83;
border: 1px solid transparent;
}
input[type="checkbox"]:disabled {
--form-control-color: var(--form-control-disabled);
color: var(--form-control-disabled);
cursor: not-allowed;
--form-control-color: var(--form-control-disabled);
color: var(--form-control-disabled);
cursor: not-allowed;
}

View File

@ -12,7 +12,6 @@ import {
Zap,
} from "lucide-react";
import moment from "moment-jalaali";
import { formatNumber } from "~/lib/utils";
import { useCallback, useEffect, useMemo, useRef, useState } from "react";
import toast from "react-hot-toast";
import { Badge } from "~/components/ui/badge";
@ -35,7 +34,7 @@ import {
TableRow,
} from "~/components/ui/table";
import apiService from "~/lib/api";
import { formatCurrency } from "~/lib/utils";
import { formatCurrency, formatNumber } from "~/lib/utils";
import { DashboardLayout } from "../layout";
moment.loadPersian({ usePersianDigits: true });
@ -370,7 +369,8 @@ export function DigitalInnovationPage() {
const scrollContainer = scrollContainerRef.current;
const handleScroll = () => {
if (!scrollContainer || !hasMore || loadingMore || fetchingRef.current) return;
if (!scrollContainer || !hasMore || loadingMore || fetchingRef.current)
return;
// Clear previous timeout
if (scrollTimeoutRef.current) {
@ -390,7 +390,9 @@ export function DigitalInnovationPage() {
};
if (scrollContainer) {
scrollContainer.addEventListener("scroll", handleScroll, { passive: true });
scrollContainer.addEventListener("scroll", handleScroll, {
passive: true,
});
}
return () => {
@ -452,8 +454,6 @@ export function DigitalInnovationPage() {
innovation_digital_function: {},
});
// let payload: DigitalInnovationMetrics = raw?.data;
// console.log("*-*-*-*" +payload);
// if (typeof payload === "string") {
@ -482,8 +482,6 @@ export function DigitalInnovationPage() {
}
}
const parseNum = (v: unknown): number => {
if (v == null) return 0;
if (typeof v === "number") return v;
@ -601,7 +599,7 @@ export function DigitalInnovationPage() {
variant="ghost"
size="sm"
onClick={() => handleProjectDetails(item)}
className="text-emerald-400 hover:text-emerald-300 hover:bg-emerald-500/20 p-2 h-auto cursor-pointer"
className="text-pr-green hover:text-emerald-300 underline-offset-4 underline font-normal hover:bg-emerald-500/20 p-2 h-auto"
>
جزئیات بیشتر
</Button>
@ -654,7 +652,7 @@ export function DigitalInnovationPage() {
return (
<DashboardLayout title="نوآوری دیجیتال">
<div className="space-y-4 grid justify-between gap-8 sm:grid-cols-1 xl:grid-cols-[40%_60%]">
<div className="space-y-4 grid justify-between gap-8 pl-6 sm:grid-cols-1 xl:grid-cols-[40%_60%]">
{/* Stats Cards */}
<div className="flex flex-col gap-6 w-full mb-0">
<div className="space-y-6 w-full">
@ -955,7 +953,7 @@ export function DigitalInnovationPage() {
شرح پروژه
</DialogTitle>
</DialogHeader>
<div className="body grid grid-cols-[40%_20%_40%]">
<div className="body grid grid-cols-[40%_20%_40%] pb-6">
<div className="border-l-2 border-l-gray-600 px-6">
<span className="title text-lg font-bold">
{dialogInfo?.title}
@ -1070,7 +1068,7 @@ export function DigitalInnovationPage() {
</div>
</div>
</div>
<div className="flex flex-col pr-7 gap-4">
<div className="flex flex-col px-6 gap-4">
<div className="costBoard mx-auto w-full">
<div className="board o border border-gray-600 rounded-xl overflow-hidden flex flex-col">
<span className="title bg-[#3F415A] text-white w-full p-2.5 pr-4 ">

View File

@ -1,6 +1,5 @@
// import moment from "moment-jalaali";
import { useCallback, useEffect, useRef, useState } from "react";
import { formatNumber } from "~/lib/utils";
import {
Bar,
BarChart,
@ -27,6 +26,7 @@ import {
TableHeader,
TableRow,
} from "~/components/ui/table";
import { formatNumber } from "~/lib/utils";
import {
Building2,
@ -602,7 +602,7 @@ export function GreenInnovationPage() {
variant="ghost"
size="sm"
onClick={() => handleProjectDetails(item)}
className="text-emerald-400 hover:text-emerald-300 hover:bg-emerald-500/20 p-2 h-auto cursor-pointer"
className="text-pr-green hover:text-emerald-300 underline-offset-4 underline font-normal hover:bg-emerald-500/20 p-2 h-auto"
>
جزئیات بیشتر
</Button>
@ -813,7 +813,10 @@ export function GreenInnovationPage() {
<div className="params flex flex-col gap-3.5">
{Object.entries(recycleParams).map((el, index) => {
return (
<div key={index} className="param flex flex-row justify-between items-center">
<div
key={index}
className="param flex flex-row justify-between items-center"
>
<div className="flex flex-row gap-2">
{el[1].icon}
<span className="font-normal text-sm font-persian">

View File

@ -624,7 +624,7 @@ export function InnovationBuiltInsidePage() {
variant="ghost"
size="sm"
onClick={() => handleProjectDetails(item)}
className="text-emerald-500 hover:text-emerald-300 hover:bg-emerald-500/20 p-2 h-auto cursor-pointer"
className="text-pr-green hover:text-emerald-300 underline-offset-4 underline font-normal hover:bg-emerald-500/20 p-2 h-auto"
>
جزئیات بیشتر
</Button>
@ -701,7 +701,7 @@ export function InnovationBuiltInsidePage() {
return (
<DashboardLayout title="نوآوری ساخت داخل">
<div className="space-y-4 justify-between gap-8 grid sm:grid-cols-1 xl:grid-cols-[40%_60%]">
<div className="space-y-4 justify-between gap-8 grid pl-3.5 sm:grid-cols-1 xl:grid-cols-[40%_60%]">
{/* Stats Cards */}
<div className="flex gap-6 w-full mb-0">
<div className="flex flex-col justify-between w-full gap-6">

View File

@ -15,8 +15,9 @@ import moment from "moment-jalaali";
import { useCallback, useEffect, useRef, useState } from "react";
import toast from "react-hot-toast";
import { Badge } from "~/components/ui/badge";
import { Button } from "~/components/ui/button";
import { BaseCard } from "~/components/ui/base-card";
import { Button } from "~/components/ui/button";
import { Card, CardContent } from "~/components/ui/card";
import { Checkbox } from "~/components/ui/checkbox";
import { CustomBarChart } from "~/components/ui/custom-bar-chart";
import {
@ -36,7 +37,6 @@ import {
import apiService from "~/lib/api";
import { formatNumber } from "~/lib/utils";
import { DashboardLayout } from "../layout";
import { Card , CardContent} from "~/components/ui/card";
moment.loadPersian({ usePersianDigits: true });
interface ProcessInnovationData {
@ -176,7 +176,7 @@ export function ProcessInnovationPage() {
stats.currencyReductionSum.toFixed?.(0) ?? stats.currencyReductionSum
),
description: "دلار کاهش یافته",
icon: DollarSign ,
icon: DollarSign,
color: "text-emerald-400",
},
frequentfailuresreduction: {
@ -551,7 +551,11 @@ export function ProcessInnovationPage() {
</Badge>
);
case "title":
return <span className="font-normal text-sm text-white">{String(value)}</span>;
return (
<span className="font-normal text-sm text-white">
{String(value)}
</span>
);
case "project_status":
return (
<div className="flex items-center gap-1">
@ -567,7 +571,10 @@ export function ProcessInnovationPage() {
);
case "project_rating":
return (
<Badge variant="outline" className="text-base font-semibold text-center border-none">
<Badge
variant="outline"
className="text-base font-semibold text-center border-none"
>
{formatNumber(String(value))}
</Badge>
);
@ -595,7 +602,10 @@ export function ProcessInnovationPage() {
{loading || statsLoading
? // Loading skeleton for stats cards - matching new design
Array.from({ length: 4 }).map((_, index) => (
<BaseCard key={`skeleton-${index}`} className="rounded-2xl overflow-hidden">
<BaseCard
key={`skeleton-${index}`}
className="rounded-2xl overflow-hidden"
>
<div className="flex flex-col justify-between gap-2">
<div className="flex justify-between items-center border-b-2 mx-4 border-gray-500/20">
<div
@ -621,7 +631,10 @@ export function ProcessInnovationPage() {
))
: Object.entries(stateCard).map(([key, card]) => {
// map percent values for each card key
const percentMap: Record<string, number | string | undefined> = {
const percentMap: Record<
string,
number | string | undefined
> = {
productionstopsprevention: stats.percentProductionStops,
bottleneckremoval: stats.percentBottleneckRemoval,
currencyreduction: stats.percentCurrencyReduction,
@ -640,7 +653,7 @@ export function ProcessInnovationPage() {
<div className="flex items-center gap-4">
<div className="text-center">
<p className="text-3xl text-pr-green font-bold mb-1">
{(card.value)}
{card.value}
</p>
<div className="text-[11px] text-[#ACACAC] font-light font-persian">
{card.description}
@ -846,10 +859,12 @@ export function ProcessInnovationPage() {
شرح پروژه
</DialogTitle>
</DialogHeader>
<div className="space-y-4 flex justify-between text-right px-6">
<div className="space-y-4 flex justify-between text-right p-6">
{/* Project Description */}
<div className="flex-[4] border-l-2 border-gray-600">
<h2 className="font-bold text-base">{selectedProjectDetails?.title}</h2>
<h2 className="font-bold text-base">
{selectedProjectDetails?.title}
</h2>
<p className="text-white font-normal text-base font-persian px-2 mt-2">
{selectedProjectDetails?.project_description || "-"}
</p>

View File

@ -616,22 +616,22 @@ export function ProductInnovationPage() {
size="sm"
onClick={() => {
handleProjectDetails(item)}}
className="text-emerald-400 underline underline-offset-4 font-ligth text-base hover:bg-emerald-500/20 p-2 h-auto"
className="text-emerald-400 underline underline-offset-4 font-ligth text-sm hover:bg-emerald-500/20 p-2 h-auto"
>
جزئیات بیشتر
</Button>
);
case "project_no":
return (
<Badge variant="outline" className="font-mono text-base font-light">
<Badge variant="outline" className="font-mono text-sm font-light">
{String(value)}
</Badge>
);
case "title":
return <span className="font-light text-base text-white">{String(value)}</span>;
return <span className="font-light text-sm text-white">{String(value)}</span>;
case "project_status":
return (
<div className="flex items-center text-base font-light gap-1">
<div className="flex items-center text-sm font-light gap-1">
<Badge
variant={statusColor(value as projectStatus)}
className="font-semibold text-base border-2 p-0 block w-2 h-2 rounded-full"
@ -652,7 +652,7 @@ export function ProductInnovationPage() {
</Badge>
);
default:
return <span className="text-white text-base font-light">{String(value) || "-"}</span>;
return <span className="text-white text-sm font-light">{String(value) || "-"}</span>;
}
};

View File

@ -1,25 +1,16 @@
import {
Box,
Building2,
ChevronDown,
ChevronRight,
FolderKanban,
GalleryVerticalEnd,
Globe,
LayoutDashboard,
Leaf,
Lightbulb,
House,
LightbulbIcon,
ListTodo,
LogOut,
MonitorSmartphone,
Package,
Radar,
Settings,
Star,
Workflow,
DiscAlbum,
House,
ListTodo,
LightbulbIcon,
Radar
LucideLightbulb
} from "lucide-react";
import React, { useState } from "react";
import { Link, useLocation } from "react-router";
@ -49,7 +40,6 @@ interface MenuItem {
}
const menuItems: MenuItem[] = [
{
id: "dashboard",
label: "صفحه اصلی",
@ -108,22 +98,15 @@ const menuItems: MenuItem[] = [
{
id: "ideas",
label: "ایده‌های فناوری و نوآوری",
icon: House,
icon: LucideLightbulb,
href: "/dashboard/manage-ideas-tech",
},
{
id: "top-innovations",
label: "نوآور برتر",
icon: Star,
href: "/dashboard/top-innovations",
},
{
{
id: "strategic-alignment",
label: "میزان انطباق راهبردی",
icon: null,
href: "#", // This is not a route, it opens a popup
},
];
const bottomMenuItems: MenuItem[] = [
@ -172,7 +155,10 @@ export function Sidebar({
// Update header title based on current route
// If a child route is active, use that child's label prefixed by parent label
let activeTitle: string | undefined = undefined;
let activeIcon: React.ComponentType<{ className?: string }> | null | undefined = undefined;
let activeIcon:
| React.ComponentType<{ className?: string }>
| null
| undefined = undefined;
menuItems.forEach((item) => {
if (item.children) {
const activeChild = item.children.find(
@ -190,7 +176,10 @@ export function Sidebar({
}
});
if (onTitleChange) {
onTitleChange({ title: activeTitle ?? "صفحه اول", icon: activeIcon ?? null });
onTitleChange({
title: activeTitle ?? "صفحه اول",
icon: activeIcon ?? null,
});
}
};
@ -261,7 +250,7 @@ export function Sidebar({
<button
key={item.id}
className={cn(
"flex items-center justify-center w-full px-2 rounded-none mt-4 transition-all duration-200 group",
"flex items-center justify-center w-full px-2 rounded-none mt-4 transition-all duration-200 group"
)}
onClick={handleClick}
>
@ -271,8 +260,7 @@ export function Sidebar({
</span>
</div>
</button>
)
);
}
return (
@ -331,10 +319,10 @@ export function Sidebar({
"w-full text-right",
// Disable pointer cursor when child is active (cannot collapse)
item.children &&
item.children.some(
(child) => child.href && location.pathname === child.href
) &&
"cursor-not-allowed"
item.children.some(
(child) => child.href && location.pathname === child.href
) &&
"cursor-not-allowed"
)}
onClick={handleClick}
>
@ -435,9 +423,9 @@ export function Sidebar({
/>
<div className="font-persian">
<div className="text-sm font-semibold text-white">
اینوژن بندر امام
داشبورد مدیریت فناوری و نوآوری
</div>
<div className="text-xs text-gray-400">نسخه ۰.۱</div>
{/* <div className="text-xs text-gray-400">نسخه ۰.۱</div> */}
</div>
</div>
) : (

View File

@ -1,25 +1,25 @@
import React, { useEffect, useState } from "react";
import { useEffect, useReducer, useRef, useState } from "react";
import {
Dialog,
DialogContent,
DialogHeader,
DialogTitle,
} from "~/components/ui/dialog";
import {
BarChart,
Bar,
BarChart,
CartesianGrid,
Cell,
LabelList,
ResponsiveContainer,
XAxis,
YAxis,
CartesianGrid,
Tooltip,
ResponsiveContainer,
LabelList,
Cell,
} from "recharts";
import apiService from "~/lib/api";
import { Dialog, DialogContent, DialogHeader } from "~/components/ui/dialog";
import { Skeleton } from "~/components/ui/skeleton";
import apiService from "~/lib/api";
import { formatNumber } from "~/lib/utils";
import { ChartContainer } from "../ui/chart";
import {
DropdownMenu,
DropdownMenuButton,
DropdownMenuContent,
DropdownMenuItem,
} from "../ui/dropdown-menu";
import { TruncatedText } from "../ui/truncatedText";
interface StrategicAlignmentData {
@ -28,6 +28,51 @@ interface StrategicAlignmentData {
percentage?: number;
}
interface DropDownConfig {
isOpen: boolean;
selectedValue: string;
dropDownItems: Array<string>;
}
type Action =
| { type: "OPEN" }
| { type: "CLOSE" }
| { type: "SETVALUE"; value: Array<string> }
| { type: "SELECT"; value: string };
// const DropDownItems = [
// {
// id: 0,
// key: "همه مضامین",
// Value: "همه مضامین",
// },
// {
// id: 1,
// key: "ارزش های هم افزایی نوآورانه",
// Value: "همه مضامین",
// },
// {
// id: 2,
// key: "ارزش های خودکفایی نوآوورانه",
// Value: "همه مضامین",
// },
// {
// id: 3,
// key: "ارزش های فناوری های نوین",
// Value: "همه مضامین",
// },
// {
// id: 4,
// key: "ارزش های توسعه منابع انسانی",
// Value: "همه مضامین",
// },
// {
// id: 5,
// key: "ارزش های نوآوری سبز",
// Value: "همه مضامین",
// },
// ];
interface StrategicAlignmentPopupProps {
open: boolean;
onOpenChange: (open: boolean) => void;
@ -41,11 +86,10 @@ const chartConfig = {
},
};
const maxHeight = 150;
const barHeights = () => Math.floor(Math.random() * maxHeight);
const maxHeight = 150;
const barHeights = () => Math.floor(Math.random() * maxHeight);
const ChartSkeleton = () => (
<div className="flex justify-center h-96 w-full p-4">
{/* Chart bars */}
<div className=" w-full flex items-end gap-10">
@ -58,7 +102,7 @@ const ChartSkeleton = () => (
</div>
))}
</div>
{/* Left space for Y-axis label */}
{/* Left space for Y-axis label */}
<div className="flex flex-col justify-between mr-2">
<Skeleton className="h-6 w-15 bg-gray-700 rounded" />
<Skeleton className="h-6 w-15 bg-gray-700 rounded" />
@ -74,6 +118,12 @@ export function StrategicAlignmentPopup({
}: StrategicAlignmentPopupProps) {
const [data, setData] = useState<StrategicAlignmentData[]>([]);
const [loading, setLoading] = useState(false);
const contentRef = useRef<HTMLDivElement | null>(null);
const [state, dispatch] = useReducer(reducer, {
isOpen: false,
selectedValue: "همه مضامین",
dropDownItems: [],
});
useEffect(() => {
if (open) {
@ -98,29 +148,12 @@ export function StrategicAlignmentPopup({
? JSON.parse(response.data)
: response.data;
const processedData = responseData
.map((item: any) => ({
strategic_theme: item.strategic_theme || "N/A",
operational_fee_sum: Math.max(0, Number(item.operational_fee_sum)),
}))
.filter((item: StrategicAlignmentData) => item.strategic_theme !== "");
const total = processedData.reduce(
(acc: number, item: StrategicAlignmentData) =>
acc + item.operational_fee_sum,
0
setBarItems(responseData);
const dropDownItems = responseData.map(
(item: any) => item.strategic_theme
);
const dataWithPercentage = processedData.map(
(item: StrategicAlignmentData) => ({
...item,
percentage:
total > 0
? Math.round((item.operational_fee_sum / total) * 100)
: 0,
})
);
setData(dataWithPercentage || []);
setDropDownValues(["همه مضامین", ...dropDownItems]);
} catch (error) {
console.error("Error fetching strategic alignment data:", error);
} finally {
@ -128,19 +161,168 @@ export function StrategicAlignmentPopup({
}
};
const fetchDropDownItems = async (item: string) => {
try {
if (item !== "همه مضامین") {
const response = await apiService.select({
ProcessName: "project",
OutputFields: [
"value_technology_and_innovation",
"sum(operational_fee)",
],
Conditions: [["strategic_theme", "=", item]],
GroupBy: ["value_technology_and_innovation"],
});
const responseData =
typeof response.data === "string"
? JSON.parse(response.data)
: response.data;
setBarItems(responseData);
} else fetchData();
} catch (error) {
console.error("Error fetching strategic alignment data:", error);
} finally {
setLoading(false);
}
};
function reducer(state: DropDownConfig, action: Action): DropDownConfig {
switch (action.type) {
case "OPEN":
return { ...state, isOpen: true };
case "CLOSE":
return { ...state, isOpen: false };
case "SETVALUE":
return { ...state, dropDownItems: action.value };
case "SELECT":
return { ...state, selectedValue: action.value };
default:
return state;
}
}
const toggleMenuHandler = () => {
dispatch({
type: "OPEN",
});
};
const selectItem = (item: string) => {
dispatch({
type: "SELECT",
value: item,
});
dispatch({
type: "CLOSE",
});
fetchDropDownItems(item);
};
const setDropDownValues = (items: Array<string>) => {
dispatch({
type: "SETVALUE",
value: items,
});
};
const setBarItems = (responseData: any) => {
const processedData = responseData
.map((item: any) => ({
strategic_theme:
item.strategic_theme || item.value_technology_and_innovation || "N/A",
operational_fee_sum: Math.max(0, Number(item.operational_fee_sum)),
}))
.filter((item: StrategicAlignmentData) => item.strategic_theme !== "");
const total = processedData.reduce(
(acc: number, item: StrategicAlignmentData) =>
acc + item.operational_fee_sum,
0
);
const dataWithPercentage = processedData.map(
(item: StrategicAlignmentData) => ({
...item,
percentage:
total > 0 ? Math.round((item.operational_fee_sum / total) * 100) : 0,
})
);
setData(dataWithPercentage || []);
};
const dialogHandler = (status: boolean) => {
if (onOpenChange) onOpenChange(status);
dispatch({
type: "SELECT",
value: "همه مضامین",
});
};
useEffect(() => {
if (!open) return;
const handleClickOutside = (event: MouseEvent) => {
if (
contentRef.current &&
!contentRef.current.contains(event.target as Node)
) {
dispatch({
type: "CLOSE",
});
}
};
document.addEventListener("mousedown", handleClickOutside);
return () => {
document.removeEventListener("mousedown", handleClickOutside);
};
}, [open]);
return (
<Dialog open={open} onOpenChange={onOpenChange}>
<Dialog open={open} onOpenChange={dialogHandler}>
<DialogContent className="w-full max-w-4xl bg-[linear-gradient(to_bottom_left,#464861,50%,#111628)] text-white border-none">
<DialogHeader className="mb-10 py-2 w-full pb-4 border-b-2 border-gray-500/20">
<DialogTitle className="ml-auto text-sm text-white ">میزان انطباق راهبردی</DialogTitle>
<DialogHeader className="mb-10 w-full border-b-2 border-gray-500/20">
<div>
<div className="flex">
<DropdownMenu
modal={true}
open={state.isOpen}
onOpenChange={toggleMenuHandler}
>
<DropdownMenuButton>{state.selectedValue}</DropdownMenuButton>
<DropdownMenuContent
ref={contentRef}
forceMount={true}
className="w-56"
>
{state.dropDownItems.map((item: string, key: number) => (
<div
onClick={() => selectItem(item)}
key={`${key}-${item}`}
>
<DropdownMenuItem selected={state.selectedValue === item}>
{item}
</DropdownMenuItem>
</div>
))}
</DropdownMenuContent>
</DropdownMenu>
</div>
</div>
</DialogHeader>
{loading ? (
<ChartSkeleton />
) : (
<>
<ResponsiveContainer width="100%" height={400}>
<ChartContainer config={chartConfig} className="aspect-auto h-96 w-full">
<ResponsiveContainer width="100%" height={400}>
<ChartContainer
config={chartConfig}
className="aspect-auto h-96 w-full"
>
<BarChart
data={data}
margin={{ left: 12, right: 12 }}
@ -149,7 +331,7 @@ export function StrategicAlignmentPopup({
accessibilityLayer
>
<CartesianGrid vertical={false} stroke="#475569" />
<XAxis
<XAxis
dataKey="strategic_theme"
tickLine={false}
axisLine={false}
@ -161,11 +343,8 @@ export function StrategicAlignmentPopup({
return (
<g transform={`translate(${x},${y})`}>
<foreignObject width={80} height={20} x={-45} y={0}>
<TruncatedText
maxWords={2}
text={payload.value}
/>
</foreignObject>
<TruncatedText maxWords={2} text={payload.value} />
</foreignObject>
</g>
);
}}
@ -179,37 +358,38 @@ export function StrategicAlignmentPopup({
tickFormatter={(value) =>
`${formatNumber(Math.round(value))}`
}
label={{
value: "تعداد برنامه ها" ,
angle: -90,
position: "insideLeft",
fill: "#94a3b8",
fontSize: 11,
offset: 0,
dy: 0,
style: { textAnchor: "middle" },
}}
label={{
value: "تعداد برنامه ها",
angle: -90,
position: "insideLeft",
fill: "#94a3b8",
fontSize: 11,
offset: 0,
dy: 0,
style: { textAnchor: "middle" },
}}
/>
<Bar dataKey="percentage" radius={[8, 8, 0, 0]}>
<Bar dataKey="percentage" radius={[8, 8, 0, 0]}>
{data.map((entry, index) => (
<Cell key={`cell-${index}`} fill={chartConfig.percentage.color} />
<Cell
key={`cell-${index}`}
fill={chartConfig.percentage.color}
/>
))}
<LabelList
dataKey="percentage"
position="top"
offset={15}
offset={15}
style={{
fill: "#ffffff",
fontSize: "16px",
fontWeight: "bold",
}}
formatter={(v: number) => `${formatNumber(Math.round(v))}`}
formatter={(v: number) =>
`${formatNumber(Math.round(v))}`
}
/>
</Bar>
</BarChart>
</ChartContainer>

View File

@ -3,7 +3,6 @@ import * as d3 from "d3";
import apiService from "../../lib/api";
import { useAuth } from "../../contexts/auth-context";
// Get API base URL at module level to avoid process.env access in browser
const API_BASE_URL =
import.meta.env.VITE_API_URL || "https://inogen-back.pelekan.org/api";
@ -46,7 +45,6 @@ export interface NetworkGraphProps {
onNodeClick?: (node: CompanyDetails) => void;
}
// Helper to robustly parse backend response
function parseApiResponse(raw: any): any[] {
let data = raw;
try {
@ -56,7 +54,6 @@ function parseApiResponse(raw: any): any[] {
return Array.isArray(data) ? data : [];
}
// Check if we're in browser environment
function isBrowser(): boolean {
return typeof window !== "undefined";
}
@ -70,7 +67,6 @@ export function NetworkGraph({ onNodeClick }: NetworkGraphProps) {
const [error, setError] = useState<string | null>(null);
const { token } = useAuth();
// Ensure component only renders on client side
useEffect(() => {
if (isBrowser()) {
const timer = setTimeout(() => setIsMounted(true), 100);
@ -78,7 +74,22 @@ export function NetworkGraph({ onNodeClick }: NetworkGraphProps) {
}
}, []);
// Fetch data from API
const getImageUrl = useCallback(
(stageid: number) => {
if (!token?.accessToken) return null;
return `${API_BASE_URL}/getimage?stageID=${stageid}&nameOrID=image&token=${token.accessToken}`;
},
[token?.accessToken],
);
const callAPI = useCallback(async (stage_id: number) => {
return await apiService.call<any>({
get_values_workflow_function: {
stage_id: stage_id,
},
});
}, []);
useEffect(() => {
if (!isMounted) return;
@ -99,18 +110,28 @@ export function NetworkGraph({ onNodeClick }: NetworkGraphProps) {
Object.keys(data[0] || {}),
);
// Create center node
// نود مرکزی
const centerNode: Node = {
id: "center",
label: "پتروشیمی بندر امام", //مرکز زیست بوم
label: "پتروشیمی بندر امام",
category: "center",
stageid: 0,
isCenter: true,
};
// Create ecosystem nodes
const ecosystemNodes: Node[] = data.map((item: any) => ({
id: String(item.stageid),
// دسته‌بندی‌ها
const categories = Array.from(new Set(data.map((item: any) => item.category)));
const categoryNodes: Node[] = categories.map((cat, index) => ({
id: `cat-${index}`,
label: cat,
category: cat,
stageid: -1,
}));
// نودهای نهایی
const finalNodes: Node[] = data.map((item: any) => ({
id: `node-${item.stageid}`,
label: item.title,
category: item.category,
stageid: item.stageid,
@ -118,13 +139,16 @@ export function NetworkGraph({ onNodeClick }: NetworkGraphProps) {
rawData: item,
}));
// Create links (all nodes connected to center)
const graphLinks: Link[] = ecosystemNodes.map((node) => ({
source: "center",
target: node.id,
}));
// لینک‌ها: مرکز → دسته‌بندی‌ها → نودهای نهایی
const graphLinks: Link[] = [
...categoryNodes.map((cat) => ({ source: "center", target: cat.id })),
...finalNodes.map((node) => {
const catIndex = categories.indexOf(node.category);
return { source: `cat-${catIndex}`, target: node.id };
}),
];
setNodes([centerNode, ...ecosystemNodes]);
setNodes([centerNode, ...categoryNodes, ...finalNodes]);
setLinks(graphLinks);
} catch (err: any) {
if (err.name !== "AbortError") {
@ -142,43 +166,18 @@ export function NetworkGraph({ onNodeClick }: NetworkGraphProps) {
aborted = true;
controller.abort();
};
}, [isMounted, token]);
}, [isMounted, token, getImageUrl]);
// Get image URL for a node
const getImageUrl = useCallback(
(stageid: number) => {
if (!token?.accessToken) return null;
return `${API_BASE_URL}/getimage?stageID=${stageid}&nameOrID=image&token=${token.accessToken}`;
},
[token?.accessToken],
);
// Import apiService for the onClick handler
const callAPI = useCallback(async (stage_id: number) => {
return await apiService.call<any>({
get_values_workflow_function: {
stage_id: stage_id,
},
});
}, []);
// Initialize D3 graph
useEffect(() => {
if (!isMounted || !svgRef.current || isLoading || nodes.length === 0) {
return;
}
if (!isMounted || !svgRef.current || isLoading || nodes.length === 0) return;
const svg = d3.select(svgRef.current);
const width = svgRef.current.clientWidth;
const height = svgRef.current.clientHeight;
// Clear previous content
svg.selectAll("*").remove();
// Create defs for patterns and filters
const defs = svg.append("defs");
// Add glow filter for hover effect
const filter = defs
.append("filter")
.attr("id", "glow")
@ -196,20 +195,15 @@ export function NetworkGraph({ onNodeClick }: NetworkGraphProps) {
feMerge.append("feMergeNode").attr("in", "coloredBlur");
feMerge.append("feMergeNode").attr("in", "SourceGraphic");
// Create zoom behavior
const container = svg.append("g");
const zoom = d3
.zoom<SVGSVGElement, unknown>()
.scaleExtent([0.8, 2.5]) // Limit zoom out to 1x, zoom in to 2.5x
.on("zoom", (event) => {
container.attr("transform", event.transform);
});
.scaleExtent([0.3, 2.5])
.on("zoom", (event) => container.attr("transform", event.transform));
svg.call(zoom);
// Create container group
const container = svg.append("g");
// Category colors
const categoryToColor: Record<string, string> = {
دانشگاه: "#3B82F6",
مشاور: "#10B981",
@ -222,7 +216,6 @@ export function NetworkGraph({ onNodeClick }: NetworkGraphProps) {
center: "#34D399",
};
// Create force simulation
const simulation = d3
.forceSimulation<Node>(nodes)
.force(
@ -231,16 +224,15 @@ export function NetworkGraph({ onNodeClick }: NetworkGraphProps) {
.forceLink<Node, Link>(links)
.id((d) => d.id)
.distance(150)
.strength(0.1),
.strength(0.2),
)
.force("charge", d3.forceManyBody().strength(-300))
.force("center", d3.forceCenter(width / 2, height / 2))
.force(
"collision",
d3.forceCollide().radius((d) => (d.isCenter ? 40 : 30)),
);
.force("radial", d3.forceRadial(d => d.isCenter ? 0 : 300, width/2, height/2))
.force("collision", d3.forceCollide().radius((d) => (d.isCenter ? 50 : 35)));
const initialScale = 0.85;
// Initial zoom to show entire graph
const initialScale = 0.6;
const initialTranslate = [
width / 2 - (width / 2) * initialScale,
height / 2 - (height / 2) * initialScale,
@ -252,25 +244,60 @@ export function NetworkGraph({ onNodeClick }: NetworkGraphProps) {
.scale(initialScale),
);
// Fix center node position
const centerNode = nodes.find((n) => n.isCenter);
// Fix center node
const centerNode = nodes.find(n => n.isCenter);
const categoryNodes = nodes.filter(n => !n.isCenter && n.stageid === -1);
if (centerNode) {
centerNode.fx = width / 2;
centerNode.fy = height / 2;
const centerX = width / 2;
const centerY = height / 2;
centerNode.fx = centerX;
centerNode.fy = centerY;
const baseRadius = 450; // شعاع پایه
const variation = 100; // تغییر طول یکی در میان
const angleStep = (2 * Math.PI) / categoryNodes.length;
categoryNodes.forEach((catNode, i) => {
const angle = i * angleStep;
const radius = baseRadius + (i % 2 === 0 ? -variation : variation);
catNode.fx = centerX + radius * Math.cos(angle);
catNode.fy = centerY + radius * Math.sin(angle);
});
}
// Create links
// نودهای نهایی **هیچ fx/fy نداشته باشند**
// فقط forceLink آن‌ها را به دسته‌ها متصل نگه می‌دارد
// const finalNodes = nodes.filter(n => !n.isCenter && n.stageid !== -1);
// categoryNodes.forEach((catNode) => {
// const childNodes = finalNodes.filter(n => n.category === catNode.category);
// const childCount = childNodes.length;
// const radius = 100; // فاصله از دسته
// const angleStep = (2 * Math.PI) / childCount;
// childNodes.forEach((node, i) => {
// const angle = i * angleStep;
// node.fx = catNode.fx! + radius * Math.cos(angle);
// node.fy = catNode.fy! + radius * Math.sin(angle);
// });
// });
// Curved links
const link = container
.selectAll(".link")
.data(links)
.enter()
.append("line")
.append("path")
.attr("class", "link")
.attr("stroke", "#E2E8F0")
.attr("stroke-width", 2)
.attr("stroke-opacity", 0.6);
.attr("stroke-opacity", 0.6)
.attr("fill", "none");
// Create node groups
const nodeGroup = container
.selectAll(".node")
.data(nodes)
@ -279,7 +306,6 @@ export function NetworkGraph({ onNodeClick }: NetworkGraphProps) {
.attr("class", "node")
.style("cursor", "pointer");
// Add drag behavior
const drag = d3
.drag<SVGGElement, Node>()
.on("start", (event, d) => {
@ -301,18 +327,16 @@ export function NetworkGraph({ onNodeClick }: NetworkGraphProps) {
nodeGroup.call(drag);
// Add node circles/rectangles
nodeGroup.each(function (d) {
const group = d3.select(this);
if (d.isCenter) {
// Center node as rectangle
const rect = group
.append("rect")
.attr("width", 150)
.attr("height", 60)
.attr("x", -75)
.attr("y", -30)
.attr("width", 200)
.attr("height", 80)
.attr("x", -100) // نصف عرض جدید منفی
.attr("y", -40) // نصف ارتفاع جدید منفی
.attr("rx", 8)
.attr("ry", 8)
.attr("fill", categoryToColor[d.category] || "#94A3B8")
@ -320,7 +344,6 @@ export function NetworkGraph({ onNodeClick }: NetworkGraphProps) {
.attr("stroke-width", 3)
.style("pointer-events", "none");
// Add center image if available
if (d.imageUrl || d.isCenter) {
const pattern = defs
.append("pattern")
@ -334,23 +357,21 @@ export function NetworkGraph({ onNodeClick }: NetworkGraphProps) {
.append("image")
.attr("x", 0)
.attr("y", 0)
.attr("width", 150)
.attr("height", 60)
.attr("width", 200) // ← هم‌اندازه با مستطیل
.attr("height", 80)
.attr("href", d.isCenter ? "/main-circle.png" : d.imageUrl)
.attr("preserveAspectRatio", "xMidYMid slice");
rect.attr("fill", `url(#image-${d.id})`);
}
} else {
// Regular nodes as circles
const circle = group
.append("circle")
.attr("r", 25)
.attr("fill", categoryToColor[d.category] || "8#fff")
.attr("fill", categoryToColor[d.category] || "#fff")
.attr("stroke", "#FFFFFF")
.attr("stroke-width", 3);
// Add node image if available
if (d.imageUrl) {
const pattern = defs
.append("pattern")
@ -367,10 +388,8 @@ export function NetworkGraph({ onNodeClick }: NetworkGraphProps) {
.attr("width", 50)
.attr("height", 50)
.attr("href", d.imageUrl)
.attr("backgroundColor", "#fff")
.attr("preserveAspectRatio", "xMidYMid slice");
// Create circular clip path
defs
.append("clipPath")
.attr("id", `clip-${d.id}`)
@ -384,7 +403,6 @@ export function NetworkGraph({ onNodeClick }: NetworkGraphProps) {
}
});
// Add labels below nodes
const labels = nodeGroup
.append("text")
.text((d) => d.label)
@ -397,7 +415,6 @@ export function NetworkGraph({ onNodeClick }: NetworkGraphProps) {
.attr("stroke-width", 4)
.attr("paint-order", "stroke");
// Add hover effects
nodeGroup
.on("mouseenter", function (event, d) {
if (d.isCenter) return;
@ -419,22 +436,17 @@ export function NetworkGraph({ onNodeClick }: NetworkGraphProps) {
.attr("stroke-width", 3);
});
// Add click handlers
nodeGroup.on("click", async function (event, d) {
event.stopPropagation();
// Don't handle center node clicks
if (d.isCenter) return;
if (onNodeClick && d.stageid) {
try {
// Fetch detailed company data
const res = await callAPI(d.stageid);
const responseData = JSON.parse(res.data);
const fieldValues =
JSON.parse(responseData?.getvalue)?.[0]?.FieldValues || [];
// Filter out image fields and find description
const filteredFields = fieldValues.filter(
(field: any) =>
!["image", "img", "full_name", "about_collaboration"].includes(
@ -461,7 +473,6 @@ export function NetworkGraph({ onNodeClick }: NetworkGraphProps) {
onNodeClick(companyDetails);
} catch (error) {
console.error("Failed to fetch company details:", error);
// Fallback to basic info
const basicDetails: CompanyDetails = {
id: d.id,
label: d.label,
@ -474,24 +485,26 @@ export function NetworkGraph({ onNodeClick }: NetworkGraphProps) {
}
});
// Update positions on simulation tick
simulation.on("tick", () => {
link
.attr("x1", (d) => (d.source as Node).x!)
.attr("y1", (d) => (d.source as Node).y!)
.attr("x2", (d) => (d.target as Node).x!)
.attr("y2", (d) => (d.target as Node).y!);
link.attr("d", (d: any) => {
const sx = (d.source as Node).x!;
const sy = (d.source as Node).y!;
const tx = (d.target as Node).x!;
const ty = (d.target as Node).y!;
const dx = tx - sx;
const dy = ty - sy;
const dr = Math.sqrt(dx * dx + dy * dy) * 1.2; // منحنی
return `M${sx},${sy}A${dr},${dr} 0 0,1 ${tx},${ty}`;
});
nodeGroup.attr("transform", (d) => `translate(${d.x},${d.y})`);
});
// Cleanup function
return () => {
simulation.stop();
};
}, [nodes, links, isLoading, isMounted, onNodeClick, callAPI]);
// Show error message
if (error) {
return (
<div className="w-full h-full flex items-center justify-center bg-gradient-to-br from-gray-900 to-gray-800">
@ -505,7 +518,6 @@ export function NetworkGraph({ onNodeClick }: NetworkGraphProps) {
);
}
// Don't render on server side
if (!isMounted) {
return (
<div className="w-full h-full flex items-center justify-center bg-transparent">
@ -519,14 +531,11 @@ export function NetworkGraph({ onNodeClick }: NetworkGraphProps) {
if (isLoading) {
return (
<div className="w-full h-full relative bg-transparent">
{/* Skeleton Graph Container */}
<div className="w-full h-full flex items-center justify-center relative">
{/* Center Node Skeleton */}
<div className="w-12 h-12 rounded-lg bg-gray-600 animate-pulse relative z-10">
<div className="absolute inset-0 rounded-lg bg-gradient-to-r from-gray-500 to-gray-600 animate-pulse"></div>
</div>
{/* Outer Ring Nodes Skeleton */}
{Array.from({ length: 8 }).map((_, i) => {
const angle = (i * 2 * Math.PI) / 8;
const radius = 120;
@ -547,42 +556,28 @@ export function NetworkGraph({ onNodeClick }: NetworkGraphProps) {
<div
className="absolute w-16 h-3 bg-gray-600 rounded animate-pulse"
style={{
left: "50%",
top: "40px",
transform: "translateX(-50%)",
animationDelay: `${i * 200 + 100}ms`,
}}
></div>
<div
className="absolute w-0.5 bg-gray-600 animate-pulse opacity-30"
style={{
left: "50%",
top: "50%",
height: `${radius - 16}px`,
transformOrigin: "top",
transform: `translateX(-50%) rotate(${angle + Math.PI}rad)`,
animationDelay: `${i * 100}ms`,
transform: `rotate(${(i * 360) / 8}deg) translateX(32px)`,
transformOrigin: "left center",
}}
></div>
</div>
);
})}
</div>
<div className="absolute bottom-6 left-1/2 transform -translate-x-1/2">
<div className="text-white font-persian text-sm animate-pulse">
در حال بارگذاری نمودار...
</div>
</div>
</div>
);
}
return (
<div className="w-full h-full relative bg-transparent overflow-hidden">
<svg ref={svgRef} className="w-full h-full" style={{ minHeight: 500 }} />
<div className="w-full h-full">
<svg
ref={svgRef}
className="w-full h-full bg-transparent"
style={{ cursor: "grab" }}
/>
</div>
);
}
export default NetworkGraph;

View File

@ -1,18 +1,18 @@
"use client"
"use client";
import * as React from "react"
import * as DialogPrimitive from "@radix-ui/react-dialog"
import { X } from "lucide-react"
import * as DialogPrimitive from "@radix-ui/react-dialog";
import { X } from "lucide-react";
import * as React from "react";
import { cn } from "~/lib/utils"
import { cn } from "~/lib/utils";
const Dialog = DialogPrimitive.Root
const Dialog = DialogPrimitive.Root;
const DialogTrigger = DialogPrimitive.Trigger
const DialogTrigger = DialogPrimitive.Trigger;
const DialogPortal = DialogPrimitive.Portal
const DialogPortal = DialogPrimitive.Portal;
const DialogClose = DialogPrimitive.Close
const DialogClose = DialogPrimitive.Close;
const DialogOverlay = React.forwardRef<
React.ElementRef<typeof DialogPrimitive.Overlay>,
@ -26,8 +26,8 @@ const DialogOverlay = React.forwardRef<
)}
{...props}
/>
))
DialogOverlay.displayName = DialogPrimitive.Overlay.displayName
));
DialogOverlay.displayName = DialogPrimitive.Overlay.displayName;
const DialogContent = React.forwardRef<
React.ElementRef<typeof DialogPrimitive.Content>,
@ -38,7 +38,7 @@ const DialogContent = React.forwardRef<
<DialogPrimitive.Content
ref={ref}
className={cn(
"fixed left-[50%] top-[50%] z-50 grid w-full max-w-lg translate-x-[-50%] translate-y-[-50%] gap-4 border bg-background p-6 shadow-lg duration-200 data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[state=closed]:slide-out-to-left-1/2 data-[state=closed]:slide-out-to-top-[48%] data-[state=open]:slide-in-from-left-1/2 data-[state=open]:slide-in-from-top-[48%] sm:rounded-lg",
"fixed left-[50%] top-[50%] z-50 grid w-full max-w-lg translate-x-[-50%] translate-y-[-50%] gap-4 border bg-background shadow-lg duration-200 data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[state=closed]:slide-out-to-left-1/2 data-[state=closed]:slide-out-to-top-[48%] data-[state=open]:slide-in-from-left-1/2 data-[state=open]:slide-in-from-top-[48%] sm:rounded-lg",
className
)}
{...props}
@ -50,8 +50,8 @@ const DialogContent = React.forwardRef<
</DialogPrimitive.Close>
</DialogPrimitive.Content>
</DialogPortal>
))
DialogContent.displayName = DialogPrimitive.Content.displayName
));
DialogContent.displayName = DialogPrimitive.Content.displayName;
const DialogHeader = ({
className,
@ -59,13 +59,13 @@ const DialogHeader = ({
}: React.HTMLAttributes<HTMLDivElement>) => (
<div
className={cn(
"flex flex-col space-y-1.5 text-center sm:text-left",
"flex flex-col p-4 space-y-1.5 text-center sm:text-left",
className
)}
{...props}
/>
)
DialogHeader.displayName = "DialogHeader"
);
DialogHeader.displayName = "DialogHeader";
const DialogFooter = ({
className,
@ -78,8 +78,8 @@ const DialogFooter = ({
)}
{...props}
/>
)
DialogFooter.displayName = "DialogFooter"
);
DialogFooter.displayName = "DialogFooter";
const DialogTitle = React.forwardRef<
React.ElementRef<typeof DialogPrimitive.Title>,
@ -93,8 +93,8 @@ const DialogTitle = React.forwardRef<
)}
{...props}
/>
))
DialogTitle.displayName = DialogPrimitive.Title.displayName
));
DialogTitle.displayName = DialogPrimitive.Title.displayName;
const DialogDescription = React.forwardRef<
React.ElementRef<typeof DialogPrimitive.Description>,
@ -105,18 +105,18 @@ const DialogDescription = React.forwardRef<
className={cn("text-sm text-muted-foreground", className)}
{...props}
/>
))
DialogDescription.displayName = DialogPrimitive.Description.displayName
));
DialogDescription.displayName = DialogPrimitive.Description.displayName;
export {
Dialog,
DialogPortal,
DialogOverlay,
DialogClose,
DialogTrigger,
DialogContent,
DialogHeader,
DialogFooter,
DialogTitle,
DialogDescription,
}
DialogFooter,
DialogHeader,
DialogOverlay,
DialogPortal,
DialogTitle,
DialogTrigger,
};

View File

@ -1,27 +1,27 @@
"use client"
"use client";
import * as React from "react"
import * as DropdownMenuPrimitive from "@radix-ui/react-dropdown-menu"
import { Check, ChevronRight, Circle } from "lucide-react"
import * as DropdownMenuPrimitive from "@radix-ui/react-dropdown-menu";
import { Check, ChevronDown, Circle } from "lucide-react";
import * as React from "react";
import { cn } from "~/lib/utils"
import { cn } from "~/lib/utils";
const DropdownMenu = DropdownMenuPrimitive.Root
const DropdownMenu = DropdownMenuPrimitive.Root;
const DropdownMenuTrigger = DropdownMenuPrimitive.Trigger
const DropdownMenuTrigger = DropdownMenuPrimitive.Trigger;
const DropdownMenuGroup = DropdownMenuPrimitive.Group
const DropdownMenuGroup = DropdownMenuPrimitive.Group;
const DropdownMenuPortal = DropdownMenuPrimitive.Portal
const DropdownMenuPortal = DropdownMenuPrimitive.Portal;
const DropdownMenuSub = DropdownMenuPrimitive.Sub
const DropdownMenuSub = DropdownMenuPrimitive.Sub;
const DropdownMenuRadioGroup = DropdownMenuPrimitive.RadioGroup
const DropdownMenuRadioGroup = DropdownMenuPrimitive.RadioGroup;
const DropdownMenuSubTrigger = React.forwardRef<
React.ElementRef<typeof DropdownMenuPrimitive.SubTrigger>,
React.ComponentPropsWithoutRef<typeof DropdownMenuPrimitive.SubTrigger> & {
inset?: boolean
inset?: boolean;
}
>(({ className, inset, children, ...props }, ref) => (
<DropdownMenuPrimitive.SubTrigger
@ -34,11 +34,10 @@ const DropdownMenuSubTrigger = React.forwardRef<
{...props}
>
{children}
<ChevronRight className="ml-auto h-4 w-4" />
</DropdownMenuPrimitive.SubTrigger>
))
));
DropdownMenuSubTrigger.displayName =
DropdownMenuPrimitive.SubTrigger.displayName
DropdownMenuPrimitive.SubTrigger.displayName;
const DropdownMenuSubContent = React.forwardRef<
React.ElementRef<typeof DropdownMenuPrimitive.SubContent>,
@ -52,9 +51,9 @@ const DropdownMenuSubContent = React.forwardRef<
)}
{...props}
/>
))
));
DropdownMenuSubContent.displayName =
DropdownMenuPrimitive.SubContent.displayName
DropdownMenuPrimitive.SubContent.displayName;
const DropdownMenuContent = React.forwardRef<
React.ElementRef<typeof DropdownMenuPrimitive.Content>,
@ -65,32 +64,34 @@ const DropdownMenuContent = React.forwardRef<
ref={ref}
sideOffset={sideOffset}
className={cn(
"z-50 min-w-[8rem] overflow-hidden rounded-md border bg-popover p-1 text-popover-foreground shadow-md data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2",
"z-50 min-w-[8rem] overflow-hidden mt-1 rounded-xl border border-gray-500 bg-pr-gray p-1 text-popover-foreground shadow-md data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2",
className
)}
{...props}
/>
</DropdownMenuPrimitive.Portal>
))
DropdownMenuContent.displayName = DropdownMenuPrimitive.Content.displayName
));
DropdownMenuContent.displayName = DropdownMenuPrimitive.Content.displayName;
const DropdownMenuItem = React.forwardRef<
React.ElementRef<typeof DropdownMenuPrimitive.Item>,
React.ComponentPropsWithoutRef<typeof DropdownMenuPrimitive.Item> & {
inset?: boolean
inset?: boolean;
selected?: boolean;
}
>(({ className, inset, ...props }, ref) => (
>(({ className, inset, selected, ...props }, ref) => (
<DropdownMenuPrimitive.Item
ref={ref}
className={cn(
"relative flex cursor-default select-none items-center rounded-sm px-2 py-1.5 text-sm outline-none transition-colors focus:bg-accent focus:text-accent-foreground data-[disabled]:pointer-events-none data-[disabled]:opacity-50",
"cursor-pointer select-none rounded-md px-2 py-1.5 text-sm outline-none transition-colors hover:bg-dark-blue data-[disabled]:pointer-events-none data-[disabled]:opacity-50",
inset && "pl-8",
selected && "bg-dark-blue text-white",
className
)}
{...props}
/>
))
DropdownMenuItem.displayName = DropdownMenuPrimitive.Item.displayName
));
DropdownMenuItem.displayName = DropdownMenuPrimitive.Item.displayName;
const DropdownMenuCheckboxItem = React.forwardRef<
React.ElementRef<typeof DropdownMenuPrimitive.CheckboxItem>,
@ -112,9 +113,9 @@ const DropdownMenuCheckboxItem = React.forwardRef<
</span>
{children}
</DropdownMenuPrimitive.CheckboxItem>
))
));
DropdownMenuCheckboxItem.displayName =
DropdownMenuPrimitive.CheckboxItem.displayName
DropdownMenuPrimitive.CheckboxItem.displayName;
const DropdownMenuRadioItem = React.forwardRef<
React.ElementRef<typeof DropdownMenuPrimitive.RadioItem>,
@ -135,13 +136,13 @@ const DropdownMenuRadioItem = React.forwardRef<
</span>
{children}
</DropdownMenuPrimitive.RadioItem>
))
DropdownMenuRadioItem.displayName = DropdownMenuPrimitive.RadioItem.displayName
));
DropdownMenuRadioItem.displayName = DropdownMenuPrimitive.RadioItem.displayName;
const DropdownMenuLabel = React.forwardRef<
React.ElementRef<typeof DropdownMenuPrimitive.Label>,
React.ComponentPropsWithoutRef<typeof DropdownMenuPrimitive.Label> & {
inset?: boolean
inset?: boolean;
}
>(({ className, inset, ...props }, ref) => (
<DropdownMenuPrimitive.Label
@ -153,8 +154,8 @@ const DropdownMenuLabel = React.forwardRef<
)}
{...props}
/>
))
DropdownMenuLabel.displayName = DropdownMenuPrimitive.Label.displayName
));
DropdownMenuLabel.displayName = DropdownMenuPrimitive.Label.displayName;
const DropdownMenuSeparator = React.forwardRef<
React.ElementRef<typeof DropdownMenuPrimitive.Separator>,
@ -165,8 +166,8 @@ const DropdownMenuSeparator = React.forwardRef<
className={cn("-mx-1 my-1 h-px bg-muted", className)}
{...props}
/>
))
DropdownMenuSeparator.displayName = DropdownMenuPrimitive.Separator.displayName
));
DropdownMenuSeparator.displayName = DropdownMenuPrimitive.Separator.displayName;
const DropdownMenuShortcut = ({
className,
@ -177,24 +178,43 @@ const DropdownMenuShortcut = ({
className={cn("ml-auto text-xs tracking-widest opacity-60", className)}
{...props}
/>
)
}
DropdownMenuShortcut.displayName = "DropdownMenuShortcut"
);
};
DropdownMenuShortcut.displayName = "DropdownMenuShortcut";
const DropdownMenuButton = React.forwardRef<
React.ElementRef<typeof DropdownMenuPrimitive.Trigger>,
React.ComponentPropsWithoutRef<typeof DropdownMenuPrimitive.Trigger>
>(({ className, children, ...props }, ref) => (
<DropdownMenuPrimitive.Trigger
ref={ref}
className={cn(
"flex items-center justify-between gap-2 text-sm outline-none border border-gray-500 p-3 rounded-xl min-w-50 max-w-72 group",
className
)}
{...props}
>
{children}
<ChevronDown className="h-4 w-4 transition-transform duration-200 group-data-[state=open]:rotate-180" />
</DropdownMenuPrimitive.Trigger>
));
DropdownMenuButton.displayName = "DropdownMenuButton";
export {
DropdownMenu,
DropdownMenuTrigger,
DropdownMenuContent,
DropdownMenuItem,
DropdownMenuButton,
DropdownMenuCheckboxItem,
DropdownMenuRadioItem,
DropdownMenuContent,
DropdownMenuGroup,
DropdownMenuItem,
DropdownMenuLabel,
DropdownMenuPortal,
DropdownMenuRadioGroup,
DropdownMenuRadioItem,
DropdownMenuSeparator,
DropdownMenuShortcut,
DropdownMenuGroup,
DropdownMenuPortal,
DropdownMenuSub,
DropdownMenuSubContent,
DropdownMenuSubTrigger,
DropdownMenuRadioGroup,
}
DropdownMenuTrigger,
};

View File

@ -4,7 +4,7 @@ import { cn } from "~/lib/utils"
interface TableProps extends React.HTMLAttributes<HTMLTableElement> {
containerClassName?: string
containerRef?: React.RefObject<HTMLDivElement>
containerRef?: React.RefObject<HTMLDivElement | null>
}
const Table = React.forwardRef<HTMLTableElement, TableProps>(

View File

@ -70,7 +70,7 @@ export default function EcosystemPage() {
return (
<ProtectedRoute requireAuth={true}>
<DashboardLayout title="زیست بوم فناوری">
<div className="">
<div>
<div className="grid grid-cols-1 items-start lg:grid-cols-12 gap-4">
<div className="lg:col-span-4">
<InfoPanel selectedCompany={selectedCompany} />