Compare commits
9 Commits
31955592dd
...
67815aec2d
| Author | SHA1 | Date | |
|---|---|---|---|
| 67815aec2d | |||
| e10c25fc3e | |||
|
|
b4b023ec32 | ||
|
|
9d0fd5968b | ||
|
|
cacf40938f | ||
|
|
ef96cb4778 | ||
|
|
d4fd97daaa | ||
|
|
b60216c71d | ||
|
|
921afe42fa |
644
app/app.css
644
app/app.css
|
|
@ -2,51 +2,51 @@
|
||||||
@import "tailwindcss";
|
@import "tailwindcss";
|
||||||
|
|
||||||
@theme {
|
@theme {
|
||||||
/* Teal color scale */
|
/* Teal color scale */
|
||||||
--color-teal-50: #f0fdfa;
|
--color-teal-50: #f0fdfa;
|
||||||
--color-teal-100: #ccfbf1;
|
--color-teal-100: #ccfbf1;
|
||||||
--color-teal-200: #99f6e4;
|
--color-teal-200: #99f6e4;
|
||||||
--color-teal-300: #5eead4;
|
--color-teal-300: #5eead4;
|
||||||
--color-teal-400: #2dd4bf;
|
--color-teal-400: #2dd4bf;
|
||||||
--color-teal-500: #14b8a6;
|
--color-teal-500: #14b8a6;
|
||||||
--color-teal-600: #0d9488;
|
--color-teal-600: #0d9488;
|
||||||
--color-teal-700: #0f766e;
|
--color-teal-700: #0f766e;
|
||||||
--color-teal-800: #115e59;
|
--color-teal-800: #115e59;
|
||||||
--color-teal-900: #134e4a;
|
--color-teal-900: #134e4a;
|
||||||
--color-teal-950: #042f2e;
|
--color-teal-950: #042f2e;
|
||||||
|
|
||||||
/* Slate color scale */
|
/* Slate color scale */
|
||||||
--color-slate-50: #f8fafc;
|
--color-slate-50: #f8fafc;
|
||||||
--color-slate-100: #f1f5f9;
|
--color-slate-100: #f1f5f9;
|
||||||
--color-slate-200: #e2e8f0;
|
--color-slate-200: #e2e8f0;
|
||||||
--color-slate-300: #cbd5e1;
|
--color-slate-300: #cbd5e1;
|
||||||
--color-slate-400: #94a3b8;
|
--color-slate-400: #94a3b8;
|
||||||
--color-slate-500: #64748b;
|
--color-slate-500: #64748b;
|
||||||
--color-slate-600: #475569;
|
--color-slate-600: #475569;
|
||||||
--color-slate-700: #334155;
|
--color-slate-700: #334155;
|
||||||
--color-slate-800: #1e293b;
|
--color-slate-800: #1e293b;
|
||||||
--color-slate-900: #0f172a;
|
--color-slate-900: #0f172a;
|
||||||
--color-slate-950: #020617;
|
--color-slate-950: #020617;
|
||||||
|
|
||||||
--color-pr-green: #3aea83;
|
--color-pr-green: #3aea83;
|
||||||
--color-pr-blue: #69c8ea;
|
--color-pr-blue: #69c8ea;
|
||||||
--color-pr-red: #f76276;
|
--color-pr-red: #f76276;
|
||||||
--color-pr-gray: #3f415a;
|
--color-pr-gray: #3f415a;
|
||||||
}
|
}
|
||||||
|
|
||||||
html,
|
html,
|
||||||
body {
|
body {
|
||||||
@apply bg-background text-foreground;
|
@apply bg-background text-foreground;
|
||||||
@media (prefers-color-scheme: dark) {
|
@media (prefers-color-scheme: dark) {
|
||||||
color-scheme: dark;
|
color-scheme: dark;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
body {
|
body {
|
||||||
font-family: IRANYekanX;
|
font-family: IRANYekanX;
|
||||||
direction: rtl;
|
direction: rtl;
|
||||||
background-color: #cdcdcd;
|
background-color: #cdcdcd;
|
||||||
margin: 0;
|
margin: 0;
|
||||||
}
|
}
|
||||||
h1,
|
h1,
|
||||||
h2,
|
h2,
|
||||||
|
|
@ -56,378 +56,380 @@ h5,
|
||||||
h6,
|
h6,
|
||||||
input,
|
input,
|
||||||
textarea {
|
textarea {
|
||||||
font-family: IRANYekanX;
|
font-family: IRANYekanX;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* RTL Support */
|
/* RTL Support */
|
||||||
html[dir="rtl"] {
|
html[dir="rtl"] {
|
||||||
direction: rtl;
|
direction: rtl;
|
||||||
}
|
}
|
||||||
|
|
||||||
html[dir="rtl"] body {
|
html[dir="rtl"] body {
|
||||||
text-align: right;
|
text-align: right;
|
||||||
}
|
}
|
||||||
|
|
||||||
@theme inline {
|
@theme inline {
|
||||||
--radius-sm: calc(var(--radius) - 4px);
|
--radius-sm: calc(var(--radius) - 4px);
|
||||||
--radius-md: calc(var(--radius) - 2px);
|
--radius-md: calc(var(--radius) - 2px);
|
||||||
--radius-lg: var(--radius);
|
--radius-lg: var(--radius);
|
||||||
--radius-xl: calc(var(--radius) + 4px);
|
--radius-xl: calc(var(--radius) + 4px);
|
||||||
--color-background: var(--background);
|
--color-background: var(--background);
|
||||||
--color-foreground: var(--foreground);
|
--color-foreground: var(--foreground);
|
||||||
--color-card: var(--card);
|
--color-card: var(--card);
|
||||||
--color-card-foreground: var(--card-foreground);
|
--color-card-foreground: var(--card-foreground);
|
||||||
--color-popover: var(--popover);
|
--color-popover: var(--popover);
|
||||||
--color-popover-foreground: var(--popover-foreground);
|
--color-dark-blue: var(--dark-blue);
|
||||||
--color-primary: var(--primary);
|
--color-popover-foreground: var(--popover-foreground);
|
||||||
--color-primary-foreground: var(--primary-foreground);
|
--color-primary: var(--primary);
|
||||||
--color-secondary: var(--secondary);
|
--color-primary-foreground: var(--primary-foreground);
|
||||||
--color-secondary-foreground: var(--secondary-foreground);
|
--color-secondary: var(--secondary);
|
||||||
--color-muted: var(--muted);
|
--color-secondary-foreground: var(--secondary-foreground);
|
||||||
--color-muted-foreground: var(--muted-foreground);
|
--color-muted: var(--muted);
|
||||||
--color-accent: var(--accent);
|
--color-muted-foreground: var(--muted-foreground);
|
||||||
--color-accent-foreground: var(--accent-foreground);
|
--color-accent: var(--accent);
|
||||||
--color-destructive: var(--destructive);
|
--color-accent-foreground: var(--accent-foreground);
|
||||||
--color-destructive-foreground: var(--destructive-foreground);
|
--color-destructive: var(--destructive);
|
||||||
--color-border: var(--border);
|
--color-destructive-foreground: var(--destructive-foreground);
|
||||||
--color-input: var(--input);
|
--color-border: var(--border);
|
||||||
--color-ring: var(--ring);
|
--color-input: var(--input);
|
||||||
|
--color-ring: var(--ring);
|
||||||
}
|
}
|
||||||
|
|
||||||
:root {
|
:root {
|
||||||
--radius: 0.5rem;
|
--radius: 0.5rem;
|
||||||
|
|
||||||
--color-green: #3aea83;
|
--color-green: #3aea83;
|
||||||
--color-blue: #69c8ea;
|
--color-blue: #69c8ea;
|
||||||
--color-red: #f76276;
|
--color-red: #f76276;
|
||||||
|
|
||||||
/* primary colors */
|
/* primary colors */
|
||||||
--color-pr-gray: #3f415a;
|
--color-pr-gray: #3f415a;
|
||||||
--color-pr-green: var(--color-green);
|
--color-pr-green: var(--color-green);
|
||||||
|
|
||||||
/* Light theme colors */
|
/* Light theme colors */
|
||||||
--background: #ffffff;
|
--background: #ffffff;
|
||||||
--foreground: #0a0a0a;
|
--foreground: #0a0a0a;
|
||||||
--card: #ffffff;
|
--card: #ffffff;
|
||||||
--card-foreground: #0a0a0a;
|
--card-foreground: #0a0a0a;
|
||||||
--popover: #ffffff;
|
--popover: #ffffff;
|
||||||
--popover-foreground: #0a0a0a;
|
--popover-foreground: #0a0a0a;
|
||||||
--primary: #22c55e;
|
--primary: #22c55e;
|
||||||
--primary-foreground: #ffffff;
|
--primary-foreground: #ffffff;
|
||||||
--secondary: #f5f5f5;
|
--secondary: #f5f5f5;
|
||||||
--secondary-foreground: #0a0a0a;
|
--secondary-foreground: #0a0a0a;
|
||||||
--muted: #f5f5f5;
|
--muted: #f5f5f5;
|
||||||
--muted-foreground: #737373;
|
--muted-foreground: #737373;
|
||||||
--accent: #f5f5f5;
|
--accent: #f5f5f5;
|
||||||
--accent-foreground: #0a0a0a;
|
--accent-foreground: #0a0a0a;
|
||||||
--destructive: #ef4444;
|
--destructive: #ef4444;
|
||||||
--destructive-foreground: #ffffff;
|
--destructive-foreground: #ffffff;
|
||||||
--border: #e5e5e5;
|
--border: #e5e5e5;
|
||||||
--input: #e5e5e5;
|
--input: #e5e5e5;
|
||||||
--ring: #22c55e;
|
--ring: #22c55e;
|
||||||
|
--dark-blue: #33364d;
|
||||||
|
|
||||||
/* Primary color scale */
|
/* Primary color scale */
|
||||||
--color-primary-50: #f0fdf4;
|
--color-primary-50: #f0fdf4;
|
||||||
--color-primary-100: #dcfce7;
|
--color-primary-100: #dcfce7;
|
||||||
--color-primary-200: #bbf7d0;
|
--color-primary-200: #bbf7d0;
|
||||||
--color-primary-300: #86efac;
|
--color-primary-300: #86efac;
|
||||||
--color-primary-400: #4ade80;
|
--color-primary-400: #4ade80;
|
||||||
--color-primary-500: #22c55e;
|
--color-primary-500: #22c55e;
|
||||||
--color-primary-600: #16a34a;
|
--color-primary-600: #16a34a;
|
||||||
--color-primary-700: #15803d;
|
--color-primary-700: #15803d;
|
||||||
--color-primary-800: #166534;
|
--color-primary-800: #166534;
|
||||||
--color-primary-900: #14532d;
|
--color-primary-900: #14532d;
|
||||||
--color-primary-950: #052e16;
|
--color-primary-950: #052e16;
|
||||||
|
|
||||||
/* Secondary color scale (Blue) */
|
/* Secondary color scale (Blue) */
|
||||||
--color-secondary-50: #eff6ff;
|
--color-secondary-50: #eff6ff;
|
||||||
--color-secondary-100: #dbeafe;
|
--color-secondary-100: #dbeafe;
|
||||||
--color-secondary-200: #bfdbfe;
|
--color-secondary-200: #bfdbfe;
|
||||||
--color-secondary-300: #93c5fd;
|
--color-secondary-300: #93c5fd;
|
||||||
--color-secondary-400: #60a5fa;
|
--color-secondary-400: #60a5fa;
|
||||||
--color-secondary-500: #3b82f6;
|
--color-secondary-500: #3b82f6;
|
||||||
--color-secondary-600: #2563eb;
|
--color-secondary-600: #2563eb;
|
||||||
--color-secondary-700: #1d4ed8;
|
--color-secondary-700: #1d4ed8;
|
||||||
--color-secondary-800: #1e40af;
|
--color-secondary-800: #1e40af;
|
||||||
--color-secondary-900: #1e3a8a;
|
--color-secondary-900: #1e3a8a;
|
||||||
--color-secondary-950: #172554;
|
--color-secondary-950: #172554;
|
||||||
|
|
||||||
/* Neutral color scale */
|
/* Neutral color scale */
|
||||||
--color-neutral-50: #fafafa;
|
--color-neutral-50: #fafafa;
|
||||||
--color-neutral-100: #f5f5f5;
|
--color-neutral-100: #f5f5f5;
|
||||||
--color-neutral-200: #e5e5e5;
|
--color-neutral-200: #e5e5e5;
|
||||||
--color-neutral-300: #d4d4d4;
|
--color-neutral-300: #d4d4d4;
|
||||||
--color-neutral-400: #a3a3a3;
|
--color-neutral-400: #a3a3a3;
|
||||||
--color-neutral-500: #737373;
|
--color-neutral-500: #737373;
|
||||||
--color-neutral-600: #525252;
|
--color-neutral-600: #525252;
|
||||||
--color-neutral-700: #404040;
|
--color-neutral-700: #404040;
|
||||||
--color-neutral-800: #262626;
|
--color-neutral-800: #262626;
|
||||||
--color-neutral-900: #171717;
|
--color-neutral-900: #171717;
|
||||||
--color-neutral-950: #0a0a0a;
|
--color-neutral-950: #0a0a0a;
|
||||||
|
|
||||||
/* Status colors */
|
/* Status colors */
|
||||||
--color-success-50: #f0fdf4;
|
--color-success-50: #f0fdf4;
|
||||||
--color-success-100: #dcfce7;
|
--color-success-100: #dcfce7;
|
||||||
--color-success-500: #22c55e;
|
--color-success-500: #22c55e;
|
||||||
--color-success-600: #16a34a;
|
--color-success-600: #16a34a;
|
||||||
--color-success-700: #15803d;
|
--color-success-700: #15803d;
|
||||||
--color-success-900: #14532d;
|
--color-success-900: #14532d;
|
||||||
|
|
||||||
--color-error-50: #fef2f2;
|
--color-error-50: #fef2f2;
|
||||||
--color-error-100: #fee2e2;
|
--color-error-100: #fee2e2;
|
||||||
--color-error-500: #ef4444;
|
--color-error-500: #ef4444;
|
||||||
--color-error-600: #dc2626;
|
--color-error-600: #dc2626;
|
||||||
--color-error-700: #b91c1c;
|
--color-error-700: #b91c1c;
|
||||||
--color-error-900: #7f1d1d;
|
--color-error-900: #7f1d1d;
|
||||||
|
|
||||||
--color-warning-50: #fffbeb;
|
--color-warning-50: #fffbeb;
|
||||||
--color-warning-100: #fef3c7;
|
--color-warning-100: #fef3c7;
|
||||||
--color-warning-500: #f59e0b;
|
--color-warning-500: #f59e0b;
|
||||||
--color-warning-600: #d97706;
|
--color-warning-600: #d97706;
|
||||||
--color-warning-700: #b45309;
|
--color-warning-700: #b45309;
|
||||||
--color-warning-900: #78350f;
|
--color-warning-900: #78350f;
|
||||||
|
|
||||||
--color-info-50: #eff6ff;
|
--color-info-50: #eff6ff;
|
||||||
--color-info-100: #dbeafe;
|
--color-info-100: #dbeafe;
|
||||||
--color-info-500: #3b82f6;
|
--color-info-500: #3b82f6;
|
||||||
--color-info-600: #2563eb;
|
--color-info-600: #2563eb;
|
||||||
--color-info-700: #1d4ed8;
|
--color-info-700: #1d4ed8;
|
||||||
--color-info-900: #1e3a8a;
|
--color-info-900: #1e3a8a;
|
||||||
|
|
||||||
/* Teal colors */
|
/* Teal colors */
|
||||||
--color-teal-50: #f0fdfa;
|
--color-teal-50: #f0fdfa;
|
||||||
--color-teal-100: #ccfbf1;
|
--color-teal-100: #ccfbf1;
|
||||||
--color-teal-200: #99f6e4;
|
--color-teal-200: #99f6e4;
|
||||||
--color-teal-300: #5eead4;
|
--color-teal-300: #5eead4;
|
||||||
--color-teal-400: #2dd4bf;
|
--color-teal-400: #2dd4bf;
|
||||||
--color-teal-500: #14b8a6;
|
--color-teal-500: #14b8a6;
|
||||||
--color-teal-600: #0d9488;
|
--color-teal-600: #0d9488;
|
||||||
--color-teal-700: #0f766e;
|
--color-teal-700: #0f766e;
|
||||||
--color-teal-800: #115e59;
|
--color-teal-800: #115e59;
|
||||||
--color-teal-900: #134e4a;
|
--color-teal-900: #134e4a;
|
||||||
|
|
||||||
/* Dark colors */
|
/* Dark colors */
|
||||||
--color-dark-50: #f8fafc;
|
--color-dark-50: #f8fafc;
|
||||||
--color-dark-100: #f1f5f9;
|
--color-dark-100: #f1f5f9;
|
||||||
--color-dark-200: #e2e8f0;
|
--color-dark-200: #e2e8f0;
|
||||||
--color-dark-300: #cbd5e1;
|
--color-dark-300: #cbd5e1;
|
||||||
--color-dark-400: #94a3b8;
|
--color-dark-400: #94a3b8;
|
||||||
--color-dark-500: #64748b;
|
--color-dark-500: #64748b;
|
||||||
--color-dark-600: #475569;
|
--color-dark-600: #475569;
|
||||||
--color-dark-700: #334155;
|
--color-dark-700: #334155;
|
||||||
--color-dark-800: #1e293b;
|
--color-dark-800: #1e293b;
|
||||||
--color-dark-900: #0f172a;
|
--color-dark-900: #0f172a;
|
||||||
--color-dark-950: #020617;
|
--color-dark-950: #020617;
|
||||||
|
|
||||||
/* Login specific colors */
|
/* Login specific colors */
|
||||||
--color-login-primary: var(--color-green);
|
--color-login-primary: var(--color-green);
|
||||||
--color-login-dark-start: #464861;
|
--color-login-dark-start: #464861;
|
||||||
--color-login-dark-end: #111628;
|
--color-login-dark-end: #111628;
|
||||||
}
|
}
|
||||||
|
|
||||||
.dark {
|
.dark {
|
||||||
/* Dark theme colors */
|
/* Dark theme colors */
|
||||||
--background: #020617;
|
--background: #020617;
|
||||||
--foreground: #f8fafc;
|
--foreground: #f8fafc;
|
||||||
--card: #0f172a;
|
--card: #0f172a;
|
||||||
--card-foreground: #f8fafc;
|
--card-foreground: #f8fafc;
|
||||||
--popover: #0f172a;
|
--popover: #0f172a;
|
||||||
--popover-foreground: #f8fafc;
|
--popover-foreground: #f8fafc;
|
||||||
--primary: #22c55e;
|
--primary: #22c55e;
|
||||||
--primary-foreground: #0a0a0a;
|
--primary-foreground: #0a0a0a;
|
||||||
--secondary: #1e293b;
|
--secondary: #1e293b;
|
||||||
--secondary-foreground: #f8fafc;
|
--secondary-foreground: #f8fafc;
|
||||||
--muted: #1e293b;
|
--muted: #1e293b;
|
||||||
--muted-foreground: #94a3b8;
|
--muted-foreground: #94a3b8;
|
||||||
--accent: #1e293b;
|
--accent: #1e293b;
|
||||||
--accent-foreground: #f8fafc;
|
--accent-foreground: #f8fafc;
|
||||||
--destructive: #ef4444;
|
--destructive: #ef4444;
|
||||||
--destructive-foreground: #f8fafc;
|
--destructive-foreground: #f8fafc;
|
||||||
--border: #1e293b;
|
--border: #1e293b;
|
||||||
--input: #1e293b;
|
--input: #1e293b;
|
||||||
--ring: #22c55e;
|
--ring: #22c55e;
|
||||||
}
|
}
|
||||||
|
|
||||||
@layer base {
|
@layer base {
|
||||||
* {
|
* {
|
||||||
@apply border-border;
|
@apply border-border;
|
||||||
}
|
}
|
||||||
|
|
||||||
body {
|
body {
|
||||||
@apply bg-background text-foreground;
|
@apply bg-background text-foreground;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Persian/Farsi font class */
|
/* Persian/Farsi font class */
|
||||||
.font-persian {
|
.font-persian {
|
||||||
font-family: "IRANYekanX";
|
font-family: "IRANYekanX";
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Custom utility classes */
|
/* Custom utility classes */
|
||||||
.gradient-primary {
|
.gradient-primary {
|
||||||
background: linear-gradient(
|
background: linear-gradient(
|
||||||
135deg,
|
135deg,
|
||||||
var(--color-primary-500) 0%,
|
var(--color-primary-500) 0%,
|
||||||
var(--color-primary-600) 100%
|
var(--color-primary-600) 100%
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
.gradient-secondary {
|
.gradient-secondary {
|
||||||
background: linear-gradient(
|
background: linear-gradient(
|
||||||
135deg,
|
135deg,
|
||||||
var(--color-secondary-500) 0%,
|
var(--color-secondary-500) 0%,
|
||||||
var(--color-secondary-600) 100%
|
var(--color-secondary-600) 100%
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
.gradient-background {
|
.gradient-background {
|
||||||
background: linear-gradient(
|
background: linear-gradient(
|
||||||
135deg,
|
135deg,
|
||||||
var(--color-neutral-50) 0%,
|
var(--color-neutral-50) 0%,
|
||||||
var(--color-neutral-100) 100%
|
var(--color-neutral-100) 100%
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
.dark .gradient-background {
|
.dark .gradient-background {
|
||||||
background: linear-gradient(
|
background: linear-gradient(
|
||||||
135deg,
|
135deg,
|
||||||
var(--color-neutral-900) 0%,
|
var(--color-neutral-900) 0%,
|
||||||
var(--color-neutral-800) 100%
|
var(--color-neutral-800) 100%
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Login page specific styles */
|
/* Login page specific styles */
|
||||||
.login-page {
|
.login-page {
|
||||||
background: linear-gradient(
|
background: linear-gradient(
|
||||||
135deg,
|
135deg,
|
||||||
var(--color-login-dark-start) 0%,
|
var(--color-login-dark-start) 0%,
|
||||||
var(--color-login-dark-end) 100%
|
var(--color-login-dark-end) 100%
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
.login-sidebar {
|
.login-sidebar {
|
||||||
background: var(--color-login-primary);
|
background: var(--color-login-primary);
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Animation classes */
|
/* Animation classes */
|
||||||
.animate-fade-in {
|
.animate-fade-in {
|
||||||
animation: fadeIn 0.3s ease-in-out;
|
animation: fadeIn 0.3s ease-in-out;
|
||||||
}
|
}
|
||||||
|
|
||||||
.animate-slide-up {
|
.animate-slide-up {
|
||||||
animation: slideUp 0.3s ease-out;
|
animation: slideUp 0.3s ease-out;
|
||||||
}
|
}
|
||||||
|
|
||||||
.animate-slide-down {
|
.animate-slide-down {
|
||||||
animation: slideDown 0.3s ease-out;
|
animation: slideDown 0.3s ease-out;
|
||||||
}
|
}
|
||||||
|
|
||||||
@keyframes fadeIn {
|
@keyframes fadeIn {
|
||||||
from {
|
from {
|
||||||
opacity: 0;
|
opacity: 0;
|
||||||
}
|
}
|
||||||
to {
|
to {
|
||||||
opacity: 1;
|
opacity: 1;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@keyframes slideUp {
|
@keyframes slideUp {
|
||||||
from {
|
from {
|
||||||
transform: translateY(10px);
|
transform: translateY(10px);
|
||||||
opacity: 0;
|
opacity: 0;
|
||||||
}
|
}
|
||||||
to {
|
to {
|
||||||
transform: translateY(0);
|
transform: translateY(0);
|
||||||
opacity: 1;
|
opacity: 1;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@keyframes slideDown {
|
@keyframes slideDown {
|
||||||
from {
|
from {
|
||||||
transform: translateY(-10px);
|
transform: translateY(-10px);
|
||||||
opacity: 0;
|
opacity: 0;
|
||||||
}
|
}
|
||||||
to {
|
to {
|
||||||
transform: translateY(0);
|
transform: translateY(0);
|
||||||
opacity: 1;
|
opacity: 1;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Toast customization for RTL */
|
/* Toast customization for RTL */
|
||||||
.Toaster__toast {
|
.Toaster__toast {
|
||||||
direction: rtl;
|
direction: rtl;
|
||||||
text-align: right;
|
text-align: right;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Form focus styles */
|
/* Form focus styles */
|
||||||
.form-input:focus-within {
|
.form-input:focus-within {
|
||||||
@apply ring-2 ring-primary/20 border-primary;
|
@apply ring-2 ring-primary/20 border-primary;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Button hover effects */
|
/* Button hover effects */
|
||||||
.btn-hover-scale:hover {
|
.btn-hover-scale:hover {
|
||||||
transform: scale(1.02);
|
transform: scale(1.02);
|
||||||
transition: transform 0.2s ease-in-out;
|
transition: transform 0.2s ease-in-out;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Custom shadows */
|
/* Custom shadows */
|
||||||
.shadow-primary {
|
.shadow-primary {
|
||||||
box-shadow:
|
box-shadow:
|
||||||
0 4px 6px -1px rgb(34 197 94 / 0.1),
|
0 4px 6px -1px rgb(34 197 94 / 0.1),
|
||||||
0 2px 4px -2px rgb(34 197 94 / 0.1);
|
0 2px 4px -2px rgb(34 197 94 / 0.1);
|
||||||
}
|
}
|
||||||
|
|
||||||
.shadow-error {
|
.shadow-error {
|
||||||
box-shadow:
|
box-shadow:
|
||||||
0 4px 6px -1px rgb(239 68 68 / 0.1),
|
0 4px 6px -1px rgb(239 68 68 / 0.1),
|
||||||
0 2px 4px -2px rgb(239 68 68 / 0.1);
|
0 2px 4px -2px rgb(239 68 68 / 0.1);
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Loading states */
|
/* Loading states */
|
||||||
.loading-shimmer {
|
.loading-shimmer {
|
||||||
background: linear-gradient(90deg, #f0f0f0 25%, #e0e0e0 50%, #f0f0f0 75%);
|
background: linear-gradient(90deg, #f0f0f0 25%, #e0e0e0 50%, #f0f0f0 75%);
|
||||||
background-size: 200% 100%;
|
background-size: 200% 100%;
|
||||||
animation: shimmer 1.5s infinite;
|
animation: shimmer 1.5s infinite;
|
||||||
}
|
}
|
||||||
|
|
||||||
.dark .loading-shimmer {
|
.dark .loading-shimmer {
|
||||||
background: linear-gradient(90deg, #2a2a2a 25%, #3a3a3a 50%, #2a2a2a 75%);
|
background: linear-gradient(90deg, #2a2a2a 25%, #3a3a3a 50%, #2a2a2a 75%);
|
||||||
background-size: 200% 100%;
|
background-size: 200% 100%;
|
||||||
}
|
}
|
||||||
|
|
||||||
@keyframes shimmer {
|
@keyframes shimmer {
|
||||||
0% {
|
0% {
|
||||||
background-position: -200% 0;
|
background-position: -200% 0;
|
||||||
}
|
}
|
||||||
100% {
|
100% {
|
||||||
background-position: 200% 0;
|
background-position: 200% 0;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Table/container specific custom dark scrollbar */
|
/* Table/container specific custom dark scrollbar */
|
||||||
.custom-scrollbar {
|
.custom-scrollbar {
|
||||||
scrollbar-width: thin; /* Firefox */
|
scrollbar-width: thin; /* Firefox */
|
||||||
scrollbar-color: rgba(100, 116, 139, 0.6) transparent; /* thumb track */
|
scrollbar-color: rgba(100, 116, 139, 0.6) transparent; /* thumb track */
|
||||||
}
|
}
|
||||||
|
|
||||||
.custom-scrollbar::-webkit-scrollbar {
|
.custom-scrollbar::-webkit-scrollbar {
|
||||||
width: 2px;
|
width: 2px;
|
||||||
height: 2px;
|
height: 2px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.custom-scrollbar::-webkit-scrollbar-track {
|
.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 {
|
.custom-scrollbar::-webkit-scrollbar-thumb {
|
||||||
background: linear-gradient(
|
background: linear-gradient(
|
||||||
to bottom,
|
to bottom,
|
||||||
rgba(16, 185, 129, 0.6),
|
rgba(16, 185, 129, 0.6),
|
||||||
rgba(16, 185, 129, 0.9)
|
rgba(16, 185, 129, 0.9)
|
||||||
); /* emerald */
|
); /* emerald */
|
||||||
border-radius: 9999px;
|
border-radius: 9999px;
|
||||||
border: 0.5px solid transparent;
|
border: 0.5px solid transparent;
|
||||||
background-clip: padding-box;
|
background-clip: padding-box;
|
||||||
}
|
}
|
||||||
|
|
||||||
.custom-scrollbar:hover::-webkit-scrollbar-thumb {
|
.custom-scrollbar:hover::-webkit-scrollbar-thumb {
|
||||||
|
|
@ -443,50 +445,50 @@ html[dir="rtl"] body {
|
||||||
}
|
}
|
||||||
|
|
||||||
:root {
|
:root {
|
||||||
--form-control-color: #3f415a;
|
--form-control-color: #3f415a;
|
||||||
--form-control-disabled: ##5f6284;
|
--form-control-disabled: ##5f6284;
|
||||||
--form-background: #3aea83;
|
--form-background: #3aea83;
|
||||||
}
|
}
|
||||||
|
|
||||||
input[type="checkbox"] {
|
input[type="checkbox"] {
|
||||||
-webkit-appearance: none;
|
-webkit-appearance: none;
|
||||||
appearance: none;
|
appearance: none;
|
||||||
margin: 0;
|
margin: 0;
|
||||||
font: inherit;
|
font: inherit;
|
||||||
color: #5f6284;
|
color: #5f6284;
|
||||||
background-color: transparent;
|
background-color: transparent;
|
||||||
width: 1.15em;
|
width: 1.15em;
|
||||||
height: 1.15em;
|
height: 1.15em;
|
||||||
border: 1px solid #5f6284;
|
border: 1px solid #5f6284;
|
||||||
border-radius: 0.15em;
|
border-radius: 0.15em;
|
||||||
transform: translateY(-0.075em);
|
transform: translateY(-0.075em);
|
||||||
display: grid;
|
display: grid;
|
||||||
place-content: center;
|
place-content: center;
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
}
|
}
|
||||||
|
|
||||||
input[type="checkbox"]::before {
|
input[type="checkbox"]::before {
|
||||||
content: "";
|
content: "";
|
||||||
width: 0.65em;
|
width: 0.65em;
|
||||||
height: 0.65em;
|
height: 0.65em;
|
||||||
clip-path: polygon(14% 44%, 0 65%, 50% 100%, 100% 16%, 80% 0%, 43% 62%);
|
clip-path: polygon(14% 44%, 0 65%, 50% 100%, 100% 16%, 80% 0%, 43% 62%);
|
||||||
transform: scale(0);
|
transform: scale(0);
|
||||||
transform-origin: bottom left;
|
transform-origin: bottom left;
|
||||||
transition: 120ms transform ease-in-out;
|
transition: 120ms transform ease-in-out;
|
||||||
box-shadow: inset 1em 1em var(--form-control-color);
|
box-shadow: inset 1em 1em var(--form-control-color);
|
||||||
}
|
}
|
||||||
|
|
||||||
input[type="checkbox"]:checked::before {
|
input[type="checkbox"]:checked::before {
|
||||||
transform: scale(1);
|
transform: scale(1);
|
||||||
}
|
}
|
||||||
|
|
||||||
input[type="checkbox"]:checked {
|
input[type="checkbox"]:checked {
|
||||||
background-color: #3aea83;
|
background-color: #3aea83;
|
||||||
border: 1px solid transparent;
|
border: 1px solid transparent;
|
||||||
}
|
}
|
||||||
|
|
||||||
input[type="checkbox"]:disabled {
|
input[type="checkbox"]:disabled {
|
||||||
--form-control-color: var(--form-control-disabled);
|
--form-control-color: var(--form-control-disabled);
|
||||||
color: var(--form-control-disabled);
|
color: var(--form-control-disabled);
|
||||||
cursor: not-allowed;
|
cursor: not-allowed;
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -12,7 +12,6 @@ import {
|
||||||
Zap,
|
Zap,
|
||||||
} from "lucide-react";
|
} from "lucide-react";
|
||||||
import moment from "moment-jalaali";
|
import moment from "moment-jalaali";
|
||||||
import { formatNumber } from "~/lib/utils";
|
|
||||||
import { useCallback, useEffect, useMemo, useRef, useState } from "react";
|
import { useCallback, useEffect, useMemo, useRef, useState } from "react";
|
||||||
import toast from "react-hot-toast";
|
import toast from "react-hot-toast";
|
||||||
import { Badge } from "~/components/ui/badge";
|
import { Badge } from "~/components/ui/badge";
|
||||||
|
|
@ -35,7 +34,7 @@ import {
|
||||||
TableRow,
|
TableRow,
|
||||||
} from "~/components/ui/table";
|
} from "~/components/ui/table";
|
||||||
import apiService from "~/lib/api";
|
import apiService from "~/lib/api";
|
||||||
import { formatCurrency } from "~/lib/utils";
|
import { formatCurrency, formatNumber } from "~/lib/utils";
|
||||||
import { DashboardLayout } from "../layout";
|
import { DashboardLayout } from "../layout";
|
||||||
|
|
||||||
moment.loadPersian({ usePersianDigits: true });
|
moment.loadPersian({ usePersianDigits: true });
|
||||||
|
|
@ -370,7 +369,8 @@ export function DigitalInnovationPage() {
|
||||||
const scrollContainer = scrollContainerRef.current;
|
const scrollContainer = scrollContainerRef.current;
|
||||||
|
|
||||||
const handleScroll = () => {
|
const handleScroll = () => {
|
||||||
if (!scrollContainer || !hasMore || loadingMore || fetchingRef.current) return;
|
if (!scrollContainer || !hasMore || loadingMore || fetchingRef.current)
|
||||||
|
return;
|
||||||
|
|
||||||
// Clear previous timeout
|
// Clear previous timeout
|
||||||
if (scrollTimeoutRef.current) {
|
if (scrollTimeoutRef.current) {
|
||||||
|
|
@ -390,7 +390,9 @@ export function DigitalInnovationPage() {
|
||||||
};
|
};
|
||||||
|
|
||||||
if (scrollContainer) {
|
if (scrollContainer) {
|
||||||
scrollContainer.addEventListener("scroll", handleScroll, { passive: true });
|
scrollContainer.addEventListener("scroll", handleScroll, {
|
||||||
|
passive: true,
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
return () => {
|
return () => {
|
||||||
|
|
@ -452,14 +454,12 @@ export function DigitalInnovationPage() {
|
||||||
innovation_digital_function: {},
|
innovation_digital_function: {},
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
// let payload: DigitalInnovationMetrics = raw?.data;
|
// let payload: DigitalInnovationMetrics = raw?.data;
|
||||||
// console.log("*-*-*-*" +payload);
|
// console.log("*-*-*-*" +payload);
|
||||||
// if (typeof payload === "string") {
|
// if (typeof payload === "string") {
|
||||||
// try {
|
// try {
|
||||||
// payload = JSON.parse(payload).innovation_digital_function;
|
// payload = JSON.parse(payload).innovation_digital_function;
|
||||||
|
|
||||||
// } catch {}
|
// } catch {}
|
||||||
// }
|
// }
|
||||||
|
|
||||||
|
|
@ -469,10 +469,10 @@ export function DigitalInnovationPage() {
|
||||||
try {
|
try {
|
||||||
// مرحله اول: data رو از string به object تبدیل کن
|
// مرحله اول: data رو از string به object تبدیل کن
|
||||||
const parsedData = JSON.parse(raw.data);
|
const parsedData = JSON.parse(raw.data);
|
||||||
|
|
||||||
// مرحله دوم: innovation_digital_function رو که خودش string هست parse کن
|
// مرحله دوم: innovation_digital_function رو که خودش string هست parse کن
|
||||||
const arr = JSON.parse(parsedData.innovation_digital_function);
|
const arr = JSON.parse(parsedData.innovation_digital_function);
|
||||||
|
|
||||||
// مرحله سوم: اولین خانه آرایه رو بردار
|
// مرحله سوم: اولین خانه آرایه رو بردار
|
||||||
if (Array.isArray(arr) && arr.length > 0) {
|
if (Array.isArray(arr) && arr.length > 0) {
|
||||||
payload = arr[0];
|
payload = arr[0];
|
||||||
|
|
@ -482,8 +482,6 @@ export function DigitalInnovationPage() {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
const parseNum = (v: unknown): number => {
|
const parseNum = (v: unknown): number => {
|
||||||
if (v == null) return 0;
|
if (v == null) return 0;
|
||||||
if (typeof v === "number") return v;
|
if (typeof v === "number") return v;
|
||||||
|
|
@ -550,7 +548,7 @@ export function DigitalInnovationPage() {
|
||||||
index: greenBoxes + 1,
|
index: greenBoxes + 1,
|
||||||
style: `linear-gradient(
|
style: `linear-gradient(
|
||||||
to right,
|
to right,
|
||||||
oklch(76.5% 0.177 163.223) 0%,
|
oklch(76.5% 0.177 163.223) 0%,
|
||||||
oklch(76.5% 0.177 163.223) ${partialPercent * 100}%,
|
oklch(76.5% 0.177 163.223) ${partialPercent * 100}%,
|
||||||
oklch(55.1% 0.027 264.364) ${partialPercent * 100}%,
|
oklch(55.1% 0.027 264.364) ${partialPercent * 100}%,
|
||||||
oklch(55.1% 0.027 264.364) 100%
|
oklch(55.1% 0.027 264.364) 100%
|
||||||
|
|
@ -601,7 +599,7 @@ export function DigitalInnovationPage() {
|
||||||
variant="ghost"
|
variant="ghost"
|
||||||
size="sm"
|
size="sm"
|
||||||
onClick={() => handleProjectDetails(item)}
|
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>
|
</Button>
|
||||||
|
|
@ -654,7 +652,7 @@ export function DigitalInnovationPage() {
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<DashboardLayout title="نوآوری دیجیتال">
|
<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 */}
|
{/* Stats Cards */}
|
||||||
<div className="flex flex-col gap-6 w-full mb-0">
|
<div className="flex flex-col gap-6 w-full mb-0">
|
||||||
<div className="space-y-6 w-full">
|
<div className="space-y-6 w-full">
|
||||||
|
|
@ -955,7 +953,7 @@ export function DigitalInnovationPage() {
|
||||||
شرح پروژه
|
شرح پروژه
|
||||||
</DialogTitle>
|
</DialogTitle>
|
||||||
</DialogHeader>
|
</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">
|
<div className="border-l-2 border-l-gray-600 px-6">
|
||||||
<span className="title text-lg font-bold">
|
<span className="title text-lg font-bold">
|
||||||
{dialogInfo?.title}
|
{dialogInfo?.title}
|
||||||
|
|
@ -1070,7 +1068,7 @@ export function DigitalInnovationPage() {
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</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="costBoard mx-auto w-full">
|
||||||
<div className="board o border border-gray-600 rounded-xl overflow-hidden flex flex-col">
|
<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 ">
|
<span className="title bg-[#3F415A] text-white w-full p-2.5 pr-4 ">
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,5 @@
|
||||||
// import moment from "moment-jalaali";
|
// import moment from "moment-jalaali";
|
||||||
import { useCallback, useEffect, useRef, useState } from "react";
|
import { useCallback, useEffect, useRef, useState } from "react";
|
||||||
import { formatNumber } from "~/lib/utils";
|
|
||||||
import {
|
import {
|
||||||
Bar,
|
Bar,
|
||||||
BarChart,
|
BarChart,
|
||||||
|
|
@ -27,6 +26,7 @@ import {
|
||||||
TableHeader,
|
TableHeader,
|
||||||
TableRow,
|
TableRow,
|
||||||
} from "~/components/ui/table";
|
} from "~/components/ui/table";
|
||||||
|
import { formatNumber } from "~/lib/utils";
|
||||||
|
|
||||||
import {
|
import {
|
||||||
Building2,
|
Building2,
|
||||||
|
|
@ -602,7 +602,7 @@ export function GreenInnovationPage() {
|
||||||
variant="ghost"
|
variant="ghost"
|
||||||
size="sm"
|
size="sm"
|
||||||
onClick={() => handleProjectDetails(item)}
|
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>
|
</Button>
|
||||||
|
|
@ -813,7 +813,10 @@ export function GreenInnovationPage() {
|
||||||
<div className="params flex flex-col gap-3.5">
|
<div className="params flex flex-col gap-3.5">
|
||||||
{Object.entries(recycleParams).map((el, index) => {
|
{Object.entries(recycleParams).map((el, index) => {
|
||||||
return (
|
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">
|
<div className="flex flex-row gap-2">
|
||||||
{el[1].icon}
|
{el[1].icon}
|
||||||
<span className="font-normal text-sm font-persian">
|
<span className="font-normal text-sm font-persian">
|
||||||
|
|
|
||||||
|
|
@ -624,7 +624,7 @@ export function InnovationBuiltInsidePage() {
|
||||||
variant="ghost"
|
variant="ghost"
|
||||||
size="sm"
|
size="sm"
|
||||||
onClick={() => handleProjectDetails(item)}
|
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>
|
</Button>
|
||||||
|
|
@ -701,7 +701,7 @@ export function InnovationBuiltInsidePage() {
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<DashboardLayout title="نوآوری ساخت داخل">
|
<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 */}
|
{/* Stats Cards */}
|
||||||
<div className="flex gap-6 w-full mb-0">
|
<div className="flex gap-6 w-full mb-0">
|
||||||
<div className="flex flex-col justify-between w-full gap-6">
|
<div className="flex flex-col justify-between w-full gap-6">
|
||||||
|
|
|
||||||
|
|
@ -1,4 +1,4 @@
|
||||||
import { ChevronDown, ChevronUp, RefreshCw, Eye, Star } from "lucide-react";
|
import { ChevronDown, ChevronUp, RefreshCw, Eye, Star, TrendingUp, Hexagon, Download } from "lucide-react";
|
||||||
import { useCallback, useEffect, useRef, useState, useMemo } from "react";
|
import { useCallback, useEffect, useRef, useState, useMemo } from "react";
|
||||||
import toast from "react-hot-toast";
|
import toast from "react-hot-toast";
|
||||||
import { Badge } from "~/components/ui/badge";
|
import { Badge } from "~/components/ui/badge";
|
||||||
|
|
@ -21,6 +21,18 @@ import {
|
||||||
import apiService from "~/lib/api";
|
import apiService from "~/lib/api";
|
||||||
import { formatCurrency, formatNumber } from "~/lib/utils";
|
import { formatCurrency, formatNumber } from "~/lib/utils";
|
||||||
import { DashboardLayout } from "../layout";
|
import { DashboardLayout } from "../layout";
|
||||||
|
import {
|
||||||
|
ChartContainer,
|
||||||
|
ChartTooltip,
|
||||||
|
ChartTooltipContent,
|
||||||
|
type ChartConfig,
|
||||||
|
} from "~/components/ui/chart";
|
||||||
|
import { BarChart, Bar, XAxis, YAxis, ResponsiveContainer, CartesianGrid, LabelList, Cell, RadialBarChart, PolarGrid, RadialBar, PolarRadiusAxis } from "recharts";
|
||||||
|
import { BaseCard } from "~/components/ui/base-card";
|
||||||
|
import { Label } from "~/components/ui/label";
|
||||||
|
import { MetricCard } from "~/components/ui/metric-card";
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
interface IdeaData {
|
interface IdeaData {
|
||||||
idea_title: string;
|
idea_title: string;
|
||||||
|
|
@ -48,6 +60,18 @@ interface PersonRanking {
|
||||||
stars: number;
|
stars: number;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
interface IdeaStatusData {
|
||||||
|
idea_status: string;
|
||||||
|
idea_status_count: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface IdeaStatsData {
|
||||||
|
registered_innovation_technology_idea: string;
|
||||||
|
ongoing_innovation_technology_ideas: string;
|
||||||
|
increased_revenue_from_ideas: string;
|
||||||
|
increased_revenue_from_ideas_percent: string;
|
||||||
|
}
|
||||||
|
|
||||||
interface SortConfig {
|
interface SortConfig {
|
||||||
field: string;
|
field: string;
|
||||||
direction: "asc" | "desc";
|
direction: "asc" | "desc";
|
||||||
|
|
@ -88,6 +112,14 @@ export function ManageIdeasTechPage() {
|
||||||
const [peopleRanking, setPeopleRanking] = useState<PersonRanking[]>([]);
|
const [peopleRanking, setPeopleRanking] = useState<PersonRanking[]>([]);
|
||||||
const [loadingPeople, setLoadingPeople] = useState(false);
|
const [loadingPeople, setLoadingPeople] = useState(false);
|
||||||
|
|
||||||
|
// Chart state
|
||||||
|
const [chartData, setChartData] = useState<IdeaStatusData[]>([]);
|
||||||
|
const [loadingChart, setLoadingChart] = useState(false);
|
||||||
|
|
||||||
|
// Stats state
|
||||||
|
const [statsData, setStatsData] = useState<IdeaStatsData | null>(null);
|
||||||
|
const [loadingStats, setLoadingStats] = useState(false);
|
||||||
|
|
||||||
const observerRef = useRef<HTMLDivElement>(null);
|
const observerRef = useRef<HTMLDivElement>(null);
|
||||||
const fetchingRef = useRef(false);
|
const fetchingRef = useRef(false);
|
||||||
const scrollTimeoutRef = useRef<NodeJS.Timeout | null>(null);
|
const scrollTimeoutRef = useRef<NodeJS.Timeout | null>(null);
|
||||||
|
|
@ -204,6 +236,8 @@ export function ManageIdeasTechPage() {
|
||||||
fetchIdeas(true);
|
fetchIdeas(true);
|
||||||
fetchTotalCount();
|
fetchTotalCount();
|
||||||
fetchPeopleRanking();
|
fetchPeopleRanking();
|
||||||
|
fetchChartData();
|
||||||
|
fetchStatsData();
|
||||||
}, [sortConfig]);
|
}, [sortConfig]);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
|
|
@ -350,6 +384,68 @@ export function ManageIdeasTechPage() {
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const fetchChartData = async () => {
|
||||||
|
try {
|
||||||
|
setLoadingChart(true);
|
||||||
|
|
||||||
|
const response = await apiService.select({
|
||||||
|
ProcessName: "idea",
|
||||||
|
OutputFields: ["idea_status", "count(idea_status)"],
|
||||||
|
GroupBy: ["idea_status"],
|
||||||
|
});
|
||||||
|
|
||||||
|
if (response.state === 0) {
|
||||||
|
const dataString = response.data;
|
||||||
|
if (dataString && typeof dataString === "string") {
|
||||||
|
try {
|
||||||
|
const parsedData: IdeaStatusData[] = JSON.parse(dataString);
|
||||||
|
if (Array.isArray(parsedData)) {
|
||||||
|
setChartData(parsedData?.reverse());
|
||||||
|
}
|
||||||
|
} catch (parseError) {
|
||||||
|
console.error("Error parsing chart data:", parseError);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
toast.error(response.message || "خطا در دریافت اطلاعات نمودار");
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error("Error fetching chart data:", error);
|
||||||
|
toast.error("خطا در دریافت اطلاعات نمودار");
|
||||||
|
} finally {
|
||||||
|
setLoadingChart(false);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const fetchStatsData = async () => {
|
||||||
|
try {
|
||||||
|
setLoadingStats(true);
|
||||||
|
|
||||||
|
const response = await apiService.call({
|
||||||
|
idea_page_function: {}
|
||||||
|
});
|
||||||
|
|
||||||
|
if (response.state === 0) {
|
||||||
|
const dataString = response.data;
|
||||||
|
if (dataString && typeof dataString === "string") {
|
||||||
|
try {
|
||||||
|
const parsedData: IdeaStatsData = JSON.parse(dataString);
|
||||||
|
setStatsData(parsedData);
|
||||||
|
} catch (parseError) {
|
||||||
|
console.error("Error parsing stats data:", parseError);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
toast.error(response.message || "خطا در دریافت آمار ایدهها");
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error("Error fetching stats data:", error);
|
||||||
|
toast.error("خطا در دریافت آمار ایدهها");
|
||||||
|
} finally {
|
||||||
|
setLoadingStats(false);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
const toPersianDigits = (input: string | number): string => {
|
const toPersianDigits = (input: string | number): string => {
|
||||||
const str = String(input);
|
const str = String(input);
|
||||||
const map: Record<string, string> = {
|
const map: Record<string, string> = {
|
||||||
|
|
@ -395,7 +491,30 @@ export function ManageIdeasTechPage() {
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
// Chart configuration for shadcn/ui
|
||||||
|
const chartConfig: ChartConfig = {
|
||||||
|
count: {
|
||||||
|
label: "تعداد",
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
// Color palette for idea status
|
// Color palette for idea status
|
||||||
|
// Specific colors for idea statuses
|
||||||
|
const getChartStatusColor = (status: string) => {
|
||||||
|
switch (status) {
|
||||||
|
case "اجرا شده":
|
||||||
|
return "#69C8EA";
|
||||||
|
case "تایید شده":
|
||||||
|
return "#3AEA83";
|
||||||
|
case "در حال بررسی":
|
||||||
|
return "#EAD069";
|
||||||
|
case "رد شده":
|
||||||
|
return "#F76276";
|
||||||
|
default:
|
||||||
|
return "#6B7280";
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
const statusColorPalette = ["#3AEA83", "#69C8EA", "#F76276", "#FFD700", "#A757FF", "#E884CE", "#C3BF8B", "#FB7185"];
|
const statusColorPalette = ["#3AEA83", "#69C8EA", "#F76276", "#FFD700", "#A757FF", "#E884CE", "#C3BF8B", "#FB7185"];
|
||||||
|
|
||||||
// Build a mapping of status value -> color based on loaded ideas
|
// Build a mapping of status value -> color based on loaded ideas
|
||||||
|
|
@ -469,7 +588,7 @@ export function ManageIdeasTechPage() {
|
||||||
variant="ghost"
|
variant="ghost"
|
||||||
size="sm"
|
size="sm"
|
||||||
onClick={() => handleShowDetails(item)}
|
onClick={() => handleShowDetails(item)}
|
||||||
className="underline text-pr-green underline-offset-4 text-sm hover:bg-emerald-500/20"
|
className="underline text-pr-green underline-offset-4 text-sm hover:bg-pr-green/20"
|
||||||
>
|
>
|
||||||
جزئیات بیشتر
|
جزئیات بیشتر
|
||||||
</Button> );
|
</Button> );
|
||||||
|
|
@ -482,9 +601,104 @@ export function ManageIdeasTechPage() {
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
// Custom Vertical Bar Chart Component using shadcn/ui
|
||||||
|
const VerticalBarChart = () => {
|
||||||
|
if (loadingChart) {
|
||||||
|
return (
|
||||||
|
<div className="p-6">
|
||||||
|
<div className="bg-gray-600 rounded animate-pulse w-48 mx-auto"></div>
|
||||||
|
<div className="h-40 bg-gray-700 rounded animate-pulse"></div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!chartData.length) {
|
||||||
|
return (
|
||||||
|
<div className="p-6 text-center">
|
||||||
|
<h3 className="text-lg font-persian font-semibold text-white mb-4">وضعیت ایده ها</h3>
|
||||||
|
<p className="text-gray-400 font-persian">هیچ دادهای یافت نشد</p>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Prepare data for recharts
|
||||||
|
const rechartData = chartData.map((item) => ({
|
||||||
|
status: item.idea_status,
|
||||||
|
count: item.idea_status_count,
|
||||||
|
fill: getChartStatusColor(item.idea_status),
|
||||||
|
}));
|
||||||
|
|
||||||
|
return (
|
||||||
|
<ResponsiveContainer width="100%">
|
||||||
|
<ChartContainer config={chartConfig} className="w-full">
|
||||||
|
<BarChart
|
||||||
|
margin={{ top : 25 ,left: 12, right: 12 }}
|
||||||
|
barGap={15}
|
||||||
|
barSize={45}
|
||||||
|
accessibilityLayer
|
||||||
|
data={rechartData} >
|
||||||
|
<CartesianGrid vertical={false} stroke="#475569" />
|
||||||
|
<XAxis
|
||||||
|
dataKey="status"
|
||||||
|
axisLine={false}
|
||||||
|
tickLine={false}
|
||||||
|
tick={{
|
||||||
|
fill: '#fff',
|
||||||
|
fontSize: 14,
|
||||||
|
fontFamily: 'inherit'
|
||||||
|
}}
|
||||||
|
interval={0}
|
||||||
|
angle={0}
|
||||||
|
tickMargin={10}
|
||||||
|
textAnchor="middle"
|
||||||
|
/>
|
||||||
|
<YAxis
|
||||||
|
tickMargin={20}
|
||||||
|
axisLine={false}
|
||||||
|
tickLine={false}
|
||||||
|
tick={{
|
||||||
|
fill: '#9CA3AF',
|
||||||
|
fontSize: 12,
|
||||||
|
fontFamily: 'inherit'
|
||||||
|
}}
|
||||||
|
tickFormatter={(value) => toPersianDigits(value)}
|
||||||
|
label={{
|
||||||
|
value: "تعداد برنامه ها" ,
|
||||||
|
angle: -90,
|
||||||
|
position: "insideLeft",
|
||||||
|
fill: "#94a3b8",
|
||||||
|
fontSize: 11,
|
||||||
|
offset: 0,
|
||||||
|
dy: 0,
|
||||||
|
style: { textAnchor: "middle" },
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
<Bar
|
||||||
|
dataKey="count"
|
||||||
|
radius={[4, 4, 0, 0]}
|
||||||
|
>
|
||||||
|
<LabelList
|
||||||
|
dataKey="count"
|
||||||
|
position="top"
|
||||||
|
offset={12}
|
||||||
|
style={{
|
||||||
|
fill: "#ffffff",
|
||||||
|
fontSize: "16px",
|
||||||
|
fontWeight: "bold",
|
||||||
|
}}
|
||||||
|
formatter={(v: number) => `${formatNumber(Math.round(v))}`}
|
||||||
|
/>
|
||||||
|
</Bar>
|
||||||
|
</BarChart>
|
||||||
|
</ChartContainer>
|
||||||
|
</ResponsiveContainer>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<DashboardLayout title="مدیریت ایده های فناوری و نوآوری">
|
<DashboardLayout title="مدیریت ایده های فناوری و نوآوری">
|
||||||
<div className="space-y-6 h-full">
|
<div className="space-y-6 h-full">
|
||||||
|
|
||||||
<div className="grid grid-cols-1 grid-rows-2 lg:grid-cols-3 gap-4 h-full">
|
<div className="grid grid-cols-1 grid-rows-2 lg:grid-cols-3 gap-4 h-full">
|
||||||
{/* People Ranking Table */}
|
{/* People Ranking Table */}
|
||||||
<div className="lg:col-span-1">
|
<div className="lg:col-span-1">
|
||||||
|
|
@ -588,7 +802,7 @@ export function ManageIdeasTechPage() {
|
||||||
<div className="relative">
|
<div className="relative">
|
||||||
<Table
|
<Table
|
||||||
containerRef={scrollContainerRef}
|
containerRef={scrollContainerRef}
|
||||||
containerClassName="overflow-auto custom-scrollbar max-h-[calc(50vh-100px)]"
|
containerClassName="overflow-auto custom-scrollbar max-h-[calc(50vh-180px)]"
|
||||||
>
|
>
|
||||||
<TableHeader className="sticky top-0 z-50 bg-pr-gray">
|
<TableHeader className="sticky top-0 z-50 bg-pr-gray">
|
||||||
<TableRow className="bg-pr-gray">
|
<TableRow className="bg-pr-gray">
|
||||||
|
|
@ -701,127 +915,354 @@ export function ManageIdeasTechPage() {
|
||||||
</div>
|
</div>
|
||||||
</Card>
|
</Card>
|
||||||
</div>
|
</div>
|
||||||
|
{/* Chart Section */}
|
||||||
|
<BaseCard icon={TrendingUp} className="col-span-1 row-start-2 col-start-3 row-span-1" title="نمودار ایدهها">
|
||||||
|
<VerticalBarChart />
|
||||||
|
</BaseCard>
|
||||||
|
<div className="col-span-1 col-start-2 row-start-2 flex flex-col-reverse justify-end gap-2 row-span-1">
|
||||||
|
<BaseCard title="ایدههای فناوری و نوآوری">
|
||||||
|
<div className="flex items-center gap-2 justify-center flex-row-reverse">
|
||||||
|
<ChartContainer
|
||||||
|
config={chartConfig}
|
||||||
|
className="aspect-square w-[6rem] h-auto"
|
||||||
|
>
|
||||||
|
<RadialBarChart
|
||||||
|
data={[
|
||||||
|
{
|
||||||
|
browser: "ideas",
|
||||||
|
visitors:
|
||||||
|
parseFloat(
|
||||||
|
statsData?.registered_innovation_technology_idea || "0"
|
||||||
|
) > 0
|
||||||
|
? Math.round(
|
||||||
|
(parseFloat(
|
||||||
|
statsData?.registered_innovation_technology_idea || "0",
|
||||||
|
) /
|
||||||
|
parseFloat(
|
||||||
|
statsData
|
||||||
|
?.registered_innovation_technology_idea ||
|
||||||
|
"1",
|
||||||
|
)) *
|
||||||
|
100,
|
||||||
|
)
|
||||||
|
: 0,
|
||||||
|
fill: "var(--color-green)",
|
||||||
|
},
|
||||||
|
]}
|
||||||
|
startAngle={90}
|
||||||
|
endAngle={
|
||||||
|
90 +
|
||||||
|
((parseFloat(
|
||||||
|
statsData
|
||||||
|
?.registered_innovation_technology_idea || "0",
|
||||||
|
) > 0
|
||||||
|
? Math.round(
|
||||||
|
(parseFloat(
|
||||||
|
statsData
|
||||||
|
?.ongoing_innovation_technology_ideas || "0",
|
||||||
|
) /
|
||||||
|
parseFloat(
|
||||||
|
statsData
|
||||||
|
?.registered_innovation_technology_idea ||
|
||||||
|
"1",
|
||||||
|
)) *
|
||||||
|
100,
|
||||||
|
)
|
||||||
|
: 0) /
|
||||||
|
100) *
|
||||||
|
360
|
||||||
|
}
|
||||||
|
innerRadius={35}
|
||||||
|
outerRadius={55}
|
||||||
|
>
|
||||||
|
<PolarGrid
|
||||||
|
gridType="circle"
|
||||||
|
radialLines={false}
|
||||||
|
stroke="none"
|
||||||
|
className="first:fill-pr-red last:fill-[#24273A]"
|
||||||
|
polarRadius={[38, 31]}
|
||||||
|
/>
|
||||||
|
<RadialBar
|
||||||
|
dataKey="visitors"
|
||||||
|
background
|
||||||
|
cornerRadius={5}
|
||||||
|
/>
|
||||||
|
<PolarRadiusAxis
|
||||||
|
tick={false}
|
||||||
|
tickLine={false}
|
||||||
|
axisLine={false}
|
||||||
|
>
|
||||||
|
<Label
|
||||||
|
content={({ viewBox }) => {
|
||||||
|
if (viewBox && "cx" in viewBox && "cy" in viewBox) {
|
||||||
|
return (
|
||||||
|
<text
|
||||||
|
x={viewBox.cx}
|
||||||
|
y={viewBox.cy}
|
||||||
|
textAnchor="middle"
|
||||||
|
dominantBaseline="middle"
|
||||||
|
>
|
||||||
|
<tspan
|
||||||
|
x={viewBox.cx}
|
||||||
|
y={viewBox.cy}
|
||||||
|
className="fill-foreground text-lg font-bold"
|
||||||
|
>
|
||||||
|
%
|
||||||
|
{formatNumber(
|
||||||
|
parseFloat(
|
||||||
|
statsData
|
||||||
|
?.registered_innovation_technology_idea ||
|
||||||
|
"0",
|
||||||
|
) > 0
|
||||||
|
? Math.round(
|
||||||
|
(parseFloat(
|
||||||
|
statsData
|
||||||
|
?.ongoing_innovation_technology_ideas ||
|
||||||
|
"0",
|
||||||
|
) /
|
||||||
|
parseFloat(
|
||||||
|
statsData
|
||||||
|
?.registered_innovation_technology_idea ||
|
||||||
|
"1",
|
||||||
|
)) *
|
||||||
|
100,
|
||||||
|
)
|
||||||
|
: 0,
|
||||||
|
)}
|
||||||
|
</tspan>
|
||||||
|
</text>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</PolarRadiusAxis>
|
||||||
|
</RadialBarChart>
|
||||||
|
</ChartContainer>
|
||||||
|
<div className="font-bold font-persian text-center">
|
||||||
|
<div className="flex flex-col justify-between items-center gap-2">
|
||||||
|
<span className="flex font-bold items-center gap-1 text-base">
|
||||||
|
<div className="font-light text-sm">ثبت شده :</div>
|
||||||
|
{formatNumber(
|
||||||
|
statsData
|
||||||
|
?.registered_innovation_technology_idea || "0",
|
||||||
|
)}
|
||||||
|
</span>
|
||||||
|
<span className="flex items-center gap-1 font-bold text-base">
|
||||||
|
<div className="font-light text-sm">در حال اجرا :</div>
|
||||||
|
{formatNumber(
|
||||||
|
statsData
|
||||||
|
?.ongoing_innovation_technology_ideas || "0",
|
||||||
|
)}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</BaseCard>
|
||||||
|
|
||||||
|
<MetricCard
|
||||||
|
title="درآمد افزایش یافته"
|
||||||
|
value={statsData?.increased_revenue_from_ideas?.replaceAll("," , "") || "0"}
|
||||||
|
percentValue={statsData?.increased_revenue_from_ideas_percent}
|
||||||
|
percentLabel="درصد به کل درآمد"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Details Dialog */}
|
{/* Details Dialog */}
|
||||||
<Dialog open={isDetailsOpen} onOpenChange={setIsDetailsOpen}>
|
<Dialog open={isDetailsOpen} onOpenChange={setIsDetailsOpen}>
|
||||||
<DialogContent className="max-w-4xl max-h-[90vh] overflow-y-auto">
|
<DialogContent className="bg-[linear-gradient(to_bottom_left,#464861,50%,#111628)] max-w-6xl max-h-[90vh] overflow-y-auto">
|
||||||
<DialogHeader>
|
<DialogHeader className="border-b border-gray-600/30 pb-4 mb-6">
|
||||||
<DialogTitle className="text-right font-persian text-xl">
|
<DialogTitle className="text-right font-persian text-xl text-white flex items-center justify-between">
|
||||||
جزئیات ایده: {selectedIdea?.idea_title}
|
<span>عنوان ایده: میکروکاتالیزورهای دما بالا</span>
|
||||||
</DialogTitle>
|
</DialogTitle>
|
||||||
</DialogHeader>
|
</DialogHeader>
|
||||||
|
|
||||||
{selectedIdea && (
|
{selectedIdea && <div className="flex w-full justify-center gap-4">
|
||||||
<div className="space-y-6 text-right font-persian">
|
<div className="flex gap-4 flex-col text-right font-persian w-full border-l-2 border-l-pr-gray px-4 pb-4">
|
||||||
<div className="grid grid-cols-1 md:grid-cols-2 gap-6">
|
{/* مشخصات ایده پردازان Section */}
|
||||||
<div className="space-y-4">
|
<div className="">
|
||||||
<div>
|
<h3 className="text-base font-bold text-white mb-2">
|
||||||
<label className="block text-sm font-medium text-muted-foreground mb-1">
|
مشخصات ایده پردازان
|
||||||
نام و نام خانوادگی
|
</h3>
|
||||||
</label>
|
<div className="flex flex-col gap-4 mr-5">
|
||||||
<p className="text-foreground">{selectedIdea.full_name || "-"}</p>
|
<div className="grid grid-cols-3 items-center gap-2">
|
||||||
|
<div className="flex items-center gap-2">
|
||||||
|
<Hexagon className="stroke-pr-green h-5 w-5 stroke-[1px]"/>
|
||||||
|
<span className="text-white text-sm text-light">نام ایده پرداز:</span>
|
||||||
|
</div>
|
||||||
|
<span className="text-white font-normal text-sm mr-10">{selectedIdea.full_name || "-"}</span>
|
||||||
</div>
|
</div>
|
||||||
|
<div className="grid grid-cols-3 items-center gap-2">
|
||||||
<div>
|
<div className="flex items-center gap-2">
|
||||||
<label className="block text-sm font-medium text-muted-foreground mb-1">
|
<Hexagon className="stroke-pr-green h-5 w-5 stroke-[1px]" />
|
||||||
شماره پرسنلی
|
<span className="text-white text-sm text-light">شماره پرسنلی:</span>
|
||||||
</label>
|
</div>
|
||||||
<p className="text-foreground">{toPersianDigits(selectedIdea.personnel_number) || "-"}</p>
|
<span className="text-white font-normal text-sm mr-10">{toPersianDigits(selectedIdea.personnel_number) || "۱۳۰۶۵۸۰۶"}</span>
|
||||||
</div>
|
</div>
|
||||||
|
<div className="grid grid-cols-3 items-center gap-2">
|
||||||
<div>
|
<div className="flex items-center gap-2">
|
||||||
<label className="block text-sm font-medium text-muted-foreground mb-1">
|
<Hexagon className="stroke-pr-green h-5 w-5 stroke-[1px]" />
|
||||||
مدیریت
|
<span className="text-white text-sm text-light">مدیریت:</span>
|
||||||
</label>
|
</div>
|
||||||
<p className="text-foreground">{selectedIdea.management || "-"}</p>
|
<span className="text-white font-normal text-sm mr-10">{selectedIdea.management || "مدیریت توسعه"}</span>
|
||||||
</div>
|
</div>
|
||||||
|
<div className="grid grid-cols-3 items-center gap-2">
|
||||||
<div>
|
<div className="flex items-center gap-2">
|
||||||
<label className="block text-sm font-medium text-muted-foreground mb-1">
|
<Hexagon className="stroke-pr-green h-5 w-5 stroke-[1px]" />
|
||||||
معاونت مربوطه
|
<span className="text-white text-sm text-light">معاونت:</span>
|
||||||
</label>
|
</div>
|
||||||
<p className="text-foreground">{selectedIdea.deputy || "-"}</p>
|
<span className="text-white font-normal text-sm mr-10">{selectedIdea.deputy || "توسعه"}</span>
|
||||||
</div>
|
</div>
|
||||||
|
<div className="grid grid-cols-3 items-center gap-2 col-span-2">
|
||||||
<div>
|
<div className="flex items-center gap-2">
|
||||||
<label className="block text-sm font-medium text-muted-foreground mb-1">
|
<Hexagon className="stroke-pr-green h-5 w-5 stroke-[1px]" />
|
||||||
نوع نوآوری
|
<span className="text-white text-sm text-light">اعضای تیم:</span>
|
||||||
</label>
|
</div>
|
||||||
<p className="text-foreground">{selectedIdea.innovation_type || "-"}</p>
|
<span className="text-white font-normal text-sm mr-10">
|
||||||
</div>
|
{selectedIdea.innovator_team_members || "رضا حسین پور, محمد رضا شیاطی, محمد مددی"}
|
||||||
|
</span>
|
||||||
<div>
|
|
||||||
<label className="block text-sm font-medium text-muted-foreground mb-1">
|
|
||||||
میزان اصالت ایده
|
|
||||||
</label>
|
|
||||||
<p className="text-foreground">{selectedIdea.idea_originality || "-"}</p>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div>
|
|
||||||
<label className="block text-sm font-medium text-muted-foreground mb-1">
|
|
||||||
محور ایده
|
|
||||||
</label>
|
|
||||||
<p className="text-foreground">{selectedIdea.idea_axis || "-"}</p>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
<div className="space-y-4">
|
{/* مشخصات ایده Section */}
|
||||||
<div>
|
<div className="">
|
||||||
<label className="block text-sm font-medium text-muted-foreground mb-1">
|
<h3 className="text-base font-bold text-white mb-2">
|
||||||
اعضای تیم نوآور
|
مشخصات ایده
|
||||||
</label>
|
</h3>
|
||||||
<p className="text-foreground">{selectedIdea.innovator_team_members || "-"}</p>
|
<div className="flex flex-col gap-4 mr-5">
|
||||||
|
<div className="grid grid-cols-3 items-center gap-2">
|
||||||
|
<div className="flex items-center gap-2">
|
||||||
|
<Hexagon className="stroke-pr-green h-5 w-5 stroke-[1px]" />
|
||||||
|
<span className="text-white text-sm text-light">تاریخ ثبت ایده:</span>
|
||||||
|
</div>
|
||||||
|
<span className="text-white font-normal text-sm mr-10">{formatDate(selectedIdea.idea_registration_date) || "-"}</span>
|
||||||
</div>
|
</div>
|
||||||
|
<div className="grid grid-cols-3 items-center gap-2">
|
||||||
<div>
|
<div className="flex items-center gap-2">
|
||||||
<label className="block text-sm font-medium text-muted-foreground mb-1">
|
<Hexagon className="stroke-pr-green h-5 w-5 stroke-[1px]" />
|
||||||
شرح ایده
|
<span className="text-white text-sm text-light">نوع نوآوری:</span>
|
||||||
</label>
|
</div>
|
||||||
<p className="text-foreground leading-relaxed">
|
<span className="text-white font-normal text-sm mr-10">{selectedIdea.innovation_type || "-"}</span>
|
||||||
{selectedIdea.idea_description || "-"}
|
|
||||||
</p>
|
|
||||||
</div>
|
</div>
|
||||||
|
<div className="grid grid-cols-3 items-center gap-2">
|
||||||
<div>
|
<div className="flex items-center gap-2">
|
||||||
<label className="block text-sm font-medium text-muted-foreground mb-1">
|
<Hexagon className="stroke-pr-green h-5 w-5 stroke-[1px]" />
|
||||||
توضیح وضعیت فعلی ایده
|
<span className="text-white text-sm text-light">اصالت ایده:</span>
|
||||||
</label>
|
</div>
|
||||||
<p className="text-foreground leading-relaxed">
|
<span className="text-white font-normal text-sm mr-10">{selectedIdea.idea_originality || "-"}</span>
|
||||||
{selectedIdea.idea_current_status_description || "-"}
|
|
||||||
</p>
|
|
||||||
</div>
|
</div>
|
||||||
|
<div className="grid grid-cols-3 items-center gap-2">
|
||||||
<div>
|
<div className="flex items-center gap-2">
|
||||||
<label className="block text-sm font-medium text-muted-foreground mb-1">
|
<Hexagon className="stroke-pr-green h-5 w-5 stroke-[1px]" />
|
||||||
مزایای اجرای ایده
|
<span className="text-white text-sm text-light min-w-max">محور ایده:</span>
|
||||||
</label>
|
</div>
|
||||||
<p className="text-foreground leading-relaxed">
|
<span className="text-white font-normal text-sm mr-10">{selectedIdea.idea_axis || "-"}</span>
|
||||||
{selectedIdea.idea_execution_benefits || "-"}
|
|
||||||
</p>
|
|
||||||
</div>
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{/* نتایج و خروجی ها Section */}
|
||||||
|
<div>
|
||||||
|
<h3 className="text-base font-bold text-white mb-2">
|
||||||
|
نتایج و خروجی ها
|
||||||
|
</h3>
|
||||||
|
<div className="flex flex-col gap-4 mr-5">
|
||||||
|
<div className="grid grid-cols-3 items-center gap-2">
|
||||||
|
<div className="flex items-center gap-2">
|
||||||
|
<Hexagon className="stroke-pr-green h-5 w-5 stroke-[1px]" />
|
||||||
|
<span className="text-white text-sm text-light">درآمد حاصل:</span>
|
||||||
|
</div>
|
||||||
|
<span className="text-white text-sm font-normal mr-10">{formatNumber(selectedIdea.increased_revenue) || "-"}
|
||||||
|
<span className="text-[11px] mr-2 font-light">
|
||||||
|
میلیون ریال
|
||||||
|
|
||||||
<div>
|
</span>
|
||||||
<label className="block text-sm font-medium text-muted-foreground mb-1">
|
</span>
|
||||||
بهبودهای فرآیندی
|
|
||||||
</label>
|
|
||||||
<p className="text-foreground leading-relaxed">
|
|
||||||
{selectedIdea.process_improvements || "-"}
|
|
||||||
</p>
|
|
||||||
</div>
|
</div>
|
||||||
|
<div className="grid grid-cols-3 items-center gap-2">
|
||||||
|
<div className="flex items-center gap-2">
|
||||||
|
<Hexagon className="stroke-pr-green h-5 w-5 stroke-[1px]" />
|
||||||
|
<span className="text-white text-sm text-light">مقاله چاپ شده:</span>
|
||||||
|
</div>
|
||||||
|
<span className="text-white font-normal cursor-pointer text-sm flex items-center gap-2 mr-10">
|
||||||
|
<Download className="h-4 w-4" />
|
||||||
|
|
||||||
<div>
|
دانلود
|
||||||
<label className="block text-sm font-medium text-muted-foreground mb-1">
|
</span>
|
||||||
درآمد حاصل از ایده
|
</div>
|
||||||
</label>
|
<div className="grid grid-cols-3 items-center gap-2">
|
||||||
<p className="text-success font-medium">
|
<div className="flex items-center gap-2">
|
||||||
{formatCurrency(selectedIdea.increased_revenue)}
|
<Hexagon className="stroke-pr-green h-5 w-5 stroke-[1px]" />
|
||||||
</p>
|
<span className="text-white text-sm text-light">پتنت ثبت شده:</span>
|
||||||
|
</div>
|
||||||
|
<span className="text-white cursor-pointer font-normal text-sm flex items-center gap-2 mr-10">
|
||||||
|
<Download className="h-4 w-4"/>
|
||||||
|
دانلود
|
||||||
|
</span>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
)}
|
<div className="w-full flex flex-col gap-8">
|
||||||
|
|
||||||
|
{/* شرح ایده Section */}
|
||||||
|
<div>
|
||||||
|
<h3 className="text-base font-bold text-white mb-4">
|
||||||
|
شرح ایده
|
||||||
|
</h3>
|
||||||
|
<div className="">
|
||||||
|
<p className="text-white text-sm">
|
||||||
|
{selectedIdea.idea_description ||
|
||||||
|
"-"
|
||||||
|
}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* شرح وضعیت موجود ایده Section */}
|
||||||
|
<div>
|
||||||
|
<h3 className="text-base font-bold text-white mb-4">
|
||||||
|
شرح وضعیت موجود ایده
|
||||||
|
</h3>
|
||||||
|
<div className="">
|
||||||
|
<p className="text-white leading-relaxed text-sm">
|
||||||
|
{selectedIdea.idea_current_status_description ||
|
||||||
|
"-"
|
||||||
|
}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* منافع حاصل از ایده Section */}
|
||||||
|
<div>
|
||||||
|
<h3 className="text-base font-bold text-white mb-4">
|
||||||
|
منافع حاصل از ایده
|
||||||
|
</h3>
|
||||||
|
<div>
|
||||||
|
<p className="text-white leading-relaxed text-sm">
|
||||||
|
{selectedIdea.idea_execution_benefits ||
|
||||||
|
"-"
|
||||||
|
}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* بهبود های فرآیندی ایده Section */}
|
||||||
|
<div>
|
||||||
|
<h3 className="text-base font-bold text-white mb-4">
|
||||||
|
بهبود های فرآیندی ایده
|
||||||
|
</h3>
|
||||||
|
<div>
|
||||||
|
<p className="text-white leading-relaxed text-sm">
|
||||||
|
{selectedIdea.process_improvements ||
|
||||||
|
"-"
|
||||||
|
}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
</div>
|
||||||
|
</div>}
|
||||||
</DialogContent>
|
</DialogContent>
|
||||||
</Dialog>
|
</Dialog>
|
||||||
</div>
|
</div>
|
||||||
|
|
|
||||||
|
|
@ -15,8 +15,9 @@ import moment from "moment-jalaali";
|
||||||
import { useCallback, useEffect, useRef, useState } from "react";
|
import { useCallback, useEffect, useRef, useState } from "react";
|
||||||
import toast from "react-hot-toast";
|
import toast from "react-hot-toast";
|
||||||
import { Badge } from "~/components/ui/badge";
|
import { Badge } from "~/components/ui/badge";
|
||||||
import { Button } from "~/components/ui/button";
|
|
||||||
import { BaseCard } from "~/components/ui/base-card";
|
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 { Checkbox } from "~/components/ui/checkbox";
|
||||||
import { CustomBarChart } from "~/components/ui/custom-bar-chart";
|
import { CustomBarChart } from "~/components/ui/custom-bar-chart";
|
||||||
import {
|
import {
|
||||||
|
|
@ -36,7 +37,6 @@ import {
|
||||||
import apiService from "~/lib/api";
|
import apiService from "~/lib/api";
|
||||||
import { formatNumber } from "~/lib/utils";
|
import { formatNumber } from "~/lib/utils";
|
||||||
import { DashboardLayout } from "../layout";
|
import { DashboardLayout } from "../layout";
|
||||||
import { Card , CardContent} from "~/components/ui/card";
|
|
||||||
|
|
||||||
moment.loadPersian({ usePersianDigits: true });
|
moment.loadPersian({ usePersianDigits: true });
|
||||||
interface ProcessInnovationData {
|
interface ProcessInnovationData {
|
||||||
|
|
@ -176,7 +176,7 @@ export function ProcessInnovationPage() {
|
||||||
stats.currencyReductionSum.toFixed?.(0) ?? stats.currencyReductionSum
|
stats.currencyReductionSum.toFixed?.(0) ?? stats.currencyReductionSum
|
||||||
),
|
),
|
||||||
description: "دلار کاهش یافته",
|
description: "دلار کاهش یافته",
|
||||||
icon: DollarSign ,
|
icon: DollarSign,
|
||||||
color: "text-emerald-400",
|
color: "text-emerald-400",
|
||||||
},
|
},
|
||||||
frequentfailuresreduction: {
|
frequentfailuresreduction: {
|
||||||
|
|
@ -551,7 +551,11 @@ export function ProcessInnovationPage() {
|
||||||
</Badge>
|
</Badge>
|
||||||
);
|
);
|
||||||
case "title":
|
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":
|
case "project_status":
|
||||||
return (
|
return (
|
||||||
<div className="flex items-center gap-1">
|
<div className="flex items-center gap-1">
|
||||||
|
|
@ -567,7 +571,10 @@ export function ProcessInnovationPage() {
|
||||||
);
|
);
|
||||||
case "project_rating":
|
case "project_rating":
|
||||||
return (
|
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))}
|
{formatNumber(String(value))}
|
||||||
</Badge>
|
</Badge>
|
||||||
);
|
);
|
||||||
|
|
@ -595,7 +602,10 @@ export function ProcessInnovationPage() {
|
||||||
{loading || statsLoading
|
{loading || statsLoading
|
||||||
? // Loading skeleton for stats cards - matching new design
|
? // Loading skeleton for stats cards - matching new design
|
||||||
Array.from({ length: 4 }).map((_, index) => (
|
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 flex-col justify-between gap-2">
|
||||||
<div className="flex justify-between items-center border-b-2 mx-4 border-gray-500/20">
|
<div className="flex justify-between items-center border-b-2 mx-4 border-gray-500/20">
|
||||||
<div
|
<div
|
||||||
|
|
@ -621,7 +631,10 @@ export function ProcessInnovationPage() {
|
||||||
))
|
))
|
||||||
: Object.entries(stateCard).map(([key, card]) => {
|
: Object.entries(stateCard).map(([key, card]) => {
|
||||||
// map percent values for each card key
|
// map percent values for each card key
|
||||||
const percentMap: Record<string, number | string | undefined> = {
|
const percentMap: Record<
|
||||||
|
string,
|
||||||
|
number | string | undefined
|
||||||
|
> = {
|
||||||
productionstopsprevention: stats.percentProductionStops,
|
productionstopsprevention: stats.percentProductionStops,
|
||||||
bottleneckremoval: stats.percentBottleneckRemoval,
|
bottleneckremoval: stats.percentBottleneckRemoval,
|
||||||
currencyreduction: stats.percentCurrencyReduction,
|
currencyreduction: stats.percentCurrencyReduction,
|
||||||
|
|
@ -640,7 +653,7 @@ export function ProcessInnovationPage() {
|
||||||
<div className="flex items-center gap-4">
|
<div className="flex items-center gap-4">
|
||||||
<div className="text-center">
|
<div className="text-center">
|
||||||
<p className="text-3xl text-pr-green font-bold mb-1">
|
<p className="text-3xl text-pr-green font-bold mb-1">
|
||||||
{(card.value)}
|
{card.value}
|
||||||
</p>
|
</p>
|
||||||
<div className="text-[11px] text-[#ACACAC] font-light font-persian">
|
<div className="text-[11px] text-[#ACACAC] font-light font-persian">
|
||||||
{card.description}
|
{card.description}
|
||||||
|
|
@ -846,10 +859,12 @@ export function ProcessInnovationPage() {
|
||||||
شرح پروژه
|
شرح پروژه
|
||||||
</DialogTitle>
|
</DialogTitle>
|
||||||
</DialogHeader>
|
</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 */}
|
{/* Project Description */}
|
||||||
<div className="flex-[4] border-l-2 border-gray-600">
|
<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">
|
<p className="text-white font-normal text-base font-persian px-2 mt-2">
|
||||||
{selectedProjectDetails?.project_description || "-"}
|
{selectedProjectDetails?.project_description || "-"}
|
||||||
</p>
|
</p>
|
||||||
|
|
|
||||||
|
|
@ -616,22 +616,22 @@ export function ProductInnovationPage() {
|
||||||
size="sm"
|
size="sm"
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
handleProjectDetails(item)}}
|
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>
|
</Button>
|
||||||
);
|
);
|
||||||
case "project_no":
|
case "project_no":
|
||||||
return (
|
return (
|
||||||
<Badge variant="outline" className="font-mono text-base font-light">
|
<Badge variant="outline" className="font-mono text-sm font-light">
|
||||||
{String(value)}
|
{String(value)}
|
||||||
</Badge>
|
</Badge>
|
||||||
);
|
);
|
||||||
case "title":
|
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":
|
case "project_status":
|
||||||
return (
|
return (
|
||||||
<div className="flex items-center text-base font-light gap-1">
|
<div className="flex items-center text-sm font-light gap-1">
|
||||||
<Badge
|
<Badge
|
||||||
variant={statusColor(value as projectStatus)}
|
variant={statusColor(value as projectStatus)}
|
||||||
className="font-semibold text-base border-2 p-0 block w-2 h-2 rounded-full"
|
className="font-semibold text-base border-2 p-0 block w-2 h-2 rounded-full"
|
||||||
|
|
@ -652,7 +652,7 @@ export function ProductInnovationPage() {
|
||||||
</Badge>
|
</Badge>
|
||||||
);
|
);
|
||||||
default:
|
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>;
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -1,25 +1,15 @@
|
||||||
import {
|
import {
|
||||||
Box,
|
|
||||||
Building2,
|
|
||||||
ChevronDown,
|
ChevronDown,
|
||||||
ChevronRight,
|
|
||||||
FolderKanban,
|
|
||||||
GalleryVerticalEnd,
|
GalleryVerticalEnd,
|
||||||
Globe,
|
House,
|
||||||
LayoutDashboard,
|
LightbulbIcon,
|
||||||
Leaf,
|
ListTodo,
|
||||||
Lightbulb,
|
|
||||||
LogOut,
|
LogOut,
|
||||||
MonitorSmartphone,
|
Radar,
|
||||||
Package,
|
|
||||||
Settings,
|
Settings,
|
||||||
Star,
|
Star,
|
||||||
Workflow,
|
Workflow,
|
||||||
DiscAlbum,
|
DiscAlbum,
|
||||||
House,
|
|
||||||
ListTodo,
|
|
||||||
LightbulbIcon,
|
|
||||||
Radar,
|
|
||||||
LucideLightbulb
|
LucideLightbulb
|
||||||
} from "lucide-react";
|
} from "lucide-react";
|
||||||
import React, { useState } from "react";
|
import React, { useState } from "react";
|
||||||
|
|
@ -50,7 +40,6 @@ interface MenuItem {
|
||||||
}
|
}
|
||||||
|
|
||||||
const menuItems: MenuItem[] = [
|
const menuItems: MenuItem[] = [
|
||||||
|
|
||||||
{
|
{
|
||||||
id: "dashboard",
|
id: "dashboard",
|
||||||
label: "صفحه اصلی",
|
label: "صفحه اصلی",
|
||||||
|
|
@ -118,7 +107,6 @@ const menuItems: MenuItem[] = [
|
||||||
icon: null,
|
icon: null,
|
||||||
href: "#", // This is not a route, it opens a popup
|
href: "#", // This is not a route, it opens a popup
|
||||||
},
|
},
|
||||||
|
|
||||||
];
|
];
|
||||||
|
|
||||||
const bottomMenuItems: MenuItem[] = [
|
const bottomMenuItems: MenuItem[] = [
|
||||||
|
|
@ -167,7 +155,10 @@ export function Sidebar({
|
||||||
// Update header title based on current route
|
// Update header title based on current route
|
||||||
// If a child route is active, use that child's label prefixed by parent label
|
// If a child route is active, use that child's label prefixed by parent label
|
||||||
let activeTitle: string | undefined = undefined;
|
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) => {
|
menuItems.forEach((item) => {
|
||||||
if (item.children) {
|
if (item.children) {
|
||||||
const activeChild = item.children.find(
|
const activeChild = item.children.find(
|
||||||
|
|
@ -185,7 +176,10 @@ export function Sidebar({
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
if (onTitleChange) {
|
if (onTitleChange) {
|
||||||
onTitleChange({ title: activeTitle ?? "صفحه اول", icon: activeIcon ?? null });
|
onTitleChange({
|
||||||
|
title: activeTitle ?? "صفحه اول",
|
||||||
|
icon: activeIcon ?? null,
|
||||||
|
});
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
@ -256,7 +250,7 @@ export function Sidebar({
|
||||||
<button
|
<button
|
||||||
key={item.id}
|
key={item.id}
|
||||||
className={cn(
|
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}
|
onClick={handleClick}
|
||||||
>
|
>
|
||||||
|
|
@ -266,8 +260,7 @@ export function Sidebar({
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
</button>
|
</button>
|
||||||
)
|
);
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
|
|
@ -326,10 +319,10 @@ export function Sidebar({
|
||||||
"w-full text-right",
|
"w-full text-right",
|
||||||
// Disable pointer cursor when child is active (cannot collapse)
|
// Disable pointer cursor when child is active (cannot collapse)
|
||||||
item.children &&
|
item.children &&
|
||||||
item.children.some(
|
item.children.some(
|
||||||
(child) => child.href && location.pathname === child.href
|
(child) => child.href && location.pathname === child.href
|
||||||
) &&
|
) &&
|
||||||
"cursor-not-allowed"
|
"cursor-not-allowed"
|
||||||
)}
|
)}
|
||||||
onClick={handleClick}
|
onClick={handleClick}
|
||||||
>
|
>
|
||||||
|
|
@ -430,9 +423,9 @@ export function Sidebar({
|
||||||
/>
|
/>
|
||||||
<div className="font-persian">
|
<div className="font-persian">
|
||||||
<div className="text-sm font-semibold text-white">
|
<div className="text-sm font-semibold text-white">
|
||||||
اینوژن بندر امام
|
داشبورد مدیریت فناوری و نوآوری
|
||||||
</div>
|
</div>
|
||||||
<div className="text-xs text-gray-400">نسخه ۰.۱</div>
|
{/* <div className="text-xs text-gray-400">نسخه ۰.۱</div> */}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
) : (
|
) : (
|
||||||
|
|
|
||||||
|
|
@ -1,25 +1,25 @@
|
||||||
import React, { useEffect, useState } from "react";
|
import { useEffect, useReducer, useRef, useState } from "react";
|
||||||
import {
|
import {
|
||||||
Dialog,
|
|
||||||
DialogContent,
|
|
||||||
DialogHeader,
|
|
||||||
DialogTitle,
|
|
||||||
} from "~/components/ui/dialog";
|
|
||||||
import {
|
|
||||||
BarChart,
|
|
||||||
Bar,
|
Bar,
|
||||||
|
BarChart,
|
||||||
|
CartesianGrid,
|
||||||
|
Cell,
|
||||||
|
LabelList,
|
||||||
|
ResponsiveContainer,
|
||||||
XAxis,
|
XAxis,
|
||||||
YAxis,
|
YAxis,
|
||||||
CartesianGrid,
|
|
||||||
Tooltip,
|
|
||||||
ResponsiveContainer,
|
|
||||||
LabelList,
|
|
||||||
Cell,
|
|
||||||
} from "recharts";
|
} from "recharts";
|
||||||
import apiService from "~/lib/api";
|
import { Dialog, DialogContent, DialogHeader } from "~/components/ui/dialog";
|
||||||
import { Skeleton } from "~/components/ui/skeleton";
|
import { Skeleton } from "~/components/ui/skeleton";
|
||||||
|
import apiService from "~/lib/api";
|
||||||
import { formatNumber } from "~/lib/utils";
|
import { formatNumber } from "~/lib/utils";
|
||||||
import { ChartContainer } from "../ui/chart";
|
import { ChartContainer } from "../ui/chart";
|
||||||
|
import {
|
||||||
|
DropdownMenu,
|
||||||
|
DropdownMenuButton,
|
||||||
|
DropdownMenuContent,
|
||||||
|
DropdownMenuItem,
|
||||||
|
} from "../ui/dropdown-menu";
|
||||||
import { TruncatedText } from "../ui/truncatedText";
|
import { TruncatedText } from "../ui/truncatedText";
|
||||||
|
|
||||||
interface StrategicAlignmentData {
|
interface StrategicAlignmentData {
|
||||||
|
|
@ -28,6 +28,51 @@ interface StrategicAlignmentData {
|
||||||
percentage?: number;
|
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 {
|
interface StrategicAlignmentPopupProps {
|
||||||
open: boolean;
|
open: boolean;
|
||||||
onOpenChange: (open: boolean) => void;
|
onOpenChange: (open: boolean) => void;
|
||||||
|
|
@ -41,11 +86,10 @@ const chartConfig = {
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
const maxHeight = 150;
|
const maxHeight = 150;
|
||||||
const barHeights = () => Math.floor(Math.random() * maxHeight);
|
const barHeights = () => Math.floor(Math.random() * maxHeight);
|
||||||
|
|
||||||
const ChartSkeleton = () => (
|
const ChartSkeleton = () => (
|
||||||
|
|
||||||
<div className="flex justify-center h-96 w-full p-4">
|
<div className="flex justify-center h-96 w-full p-4">
|
||||||
{/* Chart bars */}
|
{/* Chart bars */}
|
||||||
<div className=" w-full flex items-end gap-10">
|
<div className=" w-full flex items-end gap-10">
|
||||||
|
|
@ -58,7 +102,7 @@ const ChartSkeleton = () => (
|
||||||
</div>
|
</div>
|
||||||
))}
|
))}
|
||||||
</div>
|
</div>
|
||||||
{/* Left space for Y-axis label */}
|
{/* Left space for Y-axis label */}
|
||||||
<div className="flex flex-col justify-between mr-2">
|
<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" />
|
||||||
<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) {
|
}: StrategicAlignmentPopupProps) {
|
||||||
const [data, setData] = useState<StrategicAlignmentData[]>([]);
|
const [data, setData] = useState<StrategicAlignmentData[]>([]);
|
||||||
const [loading, setLoading] = useState(false);
|
const [loading, setLoading] = useState(false);
|
||||||
|
const contentRef = useRef<HTMLDivElement | null>(null);
|
||||||
|
const [state, dispatch] = useReducer(reducer, {
|
||||||
|
isOpen: false,
|
||||||
|
selectedValue: "همه مضامین",
|
||||||
|
dropDownItems: [],
|
||||||
|
});
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (open) {
|
if (open) {
|
||||||
|
|
@ -98,29 +148,12 @@ export function StrategicAlignmentPopup({
|
||||||
? JSON.parse(response.data)
|
? JSON.parse(response.data)
|
||||||
: response.data;
|
: response.data;
|
||||||
|
|
||||||
const processedData = responseData
|
setBarItems(responseData);
|
||||||
.map((item: any) => ({
|
const dropDownItems = responseData.map(
|
||||||
strategic_theme: item.strategic_theme || "N/A",
|
(item: any) => item.strategic_theme
|
||||||
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(
|
setDropDownValues(["همه مضامین", ...dropDownItems]);
|
||||||
(item: StrategicAlignmentData) => ({
|
|
||||||
...item,
|
|
||||||
percentage:
|
|
||||||
total > 0
|
|
||||||
? Math.round((item.operational_fee_sum / total) * 100)
|
|
||||||
: 0,
|
|
||||||
})
|
|
||||||
);
|
|
||||||
setData(dataWithPercentage || []);
|
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error("Error fetching strategic alignment data:", error);
|
console.error("Error fetching strategic alignment data:", error);
|
||||||
} finally {
|
} 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 (
|
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">
|
<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">
|
<DialogHeader className="mb-10 w-full border-b-2 border-gray-500/20">
|
||||||
<DialogTitle className="ml-auto text-sm text-white ">میزان انطباق راهبردی</DialogTitle>
|
<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>
|
</DialogHeader>
|
||||||
|
|
||||||
{loading ? (
|
{loading ? (
|
||||||
<ChartSkeleton />
|
<ChartSkeleton />
|
||||||
) : (
|
) : (
|
||||||
<>
|
<>
|
||||||
<ResponsiveContainer width="100%" height={400}>
|
<ResponsiveContainer width="100%" height={400}>
|
||||||
<ChartContainer config={chartConfig} className="aspect-auto h-96 w-full">
|
<ChartContainer
|
||||||
|
config={chartConfig}
|
||||||
|
className="aspect-auto h-96 w-full"
|
||||||
|
>
|
||||||
<BarChart
|
<BarChart
|
||||||
data={data}
|
data={data}
|
||||||
margin={{ left: 12, right: 12 }}
|
margin={{ left: 12, right: 12 }}
|
||||||
|
|
@ -149,7 +331,7 @@ export function StrategicAlignmentPopup({
|
||||||
accessibilityLayer
|
accessibilityLayer
|
||||||
>
|
>
|
||||||
<CartesianGrid vertical={false} stroke="#475569" />
|
<CartesianGrid vertical={false} stroke="#475569" />
|
||||||
<XAxis
|
<XAxis
|
||||||
dataKey="strategic_theme"
|
dataKey="strategic_theme"
|
||||||
tickLine={false}
|
tickLine={false}
|
||||||
axisLine={false}
|
axisLine={false}
|
||||||
|
|
@ -161,11 +343,8 @@ export function StrategicAlignmentPopup({
|
||||||
return (
|
return (
|
||||||
<g transform={`translate(${x},${y})`}>
|
<g transform={`translate(${x},${y})`}>
|
||||||
<foreignObject width={80} height={20} x={-45} y={0}>
|
<foreignObject width={80} height={20} x={-45} y={0}>
|
||||||
<TruncatedText
|
<TruncatedText maxWords={2} text={payload.value} />
|
||||||
maxWords={2}
|
</foreignObject>
|
||||||
text={payload.value}
|
|
||||||
/>
|
|
||||||
</foreignObject>
|
|
||||||
</g>
|
</g>
|
||||||
);
|
);
|
||||||
}}
|
}}
|
||||||
|
|
@ -179,37 +358,38 @@ export function StrategicAlignmentPopup({
|
||||||
tickFormatter={(value) =>
|
tickFormatter={(value) =>
|
||||||
`${formatNumber(Math.round(value))}`
|
`${formatNumber(Math.round(value))}`
|
||||||
}
|
}
|
||||||
|
label={{
|
||||||
|
value: "تعداد برنامه ها",
|
||||||
label={{
|
angle: -90,
|
||||||
value: "تعداد برنامه ها" ,
|
position: "insideLeft",
|
||||||
angle: -90,
|
fill: "#94a3b8",
|
||||||
position: "insideLeft",
|
fontSize: 11,
|
||||||
fill: "#94a3b8",
|
offset: 0,
|
||||||
fontSize: 11,
|
dy: 0,
|
||||||
offset: 0,
|
style: { textAnchor: "middle" },
|
||||||
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) => (
|
{data.map((entry, index) => (
|
||||||
<Cell key={`cell-${index}`} fill={chartConfig.percentage.color} />
|
<Cell
|
||||||
|
key={`cell-${index}`}
|
||||||
|
fill={chartConfig.percentage.color}
|
||||||
|
/>
|
||||||
))}
|
))}
|
||||||
<LabelList
|
<LabelList
|
||||||
dataKey="percentage"
|
dataKey="percentage"
|
||||||
position="top"
|
position="top"
|
||||||
offset={15}
|
offset={15}
|
||||||
|
|
||||||
style={{
|
style={{
|
||||||
fill: "#ffffff",
|
fill: "#ffffff",
|
||||||
fontSize: "16px",
|
fontSize: "16px",
|
||||||
fontWeight: "bold",
|
fontWeight: "bold",
|
||||||
}}
|
}}
|
||||||
formatter={(v: number) => `${formatNumber(Math.round(v))}`}
|
formatter={(v: number) =>
|
||||||
|
`${formatNumber(Math.round(v))}`
|
||||||
|
}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
</Bar>
|
</Bar>
|
||||||
</BarChart>
|
</BarChart>
|
||||||
</ChartContainer>
|
</ChartContainer>
|
||||||
|
|
|
||||||
|
|
@ -3,7 +3,6 @@ import * as d3 from "d3";
|
||||||
import apiService from "../../lib/api";
|
import apiService from "../../lib/api";
|
||||||
import { useAuth } from "../../contexts/auth-context";
|
import { useAuth } from "../../contexts/auth-context";
|
||||||
|
|
||||||
// Get API base URL at module level to avoid process.env access in browser
|
|
||||||
const API_BASE_URL =
|
const API_BASE_URL =
|
||||||
import.meta.env.VITE_API_URL || "https://inogen-back.pelekan.org/api";
|
import.meta.env.VITE_API_URL || "https://inogen-back.pelekan.org/api";
|
||||||
|
|
||||||
|
|
@ -46,7 +45,6 @@ export interface NetworkGraphProps {
|
||||||
onNodeClick?: (node: CompanyDetails) => void;
|
onNodeClick?: (node: CompanyDetails) => void;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Helper to robustly parse backend response
|
|
||||||
function parseApiResponse(raw: any): any[] {
|
function parseApiResponse(raw: any): any[] {
|
||||||
let data = raw;
|
let data = raw;
|
||||||
try {
|
try {
|
||||||
|
|
@ -56,7 +54,6 @@ function parseApiResponse(raw: any): any[] {
|
||||||
return Array.isArray(data) ? data : [];
|
return Array.isArray(data) ? data : [];
|
||||||
}
|
}
|
||||||
|
|
||||||
// Check if we're in browser environment
|
|
||||||
function isBrowser(): boolean {
|
function isBrowser(): boolean {
|
||||||
return typeof window !== "undefined";
|
return typeof window !== "undefined";
|
||||||
}
|
}
|
||||||
|
|
@ -70,7 +67,6 @@ export function NetworkGraph({ onNodeClick }: NetworkGraphProps) {
|
||||||
const [error, setError] = useState<string | null>(null);
|
const [error, setError] = useState<string | null>(null);
|
||||||
const { token } = useAuth();
|
const { token } = useAuth();
|
||||||
|
|
||||||
// Ensure component only renders on client side
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (isBrowser()) {
|
if (isBrowser()) {
|
||||||
const timer = setTimeout(() => setIsMounted(true), 100);
|
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(() => {
|
useEffect(() => {
|
||||||
if (!isMounted) return;
|
if (!isMounted) return;
|
||||||
|
|
||||||
|
|
@ -99,18 +110,28 @@ export function NetworkGraph({ onNodeClick }: NetworkGraphProps) {
|
||||||
Object.keys(data[0] || {}),
|
Object.keys(data[0] || {}),
|
||||||
);
|
);
|
||||||
|
|
||||||
// Create center node
|
// نود مرکزی
|
||||||
const centerNode: Node = {
|
const centerNode: Node = {
|
||||||
id: "center",
|
id: "center",
|
||||||
label: "پتروشیمی بندر امام", //مرکز زیست بوم
|
label: "پتروشیمی بندر امام",
|
||||||
category: "center",
|
category: "center",
|
||||||
stageid: 0,
|
stageid: 0,
|
||||||
isCenter: true,
|
isCenter: true,
|
||||||
};
|
};
|
||||||
|
|
||||||
// Create ecosystem nodes
|
// دستهبندیها
|
||||||
const ecosystemNodes: Node[] = data.map((item: any) => ({
|
const categories = Array.from(new Set(data.map((item: any) => item.category)));
|
||||||
id: String(item.stageid),
|
|
||||||
|
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,
|
label: item.title,
|
||||||
category: item.category,
|
category: item.category,
|
||||||
stageid: item.stageid,
|
stageid: item.stageid,
|
||||||
|
|
@ -118,13 +139,16 @@ export function NetworkGraph({ onNodeClick }: NetworkGraphProps) {
|
||||||
rawData: item,
|
rawData: item,
|
||||||
}));
|
}));
|
||||||
|
|
||||||
// Create links (all nodes connected to center)
|
// لینکها: مرکز → دستهبندیها → نودهای نهایی
|
||||||
const graphLinks: Link[] = ecosystemNodes.map((node) => ({
|
const graphLinks: Link[] = [
|
||||||
source: "center",
|
...categoryNodes.map((cat) => ({ source: "center", target: cat.id })),
|
||||||
target: node.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);
|
setLinks(graphLinks);
|
||||||
} catch (err: any) {
|
} catch (err: any) {
|
||||||
if (err.name !== "AbortError") {
|
if (err.name !== "AbortError") {
|
||||||
|
|
@ -142,43 +166,18 @@ export function NetworkGraph({ onNodeClick }: NetworkGraphProps) {
|
||||||
aborted = true;
|
aborted = true;
|
||||||
controller.abort();
|
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(() => {
|
useEffect(() => {
|
||||||
if (!isMounted || !svgRef.current || isLoading || nodes.length === 0) {
|
if (!isMounted || !svgRef.current || isLoading || nodes.length === 0) return;
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
const svg = d3.select(svgRef.current);
|
const svg = d3.select(svgRef.current);
|
||||||
const width = svgRef.current.clientWidth;
|
const width = svgRef.current.clientWidth;
|
||||||
const height = svgRef.current.clientHeight;
|
const height = svgRef.current.clientHeight;
|
||||||
|
|
||||||
// Clear previous content
|
|
||||||
svg.selectAll("*").remove();
|
svg.selectAll("*").remove();
|
||||||
|
|
||||||
// Create defs for patterns and filters
|
|
||||||
const defs = svg.append("defs");
|
const defs = svg.append("defs");
|
||||||
|
|
||||||
// Add glow filter for hover effect
|
|
||||||
const filter = defs
|
const filter = defs
|
||||||
.append("filter")
|
.append("filter")
|
||||||
.attr("id", "glow")
|
.attr("id", "glow")
|
||||||
|
|
@ -196,20 +195,15 @@ export function NetworkGraph({ onNodeClick }: NetworkGraphProps) {
|
||||||
feMerge.append("feMergeNode").attr("in", "coloredBlur");
|
feMerge.append("feMergeNode").attr("in", "coloredBlur");
|
||||||
feMerge.append("feMergeNode").attr("in", "SourceGraphic");
|
feMerge.append("feMergeNode").attr("in", "SourceGraphic");
|
||||||
|
|
||||||
// Create zoom behavior
|
const container = svg.append("g");
|
||||||
|
|
||||||
const zoom = d3
|
const zoom = d3
|
||||||
.zoom<SVGSVGElement, unknown>()
|
.zoom<SVGSVGElement, unknown>()
|
||||||
.scaleExtent([0.8, 2.5]) // Limit zoom out to 1x, zoom in to 2.5x
|
.scaleExtent([0.3, 2.5])
|
||||||
.on("zoom", (event) => {
|
.on("zoom", (event) => container.attr("transform", event.transform));
|
||||||
container.attr("transform", event.transform);
|
|
||||||
});
|
|
||||||
|
|
||||||
svg.call(zoom);
|
svg.call(zoom);
|
||||||
|
|
||||||
// Create container group
|
|
||||||
const container = svg.append("g");
|
|
||||||
|
|
||||||
// Category colors
|
|
||||||
const categoryToColor: Record<string, string> = {
|
const categoryToColor: Record<string, string> = {
|
||||||
دانشگاه: "#3B82F6",
|
دانشگاه: "#3B82F6",
|
||||||
مشاور: "#10B981",
|
مشاور: "#10B981",
|
||||||
|
|
@ -222,7 +216,6 @@ export function NetworkGraph({ onNodeClick }: NetworkGraphProps) {
|
||||||
center: "#34D399",
|
center: "#34D399",
|
||||||
};
|
};
|
||||||
|
|
||||||
// Create force simulation
|
|
||||||
const simulation = d3
|
const simulation = d3
|
||||||
.forceSimulation<Node>(nodes)
|
.forceSimulation<Node>(nodes)
|
||||||
.force(
|
.force(
|
||||||
|
|
@ -231,16 +224,15 @@ export function NetworkGraph({ onNodeClick }: NetworkGraphProps) {
|
||||||
.forceLink<Node, Link>(links)
|
.forceLink<Node, Link>(links)
|
||||||
.id((d) => d.id)
|
.id((d) => d.id)
|
||||||
.distance(150)
|
.distance(150)
|
||||||
.strength(0.1),
|
.strength(0.2),
|
||||||
)
|
)
|
||||||
.force("charge", d3.forceManyBody().strength(-300))
|
.force("charge", d3.forceManyBody().strength(-300))
|
||||||
.force("center", d3.forceCenter(width / 2, height / 2))
|
.force("center", d3.forceCenter(width / 2, height / 2))
|
||||||
.force(
|
.force("radial", d3.forceRadial(d => d.isCenter ? 0 : 300, width/2, height/2))
|
||||||
"collision",
|
.force("collision", d3.forceCollide().radius((d) => (d.isCenter ? 50 : 35)));
|
||||||
d3.forceCollide().radius((d) => (d.isCenter ? 40 : 30)),
|
|
||||||
);
|
|
||||||
|
|
||||||
const initialScale = 0.85;
|
// Initial zoom to show entire graph
|
||||||
|
const initialScale = 0.6;
|
||||||
const initialTranslate = [
|
const initialTranslate = [
|
||||||
width / 2 - (width / 2) * initialScale,
|
width / 2 - (width / 2) * initialScale,
|
||||||
height / 2 - (height / 2) * initialScale,
|
height / 2 - (height / 2) * initialScale,
|
||||||
|
|
@ -252,25 +244,60 @@ export function NetworkGraph({ onNodeClick }: NetworkGraphProps) {
|
||||||
.scale(initialScale),
|
.scale(initialScale),
|
||||||
);
|
);
|
||||||
|
|
||||||
// Fix center node position
|
// Fix center node
|
||||||
const centerNode = nodes.find((n) => n.isCenter);
|
const centerNode = nodes.find(n => n.isCenter);
|
||||||
|
const categoryNodes = nodes.filter(n => !n.isCenter && n.stageid === -1);
|
||||||
|
|
||||||
if (centerNode) {
|
if (centerNode) {
|
||||||
centerNode.fx = width / 2;
|
const centerX = width / 2;
|
||||||
centerNode.fy = height / 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);
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// نودهای نهایی **هیچ fx/fy نداشته باشند**
|
||||||
|
// فقط forceLink آنها را به دستهها متصل نگه میدارد
|
||||||
|
|
||||||
|
|
||||||
|
// const finalNodes = nodes.filter(n => !n.isCenter && n.stageid !== -1);
|
||||||
|
|
||||||
// Create links
|
// 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
|
const link = container
|
||||||
.selectAll(".link")
|
.selectAll(".link")
|
||||||
.data(links)
|
.data(links)
|
||||||
.enter()
|
.enter()
|
||||||
.append("line")
|
.append("path")
|
||||||
.attr("class", "link")
|
.attr("class", "link")
|
||||||
.attr("stroke", "#E2E8F0")
|
.attr("stroke", "#E2E8F0")
|
||||||
.attr("stroke-width", 2)
|
.attr("stroke-width", 2)
|
||||||
.attr("stroke-opacity", 0.6);
|
.attr("stroke-opacity", 0.6)
|
||||||
|
.attr("fill", "none");
|
||||||
|
|
||||||
// Create node groups
|
|
||||||
const nodeGroup = container
|
const nodeGroup = container
|
||||||
.selectAll(".node")
|
.selectAll(".node")
|
||||||
.data(nodes)
|
.data(nodes)
|
||||||
|
|
@ -279,7 +306,6 @@ export function NetworkGraph({ onNodeClick }: NetworkGraphProps) {
|
||||||
.attr("class", "node")
|
.attr("class", "node")
|
||||||
.style("cursor", "pointer");
|
.style("cursor", "pointer");
|
||||||
|
|
||||||
// Add drag behavior
|
|
||||||
const drag = d3
|
const drag = d3
|
||||||
.drag<SVGGElement, Node>()
|
.drag<SVGGElement, Node>()
|
||||||
.on("start", (event, d) => {
|
.on("start", (event, d) => {
|
||||||
|
|
@ -301,18 +327,16 @@ export function NetworkGraph({ onNodeClick }: NetworkGraphProps) {
|
||||||
|
|
||||||
nodeGroup.call(drag);
|
nodeGroup.call(drag);
|
||||||
|
|
||||||
// Add node circles/rectangles
|
|
||||||
nodeGroup.each(function (d) {
|
nodeGroup.each(function (d) {
|
||||||
const group = d3.select(this);
|
const group = d3.select(this);
|
||||||
|
|
||||||
if (d.isCenter) {
|
if (d.isCenter) {
|
||||||
// Center node as rectangle
|
|
||||||
const rect = group
|
const rect = group
|
||||||
.append("rect")
|
.append("rect")
|
||||||
.attr("width", 150)
|
.attr("width", 200)
|
||||||
.attr("height", 60)
|
.attr("height", 80)
|
||||||
.attr("x", -75)
|
.attr("x", -100) // نصف عرض جدید منفی
|
||||||
.attr("y", -30)
|
.attr("y", -40) // نصف ارتفاع جدید منفی
|
||||||
.attr("rx", 8)
|
.attr("rx", 8)
|
||||||
.attr("ry", 8)
|
.attr("ry", 8)
|
||||||
.attr("fill", categoryToColor[d.category] || "#94A3B8")
|
.attr("fill", categoryToColor[d.category] || "#94A3B8")
|
||||||
|
|
@ -320,7 +344,6 @@ export function NetworkGraph({ onNodeClick }: NetworkGraphProps) {
|
||||||
.attr("stroke-width", 3)
|
.attr("stroke-width", 3)
|
||||||
.style("pointer-events", "none");
|
.style("pointer-events", "none");
|
||||||
|
|
||||||
// Add center image if available
|
|
||||||
if (d.imageUrl || d.isCenter) {
|
if (d.imageUrl || d.isCenter) {
|
||||||
const pattern = defs
|
const pattern = defs
|
||||||
.append("pattern")
|
.append("pattern")
|
||||||
|
|
@ -334,23 +357,21 @@ export function NetworkGraph({ onNodeClick }: NetworkGraphProps) {
|
||||||
.append("image")
|
.append("image")
|
||||||
.attr("x", 0)
|
.attr("x", 0)
|
||||||
.attr("y", 0)
|
.attr("y", 0)
|
||||||
.attr("width", 150)
|
.attr("width", 200) // ← هماندازه با مستطیل
|
||||||
.attr("height", 60)
|
.attr("height", 80)
|
||||||
.attr("href", d.isCenter ? "/main-circle.png" : d.imageUrl)
|
.attr("href", d.isCenter ? "/main-circle.png" : d.imageUrl)
|
||||||
.attr("preserveAspectRatio", "xMidYMid slice");
|
.attr("preserveAspectRatio", "xMidYMid slice");
|
||||||
|
|
||||||
rect.attr("fill", `url(#image-${d.id})`);
|
rect.attr("fill", `url(#image-${d.id})`);
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
// Regular nodes as circles
|
|
||||||
const circle = group
|
const circle = group
|
||||||
.append("circle")
|
.append("circle")
|
||||||
.attr("r", 25)
|
.attr("r", 25)
|
||||||
.attr("fill", categoryToColor[d.category] || "8#fff")
|
.attr("fill", categoryToColor[d.category] || "#fff")
|
||||||
.attr("stroke", "#FFFFFF")
|
.attr("stroke", "#FFFFFF")
|
||||||
.attr("stroke-width", 3);
|
.attr("stroke-width", 3);
|
||||||
|
|
||||||
// Add node image if available
|
|
||||||
if (d.imageUrl) {
|
if (d.imageUrl) {
|
||||||
const pattern = defs
|
const pattern = defs
|
||||||
.append("pattern")
|
.append("pattern")
|
||||||
|
|
@ -367,10 +388,8 @@ export function NetworkGraph({ onNodeClick }: NetworkGraphProps) {
|
||||||
.attr("width", 50)
|
.attr("width", 50)
|
||||||
.attr("height", 50)
|
.attr("height", 50)
|
||||||
.attr("href", d.imageUrl)
|
.attr("href", d.imageUrl)
|
||||||
.attr("backgroundColor", "#fff")
|
|
||||||
.attr("preserveAspectRatio", "xMidYMid slice");
|
.attr("preserveAspectRatio", "xMidYMid slice");
|
||||||
|
|
||||||
// Create circular clip path
|
|
||||||
defs
|
defs
|
||||||
.append("clipPath")
|
.append("clipPath")
|
||||||
.attr("id", `clip-${d.id}`)
|
.attr("id", `clip-${d.id}`)
|
||||||
|
|
@ -384,7 +403,6 @@ export function NetworkGraph({ onNodeClick }: NetworkGraphProps) {
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
// Add labels below nodes
|
|
||||||
const labels = nodeGroup
|
const labels = nodeGroup
|
||||||
.append("text")
|
.append("text")
|
||||||
.text((d) => d.label)
|
.text((d) => d.label)
|
||||||
|
|
@ -397,7 +415,6 @@ export function NetworkGraph({ onNodeClick }: NetworkGraphProps) {
|
||||||
.attr("stroke-width", 4)
|
.attr("stroke-width", 4)
|
||||||
.attr("paint-order", "stroke");
|
.attr("paint-order", "stroke");
|
||||||
|
|
||||||
// Add hover effects
|
|
||||||
nodeGroup
|
nodeGroup
|
||||||
.on("mouseenter", function (event, d) {
|
.on("mouseenter", function (event, d) {
|
||||||
if (d.isCenter) return;
|
if (d.isCenter) return;
|
||||||
|
|
@ -419,22 +436,17 @@ export function NetworkGraph({ onNodeClick }: NetworkGraphProps) {
|
||||||
.attr("stroke-width", 3);
|
.attr("stroke-width", 3);
|
||||||
});
|
});
|
||||||
|
|
||||||
// Add click handlers
|
|
||||||
nodeGroup.on("click", async function (event, d) {
|
nodeGroup.on("click", async function (event, d) {
|
||||||
event.stopPropagation();
|
event.stopPropagation();
|
||||||
|
|
||||||
// Don't handle center node clicks
|
|
||||||
if (d.isCenter) return;
|
if (d.isCenter) return;
|
||||||
|
|
||||||
if (onNodeClick && d.stageid) {
|
if (onNodeClick && d.stageid) {
|
||||||
try {
|
try {
|
||||||
// Fetch detailed company data
|
|
||||||
const res = await callAPI(d.stageid);
|
const res = await callAPI(d.stageid);
|
||||||
|
|
||||||
const responseData = JSON.parse(res.data);
|
const responseData = JSON.parse(res.data);
|
||||||
const fieldValues =
|
const fieldValues =
|
||||||
JSON.parse(responseData?.getvalue)?.[0]?.FieldValues || [];
|
JSON.parse(responseData?.getvalue)?.[0]?.FieldValues || [];
|
||||||
// Filter out image fields and find description
|
|
||||||
const filteredFields = fieldValues.filter(
|
const filteredFields = fieldValues.filter(
|
||||||
(field: any) =>
|
(field: any) =>
|
||||||
!["image", "img", "full_name", "about_collaboration"].includes(
|
!["image", "img", "full_name", "about_collaboration"].includes(
|
||||||
|
|
@ -461,7 +473,6 @@ export function NetworkGraph({ onNodeClick }: NetworkGraphProps) {
|
||||||
onNodeClick(companyDetails);
|
onNodeClick(companyDetails);
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error("Failed to fetch company details:", error);
|
console.error("Failed to fetch company details:", error);
|
||||||
// Fallback to basic info
|
|
||||||
const basicDetails: CompanyDetails = {
|
const basicDetails: CompanyDetails = {
|
||||||
id: d.id,
|
id: d.id,
|
||||||
label: d.label,
|
label: d.label,
|
||||||
|
|
@ -474,24 +485,26 @@ export function NetworkGraph({ onNodeClick }: NetworkGraphProps) {
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
// Update positions on simulation tick
|
|
||||||
simulation.on("tick", () => {
|
simulation.on("tick", () => {
|
||||||
link
|
link.attr("d", (d: any) => {
|
||||||
.attr("x1", (d) => (d.source as Node).x!)
|
const sx = (d.source as Node).x!;
|
||||||
.attr("y1", (d) => (d.source as Node).y!)
|
const sy = (d.source as Node).y!;
|
||||||
.attr("x2", (d) => (d.target as Node).x!)
|
const tx = (d.target as Node).x!;
|
||||||
.attr("y2", (d) => (d.target as Node).y!);
|
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})`);
|
nodeGroup.attr("transform", (d) => `translate(${d.x},${d.y})`);
|
||||||
});
|
});
|
||||||
|
|
||||||
// Cleanup function
|
|
||||||
return () => {
|
return () => {
|
||||||
simulation.stop();
|
simulation.stop();
|
||||||
};
|
};
|
||||||
}, [nodes, links, isLoading, isMounted, onNodeClick, callAPI]);
|
}, [nodes, links, isLoading, isMounted, onNodeClick, callAPI]);
|
||||||
|
|
||||||
// Show error message
|
|
||||||
if (error) {
|
if (error) {
|
||||||
return (
|
return (
|
||||||
<div className="w-full h-full flex items-center justify-center bg-gradient-to-br from-gray-900 to-gray-800">
|
<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) {
|
if (!isMounted) {
|
||||||
return (
|
return (
|
||||||
<div className="w-full h-full flex items-center justify-center bg-transparent">
|
<div className="w-full h-full flex items-center justify-center bg-transparent">
|
||||||
|
|
@ -519,14 +531,11 @@ export function NetworkGraph({ onNodeClick }: NetworkGraphProps) {
|
||||||
if (isLoading) {
|
if (isLoading) {
|
||||||
return (
|
return (
|
||||||
<div className="w-full h-full relative bg-transparent">
|
<div className="w-full h-full relative bg-transparent">
|
||||||
{/* Skeleton Graph Container */}
|
|
||||||
<div className="w-full h-full flex items-center justify-center relative">
|
<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="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 className="absolute inset-0 rounded-lg bg-gradient-to-r from-gray-500 to-gray-600 animate-pulse"></div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Outer Ring Nodes Skeleton */}
|
|
||||||
{Array.from({ length: 8 }).map((_, i) => {
|
{Array.from({ length: 8 }).map((_, i) => {
|
||||||
const angle = (i * 2 * Math.PI) / 8;
|
const angle = (i * 2 * Math.PI) / 8;
|
||||||
const radius = 120;
|
const radius = 120;
|
||||||
|
|
@ -547,42 +556,28 @@ export function NetworkGraph({ onNodeClick }: NetworkGraphProps) {
|
||||||
<div
|
<div
|
||||||
className="absolute w-16 h-3 bg-gray-600 rounded animate-pulse"
|
className="absolute w-16 h-3 bg-gray-600 rounded animate-pulse"
|
||||||
style={{
|
style={{
|
||||||
left: "50%",
|
transform: `rotate(${(i * 360) / 8}deg) translateX(32px)`,
|
||||||
top: "40px",
|
transformOrigin: "left center",
|
||||||
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`,
|
|
||||||
}}
|
}}
|
||||||
></div>
|
></div>
|
||||||
</div>
|
</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>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="w-full h-full relative bg-transparent overflow-hidden">
|
<div className="w-full h-full">
|
||||||
<svg ref={svgRef} className="w-full h-full" style={{ minHeight: 500 }} />
|
<svg
|
||||||
|
ref={svgRef}
|
||||||
|
className="w-full h-full bg-transparent"
|
||||||
|
style={{ cursor: "grab" }}
|
||||||
|
/>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
export default NetworkGraph;
|
|
||||||
|
export default NetworkGraph;
|
||||||
|
|
@ -1,18 +1,18 @@
|
||||||
"use client"
|
"use client";
|
||||||
|
|
||||||
import * as React from "react"
|
import * as DialogPrimitive from "@radix-ui/react-dialog";
|
||||||
import * as DialogPrimitive from "@radix-ui/react-dialog"
|
import { X } from "lucide-react";
|
||||||
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<
|
const DialogOverlay = React.forwardRef<
|
||||||
React.ElementRef<typeof DialogPrimitive.Overlay>,
|
React.ElementRef<typeof DialogPrimitive.Overlay>,
|
||||||
|
|
@ -26,8 +26,8 @@ const DialogOverlay = React.forwardRef<
|
||||||
)}
|
)}
|
||||||
{...props}
|
{...props}
|
||||||
/>
|
/>
|
||||||
))
|
));
|
||||||
DialogOverlay.displayName = DialogPrimitive.Overlay.displayName
|
DialogOverlay.displayName = DialogPrimitive.Overlay.displayName;
|
||||||
|
|
||||||
const DialogContent = React.forwardRef<
|
const DialogContent = React.forwardRef<
|
||||||
React.ElementRef<typeof DialogPrimitive.Content>,
|
React.ElementRef<typeof DialogPrimitive.Content>,
|
||||||
|
|
@ -38,7 +38,7 @@ const DialogContent = React.forwardRef<
|
||||||
<DialogPrimitive.Content
|
<DialogPrimitive.Content
|
||||||
ref={ref}
|
ref={ref}
|
||||||
className={cn(
|
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
|
className
|
||||||
)}
|
)}
|
||||||
{...props}
|
{...props}
|
||||||
|
|
@ -50,8 +50,8 @@ const DialogContent = React.forwardRef<
|
||||||
</DialogPrimitive.Close>
|
</DialogPrimitive.Close>
|
||||||
</DialogPrimitive.Content>
|
</DialogPrimitive.Content>
|
||||||
</DialogPortal>
|
</DialogPortal>
|
||||||
))
|
));
|
||||||
DialogContent.displayName = DialogPrimitive.Content.displayName
|
DialogContent.displayName = DialogPrimitive.Content.displayName;
|
||||||
|
|
||||||
const DialogHeader = ({
|
const DialogHeader = ({
|
||||||
className,
|
className,
|
||||||
|
|
@ -59,13 +59,13 @@ const DialogHeader = ({
|
||||||
}: React.HTMLAttributes<HTMLDivElement>) => (
|
}: React.HTMLAttributes<HTMLDivElement>) => (
|
||||||
<div
|
<div
|
||||||
className={cn(
|
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
|
className
|
||||||
)}
|
)}
|
||||||
{...props}
|
{...props}
|
||||||
/>
|
/>
|
||||||
)
|
);
|
||||||
DialogHeader.displayName = "DialogHeader"
|
DialogHeader.displayName = "DialogHeader";
|
||||||
|
|
||||||
const DialogFooter = ({
|
const DialogFooter = ({
|
||||||
className,
|
className,
|
||||||
|
|
@ -78,8 +78,8 @@ const DialogFooter = ({
|
||||||
)}
|
)}
|
||||||
{...props}
|
{...props}
|
||||||
/>
|
/>
|
||||||
)
|
);
|
||||||
DialogFooter.displayName = "DialogFooter"
|
DialogFooter.displayName = "DialogFooter";
|
||||||
|
|
||||||
const DialogTitle = React.forwardRef<
|
const DialogTitle = React.forwardRef<
|
||||||
React.ElementRef<typeof DialogPrimitive.Title>,
|
React.ElementRef<typeof DialogPrimitive.Title>,
|
||||||
|
|
@ -93,8 +93,8 @@ const DialogTitle = React.forwardRef<
|
||||||
)}
|
)}
|
||||||
{...props}
|
{...props}
|
||||||
/>
|
/>
|
||||||
))
|
));
|
||||||
DialogTitle.displayName = DialogPrimitive.Title.displayName
|
DialogTitle.displayName = DialogPrimitive.Title.displayName;
|
||||||
|
|
||||||
const DialogDescription = React.forwardRef<
|
const DialogDescription = React.forwardRef<
|
||||||
React.ElementRef<typeof DialogPrimitive.Description>,
|
React.ElementRef<typeof DialogPrimitive.Description>,
|
||||||
|
|
@ -105,18 +105,18 @@ const DialogDescription = React.forwardRef<
|
||||||
className={cn("text-sm text-muted-foreground", className)}
|
className={cn("text-sm text-muted-foreground", className)}
|
||||||
{...props}
|
{...props}
|
||||||
/>
|
/>
|
||||||
))
|
));
|
||||||
DialogDescription.displayName = DialogPrimitive.Description.displayName
|
DialogDescription.displayName = DialogPrimitive.Description.displayName;
|
||||||
|
|
||||||
export {
|
export {
|
||||||
Dialog,
|
Dialog,
|
||||||
DialogPortal,
|
|
||||||
DialogOverlay,
|
|
||||||
DialogClose,
|
DialogClose,
|
||||||
DialogTrigger,
|
|
||||||
DialogContent,
|
DialogContent,
|
||||||
DialogHeader,
|
|
||||||
DialogFooter,
|
|
||||||
DialogTitle,
|
|
||||||
DialogDescription,
|
DialogDescription,
|
||||||
}
|
DialogFooter,
|
||||||
|
DialogHeader,
|
||||||
|
DialogOverlay,
|
||||||
|
DialogPortal,
|
||||||
|
DialogTitle,
|
||||||
|
DialogTrigger,
|
||||||
|
};
|
||||||
|
|
|
||||||
|
|
@ -1,27 +1,27 @@
|
||||||
"use client"
|
"use client";
|
||||||
|
|
||||||
import * as React from "react"
|
import * as DropdownMenuPrimitive from "@radix-ui/react-dropdown-menu";
|
||||||
import * as DropdownMenuPrimitive from "@radix-ui/react-dropdown-menu"
|
import { Check, ChevronDown, Circle } from "lucide-react";
|
||||||
import { Check, ChevronRight, 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<
|
const DropdownMenuSubTrigger = React.forwardRef<
|
||||||
React.ElementRef<typeof DropdownMenuPrimitive.SubTrigger>,
|
React.ElementRef<typeof DropdownMenuPrimitive.SubTrigger>,
|
||||||
React.ComponentPropsWithoutRef<typeof DropdownMenuPrimitive.SubTrigger> & {
|
React.ComponentPropsWithoutRef<typeof DropdownMenuPrimitive.SubTrigger> & {
|
||||||
inset?: boolean
|
inset?: boolean;
|
||||||
}
|
}
|
||||||
>(({ className, inset, children, ...props }, ref) => (
|
>(({ className, inset, children, ...props }, ref) => (
|
||||||
<DropdownMenuPrimitive.SubTrigger
|
<DropdownMenuPrimitive.SubTrigger
|
||||||
|
|
@ -34,11 +34,10 @@ const DropdownMenuSubTrigger = React.forwardRef<
|
||||||
{...props}
|
{...props}
|
||||||
>
|
>
|
||||||
{children}
|
{children}
|
||||||
<ChevronRight className="ml-auto h-4 w-4" />
|
|
||||||
</DropdownMenuPrimitive.SubTrigger>
|
</DropdownMenuPrimitive.SubTrigger>
|
||||||
))
|
));
|
||||||
DropdownMenuSubTrigger.displayName =
|
DropdownMenuSubTrigger.displayName =
|
||||||
DropdownMenuPrimitive.SubTrigger.displayName
|
DropdownMenuPrimitive.SubTrigger.displayName;
|
||||||
|
|
||||||
const DropdownMenuSubContent = React.forwardRef<
|
const DropdownMenuSubContent = React.forwardRef<
|
||||||
React.ElementRef<typeof DropdownMenuPrimitive.SubContent>,
|
React.ElementRef<typeof DropdownMenuPrimitive.SubContent>,
|
||||||
|
|
@ -52,9 +51,9 @@ const DropdownMenuSubContent = React.forwardRef<
|
||||||
)}
|
)}
|
||||||
{...props}
|
{...props}
|
||||||
/>
|
/>
|
||||||
))
|
));
|
||||||
DropdownMenuSubContent.displayName =
|
DropdownMenuSubContent.displayName =
|
||||||
DropdownMenuPrimitive.SubContent.displayName
|
DropdownMenuPrimitive.SubContent.displayName;
|
||||||
|
|
||||||
const DropdownMenuContent = React.forwardRef<
|
const DropdownMenuContent = React.forwardRef<
|
||||||
React.ElementRef<typeof DropdownMenuPrimitive.Content>,
|
React.ElementRef<typeof DropdownMenuPrimitive.Content>,
|
||||||
|
|
@ -65,32 +64,34 @@ const DropdownMenuContent = React.forwardRef<
|
||||||
ref={ref}
|
ref={ref}
|
||||||
sideOffset={sideOffset}
|
sideOffset={sideOffset}
|
||||||
className={cn(
|
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
|
className
|
||||||
)}
|
)}
|
||||||
{...props}
|
{...props}
|
||||||
/>
|
/>
|
||||||
</DropdownMenuPrimitive.Portal>
|
</DropdownMenuPrimitive.Portal>
|
||||||
))
|
));
|
||||||
DropdownMenuContent.displayName = DropdownMenuPrimitive.Content.displayName
|
DropdownMenuContent.displayName = DropdownMenuPrimitive.Content.displayName;
|
||||||
|
|
||||||
const DropdownMenuItem = React.forwardRef<
|
const DropdownMenuItem = React.forwardRef<
|
||||||
React.ElementRef<typeof DropdownMenuPrimitive.Item>,
|
React.ElementRef<typeof DropdownMenuPrimitive.Item>,
|
||||||
React.ComponentPropsWithoutRef<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
|
<DropdownMenuPrimitive.Item
|
||||||
ref={ref}
|
ref={ref}
|
||||||
className={cn(
|
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",
|
inset && "pl-8",
|
||||||
|
selected && "bg-dark-blue text-white",
|
||||||
className
|
className
|
||||||
)}
|
)}
|
||||||
{...props}
|
{...props}
|
||||||
/>
|
/>
|
||||||
))
|
));
|
||||||
DropdownMenuItem.displayName = DropdownMenuPrimitive.Item.displayName
|
DropdownMenuItem.displayName = DropdownMenuPrimitive.Item.displayName;
|
||||||
|
|
||||||
const DropdownMenuCheckboxItem = React.forwardRef<
|
const DropdownMenuCheckboxItem = React.forwardRef<
|
||||||
React.ElementRef<typeof DropdownMenuPrimitive.CheckboxItem>,
|
React.ElementRef<typeof DropdownMenuPrimitive.CheckboxItem>,
|
||||||
|
|
@ -112,9 +113,9 @@ const DropdownMenuCheckboxItem = React.forwardRef<
|
||||||
</span>
|
</span>
|
||||||
{children}
|
{children}
|
||||||
</DropdownMenuPrimitive.CheckboxItem>
|
</DropdownMenuPrimitive.CheckboxItem>
|
||||||
))
|
));
|
||||||
DropdownMenuCheckboxItem.displayName =
|
DropdownMenuCheckboxItem.displayName =
|
||||||
DropdownMenuPrimitive.CheckboxItem.displayName
|
DropdownMenuPrimitive.CheckboxItem.displayName;
|
||||||
|
|
||||||
const DropdownMenuRadioItem = React.forwardRef<
|
const DropdownMenuRadioItem = React.forwardRef<
|
||||||
React.ElementRef<typeof DropdownMenuPrimitive.RadioItem>,
|
React.ElementRef<typeof DropdownMenuPrimitive.RadioItem>,
|
||||||
|
|
@ -135,13 +136,13 @@ const DropdownMenuRadioItem = React.forwardRef<
|
||||||
</span>
|
</span>
|
||||||
{children}
|
{children}
|
||||||
</DropdownMenuPrimitive.RadioItem>
|
</DropdownMenuPrimitive.RadioItem>
|
||||||
))
|
));
|
||||||
DropdownMenuRadioItem.displayName = DropdownMenuPrimitive.RadioItem.displayName
|
DropdownMenuRadioItem.displayName = DropdownMenuPrimitive.RadioItem.displayName;
|
||||||
|
|
||||||
const DropdownMenuLabel = React.forwardRef<
|
const DropdownMenuLabel = React.forwardRef<
|
||||||
React.ElementRef<typeof DropdownMenuPrimitive.Label>,
|
React.ElementRef<typeof DropdownMenuPrimitive.Label>,
|
||||||
React.ComponentPropsWithoutRef<typeof DropdownMenuPrimitive.Label> & {
|
React.ComponentPropsWithoutRef<typeof DropdownMenuPrimitive.Label> & {
|
||||||
inset?: boolean
|
inset?: boolean;
|
||||||
}
|
}
|
||||||
>(({ className, inset, ...props }, ref) => (
|
>(({ className, inset, ...props }, ref) => (
|
||||||
<DropdownMenuPrimitive.Label
|
<DropdownMenuPrimitive.Label
|
||||||
|
|
@ -153,8 +154,8 @@ const DropdownMenuLabel = React.forwardRef<
|
||||||
)}
|
)}
|
||||||
{...props}
|
{...props}
|
||||||
/>
|
/>
|
||||||
))
|
));
|
||||||
DropdownMenuLabel.displayName = DropdownMenuPrimitive.Label.displayName
|
DropdownMenuLabel.displayName = DropdownMenuPrimitive.Label.displayName;
|
||||||
|
|
||||||
const DropdownMenuSeparator = React.forwardRef<
|
const DropdownMenuSeparator = React.forwardRef<
|
||||||
React.ElementRef<typeof DropdownMenuPrimitive.Separator>,
|
React.ElementRef<typeof DropdownMenuPrimitive.Separator>,
|
||||||
|
|
@ -165,8 +166,8 @@ const DropdownMenuSeparator = React.forwardRef<
|
||||||
className={cn("-mx-1 my-1 h-px bg-muted", className)}
|
className={cn("-mx-1 my-1 h-px bg-muted", className)}
|
||||||
{...props}
|
{...props}
|
||||||
/>
|
/>
|
||||||
))
|
));
|
||||||
DropdownMenuSeparator.displayName = DropdownMenuPrimitive.Separator.displayName
|
DropdownMenuSeparator.displayName = DropdownMenuPrimitive.Separator.displayName;
|
||||||
|
|
||||||
const DropdownMenuShortcut = ({
|
const DropdownMenuShortcut = ({
|
||||||
className,
|
className,
|
||||||
|
|
@ -177,24 +178,43 @@ const DropdownMenuShortcut = ({
|
||||||
className={cn("ml-auto text-xs tracking-widest opacity-60", className)}
|
className={cn("ml-auto text-xs tracking-widest opacity-60", className)}
|
||||||
{...props}
|
{...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 {
|
export {
|
||||||
DropdownMenu,
|
DropdownMenu,
|
||||||
DropdownMenuTrigger,
|
DropdownMenuButton,
|
||||||
DropdownMenuContent,
|
|
||||||
DropdownMenuItem,
|
|
||||||
DropdownMenuCheckboxItem,
|
DropdownMenuCheckboxItem,
|
||||||
DropdownMenuRadioItem,
|
DropdownMenuContent,
|
||||||
|
DropdownMenuGroup,
|
||||||
|
DropdownMenuItem,
|
||||||
DropdownMenuLabel,
|
DropdownMenuLabel,
|
||||||
|
DropdownMenuPortal,
|
||||||
|
DropdownMenuRadioGroup,
|
||||||
|
DropdownMenuRadioItem,
|
||||||
DropdownMenuSeparator,
|
DropdownMenuSeparator,
|
||||||
DropdownMenuShortcut,
|
DropdownMenuShortcut,
|
||||||
DropdownMenuGroup,
|
|
||||||
DropdownMenuPortal,
|
|
||||||
DropdownMenuSub,
|
DropdownMenuSub,
|
||||||
DropdownMenuSubContent,
|
DropdownMenuSubContent,
|
||||||
DropdownMenuSubTrigger,
|
DropdownMenuSubTrigger,
|
||||||
DropdownMenuRadioGroup,
|
DropdownMenuTrigger,
|
||||||
}
|
};
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue
Block a user