Compare commits
1 Commits
main
...
hotfix-scr
| Author | SHA1 | Date | |
|---|---|---|---|
| 31fa601ab2 |
|
|
@ -160,9 +160,9 @@ This document describes the exact implementation of the login page based on the
|
||||||
onChange={(e) => setRememberMe(e.target.checked)}
|
onChange={(e) => setRememberMe(e.target.checked)}
|
||||||
className="w-4 h-4 text-[#4FD1C7] bg-white border-gray-300 rounded focus:ring-[#4FD1C7] focus:ring-2 accent-[#4FD1C7]"
|
className="w-4 h-4 text-[#4FD1C7] bg-white border-gray-300 rounded focus:ring-[#4FD1C7] focus:ring-2 accent-[#4FD1C7]"
|
||||||
/>
|
/>
|
||||||
// <Label htmlFor="remember" className="text-white text-sm font-persian cursor-pointer">
|
<Label htmlFor="remember" className="text-white text-sm font-persian cursor-pointer">
|
||||||
// همیشه متصل بمانم
|
همیشه متصل بمانم
|
||||||
// </Label>
|
</Label>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Submit Button */}
|
{/* Submit Button */}
|
||||||
|
|
|
||||||
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,380 +56,378 @@ 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-dark-blue: var(--dark-blue);
|
--color-popover-foreground: var(--popover-foreground);
|
||||||
--color-popover-foreground: var(--popover-foreground);
|
--color-primary: var(--primary);
|
||||||
--color-primary: var(--primary);
|
--color-primary-foreground: var(--primary-foreground);
|
||||||
--color-primary-foreground: var(--primary-foreground);
|
--color-secondary: var(--secondary);
|
||||||
--color-secondary: var(--secondary);
|
--color-secondary-foreground: var(--secondary-foreground);
|
||||||
--color-secondary-foreground: var(--secondary-foreground);
|
--color-muted: var(--muted);
|
||||||
--color-muted: var(--muted);
|
--color-muted-foreground: var(--muted-foreground);
|
||||||
--color-muted-foreground: var(--muted-foreground);
|
--color-accent: var(--accent);
|
||||||
--color-accent: var(--accent);
|
--color-accent-foreground: var(--accent-foreground);
|
||||||
--color-accent-foreground: var(--accent-foreground);
|
--color-destructive: var(--destructive);
|
||||||
--color-destructive: var(--destructive);
|
--color-destructive-foreground: var(--destructive-foreground);
|
||||||
--color-destructive-foreground: var(--destructive-foreground);
|
--color-border: var(--border);
|
||||||
--color-border: var(--border);
|
--color-input: var(--input);
|
||||||
--color-input: var(--input);
|
--color-ring: var(--ring);
|
||||||
--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 {
|
||||||
|
|
@ -445,50 +443,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;
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -176,7 +176,7 @@ export function LoginForm({ onSuccess }: LoginFormProps) {
|
||||||
/>
|
/>
|
||||||
|
|
||||||
{/* Remember Me Checkbox */}
|
{/* Remember Me Checkbox */}
|
||||||
{/* <div className="flex justify-end">
|
<div className="flex justify-end">
|
||||||
<CheckboxField
|
<CheckboxField
|
||||||
id="remember"
|
id="remember"
|
||||||
label="همیشه متصل بمان"
|
label="همیشه متصل بمان"
|
||||||
|
|
@ -185,7 +185,7 @@ export function LoginForm({ onSuccess }: LoginFormProps) {
|
||||||
disabled={isLoading}
|
disabled={isLoading}
|
||||||
size="md"
|
size="md"
|
||||||
/>
|
/>
|
||||||
</div> */}
|
</div>
|
||||||
|
|
||||||
{/* Login Button */}
|
{/* Login Button */}
|
||||||
<Button
|
<Button
|
||||||
|
|
@ -212,9 +212,7 @@ export function LoginForm({ onSuccess }: LoginFormProps) {
|
||||||
{/* Right Side - Branding */}
|
{/* Right Side - Branding */}
|
||||||
<LoginSidebar>
|
<LoginSidebar>
|
||||||
<LoginBranding
|
<LoginBranding
|
||||||
brandName="پتروشیمی آپادانا"
|
brandName="پتروشیمی بندر امام"
|
||||||
// brandName="پتروشیمی نوری"
|
|
||||||
// brandName="پتروشیمی بندر امام"
|
|
||||||
engSub="Inception by Fara"
|
engSub="Inception by Fara"
|
||||||
companyName="توسعهیافته توسط شرکت رهپویان دانش و فناوری فرا"
|
companyName="توسعهیافته توسط شرکت رهپویان دانش و فناوری فرا"
|
||||||
logo={<img src="/brand2.svg"/>}
|
logo={<img src="/brand2.svg"/>}
|
||||||
|
|
|
||||||
|
|
@ -1,7 +1,6 @@
|
||||||
import React from "react";
|
import React from "react";
|
||||||
import { cn } from "~/lib/utils";
|
import { cn } from "~/lib/utils";
|
||||||
|
|
||||||
|
|
||||||
interface LoginLayoutProps {
|
interface LoginLayoutProps {
|
||||||
children: React.ReactNode;
|
children: React.ReactNode;
|
||||||
className?: string;
|
className?: string;
|
||||||
|
|
@ -107,25 +106,14 @@ export function LoginBranding({
|
||||||
}: LoginBrandingProps) {
|
}: LoginBrandingProps) {
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<div className="flex justify-end">
|
{/* Top Logo */}
|
||||||
<div className="text-slate-800 font-persian">
|
<div className="flex justify-end">
|
||||||
<div className="text-lg font-bold leading-tight">
|
<div className="text-slate-800 font-persian">
|
||||||
<img
|
<div className="text-lg font-bold leading-tight">
|
||||||
src="/brand.svg?v=1"
|
<img src="/brand.svg" />
|
||||||
alt="Brand Logo"
|
</div>
|
||||||
className="w-auto h-16" // اضافه کردن سایز مشخص
|
</div>
|
||||||
onError={(e) => {
|
</div>
|
||||||
e.target.style.display = 'none';
|
|
||||||
console.log('Image failed to load');
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
{/* Bottom Section */}
|
{/* Bottom Section */}
|
||||||
<div className="flex flex-col gap-2 mb-4 items-end justify-end">
|
<div className="flex flex-col gap-2 mb-4 items-end justify-end">
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,3 @@
|
||||||
//این فایل مخصوص
|
|
||||||
//شماتیک آپادانا
|
|
||||||
|
|
||||||
import React from "react";
|
import React from "react";
|
||||||
import { formatNumber } from "~/lib/utils";
|
import { formatNumber } from "~/lib/utils";
|
||||||
|
|
||||||
|
|
@ -11,10 +8,10 @@ export type CompanyInfo = {
|
||||||
costReduction: number;
|
costReduction: number;
|
||||||
revenue?: number;
|
revenue?: number;
|
||||||
capacity?: number;
|
capacity?: number;
|
||||||
costI: number;
|
costI : number,
|
||||||
capacityI: number;
|
capacityI : number,
|
||||||
revenueI: number;
|
revenueI : number,
|
||||||
cost: number | string;
|
cost : number | string,
|
||||||
};
|
};
|
||||||
|
|
||||||
export type D3ImageInfoProps = {
|
export type D3ImageInfoProps = {
|
||||||
|
|
@ -23,11 +20,9 @@ export type D3ImageInfoProps = {
|
||||||
height?: number;
|
height?: number;
|
||||||
};
|
};
|
||||||
|
|
||||||
const InfoBox = ({ company, style }: { company: CompanyInfo; style: any }) => {
|
const InfoBox = ({ company, style }: { company: CompanyInfo; style :any }) => {
|
||||||
// const hideCapacity = company.name === "واحد 300"; // اگر واحد 300 بود ظرفیت مخفی شود
|
|
||||||
const hideCapacity = false;
|
|
||||||
return (
|
return (
|
||||||
<div className={`info-box`} style={style}>
|
<div className={`info-box`} style={style}>
|
||||||
<div className="info-box-content">
|
<div className="info-box-content">
|
||||||
<div className="info-row">
|
<div className="info-row">
|
||||||
<div className="info-label">درآمد:</div>
|
<div className="info-label">درآمد:</div>
|
||||||
|
|
@ -36,78 +31,58 @@ const InfoBox = ({ company, style }: { company: CompanyInfo; style: any }) => {
|
||||||
</div>
|
</div>
|
||||||
<div className="info-row">
|
<div className="info-row">
|
||||||
<div className="info-label">هزینه:</div>
|
<div className="info-label">هزینه:</div>
|
||||||
{hideCapacity ? (
|
<div className="info-value cost text-[12px]">{formatNumber(company?.cost || 0)}</div>
|
||||||
<div className="info-value cost2 text-[12px]">{formatNumber(company?.cost || 0)}</div>
|
|
||||||
) : (
|
|
||||||
<div className="info-value cost text-[12px]">{formatNumber(company?.cost || 0)}</div>
|
|
||||||
)}
|
|
||||||
<div className="info-unit">میلیون ریال</div>
|
<div className="info-unit">میلیون ریال</div>
|
||||||
</div>
|
</div>
|
||||||
{!hideCapacity && (
|
<div className="info-row">
|
||||||
<div className="info-row">
|
<div className="info-label">ظرفیت:</div>
|
||||||
<div className="info-label">ظرفیت:</div>
|
<div className="info-value capacity text-[12px]">{formatNumber(company?.capacity || 0)}</div>
|
||||||
<div className="info-value capacity text-[12px]">{formatNumber(company?.capacity || 0)}</div>
|
<div className="info-unit">تن در سال</div>
|
||||||
<div className="info-unit">تن در سال</div>
|
</div>
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
export function D3ImageInfo({ companies }: D3ImageInfoProps) {
|
export function D3ImageInfo({ companies }: D3ImageInfoProps) {
|
||||||
// واحدهای جدید - 4 واحد
|
// Ensure we have exactly 6 companies
|
||||||
const sample = [
|
const displayCompanies = companies;
|
||||||
{ id: "واحد 100", name: "واحد 100", imageUrl: "/abniro.png" },
|
|
||||||
{ id: "واحد 200", name: "واحد 200", imageUrl: "/besparan.png" },
|
|
||||||
{ id: "واحد 300", name: "واحد 300", imageUrl: "/khwarazmi.png" },
|
|
||||||
{ id: "واحد 400", name: "واحد 400", imageUrl: "/faravash1.png" }
|
|
||||||
];
|
|
||||||
|
|
||||||
|
// Positions inside a 5x4 grid (col, row)
|
||||||
|
// Layout keeps same visual logic: left/middle/right on two bands with spacing grid around
|
||||||
const merged = sample.map(company => {
|
|
||||||
const found = companies.find(item => item.id === company.id);
|
|
||||||
return found
|
|
||||||
? found
|
|
||||||
: { ...company, cost: 0, capacity: 0, revenue: 0, costReduction: 0, costI: 0, capacityI: 0, revenueI: 0 };
|
|
||||||
});
|
|
||||||
|
|
||||||
const displayCompanies = merged;
|
|
||||||
console.log(displayCompanies);
|
|
||||||
|
|
||||||
// موقعیتهای جدید برای چیدمان لوزی شکل (3 ردیف - 1-2-1)
|
|
||||||
// گرید 5x4 نگه داشته شده اما موقعیتها تغییر کرده
|
|
||||||
const gridPositions = [
|
const gridPositions = [
|
||||||
{ col: 2, row: 1, colI: 1, rowI: 1, name: "واحد 100" }, // ردیف اول - ستون اول
|
{ col: 2, row: 2 , colI : 1 , rowI : 2 , name : "بسپاران"}, // left - top band
|
||||||
{ col: 4, row: 1, colI: 5, rowI: 1, name: "واحد 200" }, // ردیف اول - ستون دوم
|
{ col: 3, row: 2 , colI : 3 , rowI : 1 , name : "خوارزمی"}, // middle top (image sits in row 2, info box goes to row 1)
|
||||||
{ col: 2, row: 3, colI: 1, rowI: 3, name: "واحد 300" }, // ردیف دوم - ستون اول
|
{ col: 4, row: 2 ,colI : 5 , rowI : 2 , name : "فراورش 1"}, // right - top band
|
||||||
{ col: 4, row: 3, colI: 5, rowI: 3, name: "واحد 400" }, // ردیف دوم - ستون دوم
|
{ col: 2, row: 3 , colI : 1 , rowI : 3 , name : "کیمیا"}, // left - bottom band
|
||||||
|
{ col: 3, row: 3 , colI : 3, rowI : 4 , name : "آب نیرو"}, // middle bottom (image sits in row 3, info box goes to row 4)
|
||||||
|
{ col: 4, row: 3 , colI : 5 , rowI : 3 , name : "فراورش 2"}, // right - bottom band
|
||||||
];
|
];
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="w-full h-[500px] rounded-xl">
|
<div className="w-full h-[500px] rounded-xl">
|
||||||
<div dir="ltr" className="company-grid-container">
|
<div dir="ltr" className="company-grid-container">
|
||||||
{displayCompanies.map((company, index) => {
|
{displayCompanies.map((company, index) => {
|
||||||
const gp = gridPositions.find(v => v.name === company.name);
|
const gp = gridPositions.find(v => v.name === company.name) ;
|
||||||
return (
|
return (
|
||||||
<React.Fragment key={company.id}>
|
<>
|
||||||
<div
|
<div
|
||||||
className={`company-item`}
|
key={company.id}
|
||||||
style={{ gridColumn: gp?.col, gridRow: gp?.row }}
|
className={`company-item`}
|
||||||
>
|
style={{ gridColumn: gp.col, gridRow: gp.row }}
|
||||||
<div className="company-image-container">
|
>
|
||||||
<img
|
<div className="company-image-containe">
|
||||||
src={company.imageUrl}
|
<img
|
||||||
alt={company.name}
|
src={company.imageUrl}
|
||||||
className="company-image"
|
alt={company.name}
|
||||||
/>
|
className="company-image"
|
||||||
</div>
|
/>
|
||||||
{company.name}
|
|
||||||
</div>
|
</div>
|
||||||
<InfoBox company={company} style={{ gridColumn: gp?.colI, gridRow: gp?.rowI }} />
|
|
||||||
</React.Fragment>
|
{company.name}
|
||||||
);
|
</div>
|
||||||
|
<InfoBox company={company} key={index +10} style={{ gridColumn: gp?.colI , gridRow: gp?.rowI }} />
|
||||||
|
</>);
|
||||||
})}
|
})}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|
@ -139,20 +114,20 @@ export function D3ImageInfo({ companies }: D3ImageInfoProps) {
|
||||||
|
|
||||||
.company-image {
|
.company-image {
|
||||||
object-fit: contain;
|
object-fit: contain;
|
||||||
height: 100px;
|
height : 100px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.info-box {
|
.info-box {
|
||||||
border: 1px solid #3F415A;
|
border: 1px solid #3F415A;
|
||||||
border-radius: 10px;
|
border-radius: 10px;
|
||||||
height: max-content;
|
height: max-content;
|
||||||
align-self: center;
|
align-self : center;
|
||||||
justify-self: center;
|
justify-self : center;
|
||||||
padding: .2rem 1.2rem;
|
padding : .2rem 1.2rem;
|
||||||
min-width: 8rem;
|
|
||||||
background-color: transparent;
|
background-color: transparent;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
.info-box-content {
|
.info-box-content {
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
|
|
@ -160,20 +135,16 @@ export function D3ImageInfo({ companies }: D3ImageInfoProps) {
|
||||||
}
|
}
|
||||||
|
|
||||||
.info-row {
|
.info-row {
|
||||||
position: relative;
|
position : relative;
|
||||||
margin: .1rem 0;
|
margin: .1rem 0;
|
||||||
display: flex;
|
display: flex;
|
||||||
gap: .5rem;
|
gap : .5rem;
|
||||||
justify-content: space-between;
|
justify-content : space-between;
|
||||||
direction: rtl;
|
direction: rtl;
|
||||||
}
|
|
||||||
|
|
||||||
.info-row:has(.info-value.revenue) {
|
&:has(.info-value.revenue) {border-bottom: 1px solid #3AEA83;}
|
||||||
border-bottom: 1px solid #3AEA83;
|
&:has(.info-value.cost) {border-bottom: 1px solid #F76276;}
|
||||||
}
|
|
||||||
|
|
||||||
.info-row:has(.info-value.cost) {
|
|
||||||
border-bottom: 1px solid #F76276;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.info-label {
|
.info-label {
|
||||||
|
|
@ -181,7 +152,7 @@ export function D3ImageInfo({ companies }: D3ImageInfoProps) {
|
||||||
font-size: 11px;
|
font-size: 11px;
|
||||||
font-weight: 300;
|
font-weight: 300;
|
||||||
text-align: right;
|
text-align: right;
|
||||||
margin: auto 0;
|
margin : auto 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
.info-value {
|
.info-value {
|
||||||
|
|
@ -189,12 +160,11 @@ export function D3ImageInfo({ companies }: D3ImageInfoProps) {
|
||||||
font-size: 14px;
|
font-size: 14px;
|
||||||
font-weight: 500;
|
font-weight: 500;
|
||||||
text-align: right;
|
text-align: right;
|
||||||
margin-bottom: .5rem;
|
margin-bottom : .5rem;
|
||||||
}
|
}
|
||||||
|
|
||||||
.info-value.revenue { color: #fff; }
|
.info-value.revenue { color: #fff;}
|
||||||
.info-value.cost { color: #fff; }
|
.info-value.cost { color: #fff; }
|
||||||
.info-value.cost2 { color: #fff; }
|
|
||||||
.info-value.capacity { color: #fff; }
|
.info-value.capacity { color: #fff; }
|
||||||
|
|
||||||
.info-unit {
|
.info-unit {
|
||||||
|
|
@ -208,4 +178,4 @@ export function D3ImageInfo({ companies }: D3ImageInfoProps) {
|
||||||
`}</style>
|
`}</style>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,213 +0,0 @@
|
||||||
//این فایل مخصوص
|
|
||||||
//شماتیک بندر امام
|
|
||||||
import React from "react";
|
|
||||||
import { formatNumber } from "~/lib/utils";
|
|
||||||
|
|
||||||
export type CompanyInfo = {
|
|
||||||
id: string;
|
|
||||||
imageUrl: string;
|
|
||||||
name: string;
|
|
||||||
costReduction: number;
|
|
||||||
revenue?: number;
|
|
||||||
capacity?: number;
|
|
||||||
costI : number,
|
|
||||||
capacityI : number,
|
|
||||||
revenueI : number,
|
|
||||||
cost : number | string,
|
|
||||||
};
|
|
||||||
|
|
||||||
export type D3ImageInfoProps = {
|
|
||||||
companies: CompanyInfo[];
|
|
||||||
width?: number;
|
|
||||||
height?: number;
|
|
||||||
};
|
|
||||||
|
|
||||||
const InfoBox = ({ company, style }: { company: CompanyInfo; style :any }) => {
|
|
||||||
const hideCapacity = company.name === "خوارزمی"; // اگر خوارزمی بود ظرفیت مخفی شود
|
|
||||||
return (
|
|
||||||
<div className={`info-box`} style={style}>
|
|
||||||
<div className="info-box-content">
|
|
||||||
<div className="info-row">
|
|
||||||
<div className="info-label">درآمد:</div>
|
|
||||||
<div className="info-value revenue text-[12px]">{formatNumber(company?.revenue || 0)}</div>
|
|
||||||
<div className="info-unit">میلیون ریال</div>
|
|
||||||
</div>
|
|
||||||
<div className="info-row">
|
|
||||||
<div className="info-label">هزینه:</div>
|
|
||||||
{
|
|
||||||
(hideCapacity ?
|
|
||||||
|
|
||||||
<div className="info-value cost2 text-[12px]">{formatNumber(company?.cost || 0)}</div>
|
|
||||||
:
|
|
||||||
<div className="info-value cost text-[12px]">{formatNumber(company?.cost || 0)}</div>
|
|
||||||
)
|
|
||||||
|
|
||||||
}
|
|
||||||
<div className="info-unit">میلیون ریال</div>
|
|
||||||
</div>
|
|
||||||
{!hideCapacity && (
|
|
||||||
<div className="info-row">
|
|
||||||
<div className="info-label">ظرفیت:</div>
|
|
||||||
<div className="info-value capacity text-[12px]">{formatNumber(company?.capacity || 0)}</div>
|
|
||||||
<div className="info-unit">تن در سال</div>
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
export function D3ImageInfo({ companies }: D3ImageInfoProps) {
|
|
||||||
// Ensure we have exactly 6 companies
|
|
||||||
|
|
||||||
const sample = [
|
|
||||||
{ id: "آب نیرو", name: "آب نیرو", imageUrl: "/abniro.png" },
|
|
||||||
{ id: "بسپاران", name: "بسپاران", imageUrl: "/besparan.png" },
|
|
||||||
{ id: "خوارزمی", name: "خوارزمی", imageUrl: "/khwarazmi.png" },
|
|
||||||
{ id: "فراورش 1", name: "فراورش 1", imageUrl: "/faravash1.png" },
|
|
||||||
{ id: "فراورش 2", name: "فراورش 2", imageUrl: "/faravash2.png" },
|
|
||||||
{ id: "کیمیا", name: "کیمیا", imageUrl: "/kimia.png" }
|
|
||||||
];
|
|
||||||
const merged = sample.map(company => {
|
|
||||||
const found = companies.find(item => item.id == company.id);
|
|
||||||
return found
|
|
||||||
? found
|
|
||||||
: { ...company, cost: 0, capacity: 0, revenue: 0 };
|
|
||||||
});
|
|
||||||
|
|
||||||
const displayCompanies = merged;
|
|
||||||
console.log(displayCompanies)
|
|
||||||
|
|
||||||
// Positions inside a 5x4 grid (col, row)
|
|
||||||
// Layout keeps same visual logic: left/middle/right on two bands with spacing grid around
|
|
||||||
const gridPositions = [
|
|
||||||
{ col: 2, row: 2 , colI : 1 , rowI : 2 , name : "بسپاران"}, // left - top band
|
|
||||||
{ col: 3, row: 2 , colI : 3 , rowI : 1 , name : "خوارزمی"}, // middle top (image sits in row 2, info box goes to row 1)
|
|
||||||
{ col: 4, row: 2 ,colI : 5 , rowI : 2 , name : "فراورش 1"}, // right - top band
|
|
||||||
{ col: 2, row: 3 , colI : 1 , rowI : 3 , name : "کیمیا"}, // left - bottom band
|
|
||||||
{ col: 3, row: 3 , colI : 3, rowI : 4 , name : "آب نیرو"}, // middle bottom (image sits in row 3, info box goes to row 4)
|
|
||||||
{ col: 4, row: 3 , colI : 5 , rowI : 3 , name : "فراورش 2"}, // right - bottom band
|
|
||||||
];
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div className="w-full h-[500px] rounded-xl">
|
|
||||||
<div dir="ltr" className="company-grid-container">
|
|
||||||
{displayCompanies.map((company, index) => {
|
|
||||||
const gp = gridPositions.find(v => v.name === company.name) ;
|
|
||||||
return (
|
|
||||||
<>
|
|
||||||
<div
|
|
||||||
key={company.id}
|
|
||||||
className={`company-item`}
|
|
||||||
style={{ gridColumn: gp.col, gridRow: gp.row }}
|
|
||||||
>
|
|
||||||
<div className="company-image-containe">
|
|
||||||
<img
|
|
||||||
src={company.imageUrl}
|
|
||||||
alt={company.name}
|
|
||||||
className="company-image"
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{company.name}
|
|
||||||
</div>
|
|
||||||
<InfoBox company={company} key={index +10} style={{ gridColumn: gp?.colI , gridRow: gp?.rowI }} />
|
|
||||||
</>);
|
|
||||||
})}
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<style jsx>{`
|
|
||||||
.company-grid-container {
|
|
||||||
display: grid;
|
|
||||||
grid-template-columns: repeat(5, 1fr);
|
|
||||||
grid-template-rows: repeat(4, 1fr);
|
|
||||||
gap: 5px;
|
|
||||||
width: 100%;
|
|
||||||
height: 500px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.company-item {
|
|
||||||
border-radius: 8px;
|
|
||||||
display: flex;
|
|
||||||
flex-direction: column;
|
|
||||||
justify-content: center;
|
|
||||||
align-items: center;
|
|
||||||
}
|
|
||||||
|
|
||||||
.company-image-container {
|
|
||||||
display: flex;
|
|
||||||
justify-content: center;
|
|
||||||
align-items: center;
|
|
||||||
width: 100%;
|
|
||||||
height: 100%;
|
|
||||||
}
|
|
||||||
|
|
||||||
.company-image {
|
|
||||||
object-fit: contain;
|
|
||||||
height : 100px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.info-box {
|
|
||||||
border: 1px solid #3F415A;
|
|
||||||
border-radius: 10px;
|
|
||||||
height: max-content;
|
|
||||||
align-self : center;
|
|
||||||
justify-self : center;
|
|
||||||
padding : .2rem 1.2rem;
|
|
||||||
min-width : 8rem;
|
|
||||||
background-color: transparent;
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
.info-box-content {
|
|
||||||
display: flex;
|
|
||||||
flex-direction: column;
|
|
||||||
justify-content: center;
|
|
||||||
}
|
|
||||||
|
|
||||||
.info-row {
|
|
||||||
position : relative;
|
|
||||||
margin: .1rem 0;
|
|
||||||
display: flex;
|
|
||||||
gap : .5rem;
|
|
||||||
justify-content : space-between;
|
|
||||||
direction: rtl;
|
|
||||||
|
|
||||||
&:has(.info-value.revenue) {border-bottom: 1px solid #3AEA83;}
|
|
||||||
&:has(.info-value.cost) {border-bottom: 1px solid #F76276;}
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
.info-label {
|
|
||||||
color: #FFFFFF;
|
|
||||||
font-size: 11px;
|
|
||||||
font-weight: 300;
|
|
||||||
text-align: right;
|
|
||||||
margin : auto 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
.info-value {
|
|
||||||
color: #34D399;
|
|
||||||
font-size: 14px;
|
|
||||||
font-weight: 500;
|
|
||||||
text-align: right;
|
|
||||||
margin-bottom : .5rem;
|
|
||||||
}
|
|
||||||
|
|
||||||
.info-value.revenue { color: #fff;}
|
|
||||||
.info-value.cost { color: #fff; }
|
|
||||||
.info-value.cost2 { color: #fff; }
|
|
||||||
.info-value.capacity { color: #fff; }
|
|
||||||
|
|
||||||
.info-unit {
|
|
||||||
position: absolute;
|
|
||||||
left: 0;
|
|
||||||
bottom: 2px;
|
|
||||||
color: #ACACAC;
|
|
||||||
font-size: 6px;
|
|
||||||
font-weight: 400;
|
|
||||||
}
|
|
||||||
`}</style>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
@ -1,211 +0,0 @@
|
||||||
//این فایل مخصوص
|
|
||||||
//شماتیک نوری
|
|
||||||
|
|
||||||
import React from "react";
|
|
||||||
import { formatNumber } from "~/lib/utils";
|
|
||||||
|
|
||||||
export type CompanyInfo = {
|
|
||||||
id: string;
|
|
||||||
imageUrl: string;
|
|
||||||
name: string;
|
|
||||||
costReduction: number;
|
|
||||||
revenue?: number;
|
|
||||||
capacity?: number;
|
|
||||||
costI: number;
|
|
||||||
capacityI: number;
|
|
||||||
revenueI: number;
|
|
||||||
cost: number | string;
|
|
||||||
};
|
|
||||||
|
|
||||||
export type D3ImageInfoProps = {
|
|
||||||
companies: CompanyInfo[];
|
|
||||||
width?: number;
|
|
||||||
height?: number;
|
|
||||||
};
|
|
||||||
|
|
||||||
const InfoBox = ({ company, style }: { company: CompanyInfo; style: any }) => {
|
|
||||||
// const hideCapacity = company.name === "واحد 300"; // اگر واحد 300 بود ظرفیت مخفی شود
|
|
||||||
const hideCapacity = false;
|
|
||||||
return (
|
|
||||||
<div className={`info-box`} style={style}>
|
|
||||||
<div className="info-box-content">
|
|
||||||
<div className="info-row">
|
|
||||||
<div className="info-label">درآمد:</div>
|
|
||||||
<div className="info-value revenue text-[12px]">{formatNumber(company?.revenue || 0)}</div>
|
|
||||||
<div className="info-unit">میلیون ریال</div>
|
|
||||||
</div>
|
|
||||||
<div className="info-row">
|
|
||||||
<div className="info-label">هزینه:</div>
|
|
||||||
{hideCapacity ? (
|
|
||||||
<div className="info-value cost2 text-[12px]">{formatNumber(company?.cost || 0)}</div>
|
|
||||||
) : (
|
|
||||||
<div className="info-value cost text-[12px]">{formatNumber(company?.cost || 0)}</div>
|
|
||||||
)}
|
|
||||||
<div className="info-unit">میلیون ریال</div>
|
|
||||||
</div>
|
|
||||||
{!hideCapacity && (
|
|
||||||
<div className="info-row">
|
|
||||||
<div className="info-label">ظرفیت:</div>
|
|
||||||
<div className="info-value capacity text-[12px]">{formatNumber(company?.capacity || 0)}</div>
|
|
||||||
<div className="info-unit">تن در سال</div>
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
export function D3ImageInfo({ companies }: D3ImageInfoProps) {
|
|
||||||
// واحدهای جدید - 4 واحد
|
|
||||||
const sample = [
|
|
||||||
{ id: "واحد 100", name: "واحد 100", imageUrl: "/abniro.png" },
|
|
||||||
{ id: "واحد 200", name: "واحد 200", imageUrl: "/besparan.png" },
|
|
||||||
{ id: "واحد 300", name: "واحد 300", imageUrl: "/khwarazmi.png" },
|
|
||||||
{ id: "واحد 400", name: "واحد 400", imageUrl: "/faravash1.png" }
|
|
||||||
];
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
const merged = sample.map(company => {
|
|
||||||
const found = companies.find(item => item.id === company.id);
|
|
||||||
return found
|
|
||||||
? found
|
|
||||||
: { ...company, cost: 0, capacity: 0, revenue: 0, costReduction: 0, costI: 0, capacityI: 0, revenueI: 0 };
|
|
||||||
});
|
|
||||||
|
|
||||||
const displayCompanies = merged;
|
|
||||||
console.log(displayCompanies);
|
|
||||||
|
|
||||||
// موقعیتهای جدید برای چیدمان لوزی شکل (3 ردیف - 1-2-1)
|
|
||||||
// گرید 5x4 نگه داشته شده اما موقعیتها تغییر کرده
|
|
||||||
const gridPositions = [
|
|
||||||
{ col: 2, row: 1, colI: 1, rowI: 1, name: "واحد 100" }, // ردیف اول - ستون اول
|
|
||||||
{ col: 4, row: 1, colI: 5, rowI: 1, name: "واحد 200" }, // ردیف اول - ستون دوم
|
|
||||||
{ col: 2, row: 3, colI: 1, rowI: 3, name: "واحد 300" }, // ردیف دوم - ستون اول
|
|
||||||
{ col: 4, row: 3, colI: 5, rowI: 3, name: "واحد 400" }, // ردیف دوم - ستون دوم
|
|
||||||
];
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div className="w-full h-[500px] rounded-xl">
|
|
||||||
<div dir="ltr" className="company-grid-container">
|
|
||||||
{displayCompanies.map((company, index) => {
|
|
||||||
const gp = gridPositions.find(v => v.name === company.name);
|
|
||||||
return (
|
|
||||||
<React.Fragment key={company.id}>
|
|
||||||
<div
|
|
||||||
className={`company-item`}
|
|
||||||
style={{ gridColumn: gp?.col, gridRow: gp?.row }}
|
|
||||||
>
|
|
||||||
<div className="company-image-container">
|
|
||||||
<img
|
|
||||||
src={company.imageUrl}
|
|
||||||
alt={company.name}
|
|
||||||
className="company-image"
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
{company.name}
|
|
||||||
</div>
|
|
||||||
<InfoBox company={company} style={{ gridColumn: gp?.colI, gridRow: gp?.rowI }} />
|
|
||||||
</React.Fragment>
|
|
||||||
);
|
|
||||||
})}
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<style jsx>{`
|
|
||||||
.company-grid-container {
|
|
||||||
display: grid;
|
|
||||||
grid-template-columns: repeat(5, 1fr);
|
|
||||||
grid-template-rows: repeat(4, 1fr);
|
|
||||||
gap: 5px;
|
|
||||||
width: 100%;
|
|
||||||
height: 500px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.company-item {
|
|
||||||
border-radius: 8px;
|
|
||||||
display: flex;
|
|
||||||
flex-direction: column;
|
|
||||||
justify-content: center;
|
|
||||||
align-items: center;
|
|
||||||
}
|
|
||||||
|
|
||||||
.company-image-container {
|
|
||||||
display: flex;
|
|
||||||
justify-content: center;
|
|
||||||
align-items: center;
|
|
||||||
width: 100%;
|
|
||||||
height: 100%;
|
|
||||||
}
|
|
||||||
|
|
||||||
.company-image {
|
|
||||||
object-fit: contain;
|
|
||||||
height: 100px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.info-box {
|
|
||||||
border: 1px solid #3F415A;
|
|
||||||
border-radius: 10px;
|
|
||||||
height: max-content;
|
|
||||||
align-self: center;
|
|
||||||
justify-self: center;
|
|
||||||
padding: .2rem 1.2rem;
|
|
||||||
min-width: 8rem;
|
|
||||||
background-color: transparent;
|
|
||||||
}
|
|
||||||
|
|
||||||
.info-box-content {
|
|
||||||
display: flex;
|
|
||||||
flex-direction: column;
|
|
||||||
justify-content: center;
|
|
||||||
}
|
|
||||||
|
|
||||||
.info-row {
|
|
||||||
position: relative;
|
|
||||||
margin: .1rem 0;
|
|
||||||
display: flex;
|
|
||||||
gap: .5rem;
|
|
||||||
justify-content: space-between;
|
|
||||||
direction: rtl;
|
|
||||||
}
|
|
||||||
|
|
||||||
.info-row:has(.info-value.revenue) {
|
|
||||||
border-bottom: 1px solid #3AEA83;
|
|
||||||
}
|
|
||||||
|
|
||||||
.info-row:has(.info-value.cost) {
|
|
||||||
border-bottom: 1px solid #F76276;
|
|
||||||
}
|
|
||||||
|
|
||||||
.info-label {
|
|
||||||
color: #FFFFFF;
|
|
||||||
font-size: 11px;
|
|
||||||
font-weight: 300;
|
|
||||||
text-align: right;
|
|
||||||
margin: auto 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
.info-value {
|
|
||||||
color: #34D399;
|
|
||||||
font-size: 14px;
|
|
||||||
font-weight: 500;
|
|
||||||
text-align: right;
|
|
||||||
margin-bottom: .5rem;
|
|
||||||
}
|
|
||||||
|
|
||||||
.info-value.revenue { color: #fff; }
|
|
||||||
.info-value.cost { color: #fff; }
|
|
||||||
.info-value.cost2 { color: #fff; }
|
|
||||||
.info-value.capacity { color: #fff; }
|
|
||||||
|
|
||||||
.info-unit {
|
|
||||||
position: absolute;
|
|
||||||
left: 0;
|
|
||||||
bottom: 2px;
|
|
||||||
color: #ACACAC;
|
|
||||||
font-size: 6px;
|
|
||||||
font-weight: 400;
|
|
||||||
}
|
|
||||||
`}</style>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
@ -1,10 +1,5 @@
|
||||||
import React from "react";
|
import React from "react";
|
||||||
import { formatNumber } from "~/lib/utils";
|
import { formatNumber } from "~/lib/utils";
|
||||||
import {
|
|
||||||
Tooltip,
|
|
||||||
TooltipContent,
|
|
||||||
TooltipTrigger,
|
|
||||||
} from "~/components/ui/tooltip"
|
|
||||||
|
|
||||||
interface DataItem {
|
interface DataItem {
|
||||||
label: string;
|
label: string;
|
||||||
|
|
@ -59,27 +54,12 @@ export function DashboardCustomBarChart({
|
||||||
<div className="flex-row-reverse items-center gap-2 flex min-h-6 h-10 rounded-lg overflow-hidden">
|
<div className="flex-row-reverse items-center gap-2 flex min-h-6 h-10 rounded-lg overflow-hidden">
|
||||||
{/* Animated bar */}
|
{/* Animated bar */}
|
||||||
<div
|
<div
|
||||||
className={`h-auto gap-2 overflow-hidden ${item.color} rounded-lg transition-all duration-1000 ease-out flex items-center justify-end px-2`}
|
className={`h-auto gap-2 ${item.color} rounded-lg transition-all duration-1000 ease-out flex items-center justify-end px-2`}
|
||||||
style={{ width: `${widthPercentage}%` }}
|
style={{ width: `${widthPercentage}%` }}
|
||||||
>
|
>
|
||||||
{ widthPercentage > 20 ? (
|
<span className="text-[#3F415A] text-left font-persian font-medium text-sm py-1 w-max">
|
||||||
<span className="text-[#3F415A] min-w-max text-left font-persian font-medium text-sm py-1 w-max">
|
|
||||||
{item.label}
|
{item.label}
|
||||||
</span>
|
</span>
|
||||||
) : (
|
|
||||||
<Tooltip>
|
|
||||||
<TooltipTrigger className={`${item.color}`} asChild>
|
|
||||||
<span className="text-[#3F415A] text-left font-persian font-medium text-sm py-1">
|
|
||||||
<span className="invisible">""</span>
|
|
||||||
</span>
|
|
||||||
|
|
||||||
</TooltipTrigger>
|
|
||||||
<TooltipContent className={`${item.color} ${item.color.replace("bg","fill")}`}>
|
|
||||||
<p className="font-persian text-sm">{item.label}</p>
|
|
||||||
</TooltipContent>
|
|
||||||
|
|
||||||
</Tooltip>
|
|
||||||
) }
|
|
||||||
</div>
|
</div>
|
||||||
<span className="text-white font-bold text-base">
|
<span className="text-white font-bold text-base">
|
||||||
{formatNumber(item.value)}
|
{formatNumber(item.value)}
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,38 @@
|
||||||
import { Book, CheckCircle } from "lucide-react";
|
import { useState, useEffect } from "react";
|
||||||
import { useEffect, useState } from "react";
|
import { DashboardLayout } from "./layout";
|
||||||
|
import { Card, CardContent, CardHeader, CardTitle } from "~/components/ui/card";
|
||||||
|
import { Progress } from "~/components/ui/progress";
|
||||||
|
import { Badge } from "~/components/ui/badge";
|
||||||
|
import { Button } from "~/components/ui/button";
|
||||||
|
import {
|
||||||
|
BarChart,
|
||||||
|
Bar,
|
||||||
|
XAxis,
|
||||||
|
YAxis,
|
||||||
|
CartesianGrid,
|
||||||
|
Tooltip,
|
||||||
|
ResponsiveContainer,
|
||||||
|
LineChart,
|
||||||
|
Line,
|
||||||
|
} from "recharts";
|
||||||
|
import apiService from "~/lib/api";
|
||||||
import toast from "react-hot-toast";
|
import toast from "react-hot-toast";
|
||||||
|
import {
|
||||||
|
Calendar,
|
||||||
|
TrendingUp,
|
||||||
|
TrendingDown,
|
||||||
|
Target,
|
||||||
|
Lightbulb,
|
||||||
|
DollarSign,
|
||||||
|
Minus,
|
||||||
|
CheckCircle,
|
||||||
|
Book,
|
||||||
|
} from "lucide-react";
|
||||||
|
import { Tabs, TabsList, TabsTrigger, TabsContent } from "~/components/ui/tabs";
|
||||||
|
import { CustomBarChart } from "~/components/ui/custom-bar-chart";
|
||||||
|
import { DashboardCustomBarChart } from "./dashboard-custom-bar-chart";
|
||||||
|
import { InteractiveBarChart } from "./interactive-bar-chart";
|
||||||
|
import { D3ImageInfo } from "./d3-image-info";
|
||||||
import {
|
import {
|
||||||
Label,
|
Label,
|
||||||
PolarGrid,
|
PolarGrid,
|
||||||
|
|
@ -8,21 +40,10 @@ import {
|
||||||
RadialBar,
|
RadialBar,
|
||||||
RadialBarChart,
|
RadialBarChart,
|
||||||
} from "recharts";
|
} from "recharts";
|
||||||
import { BaseCard } from "~/components/ui/base-card";
|
|
||||||
import { Button } from "~/components/ui/button";
|
|
||||||
import { Card, CardContent, CardHeader, CardTitle } from "~/components/ui/card";
|
|
||||||
import { ChartContainer } from "~/components/ui/chart";
|
import { ChartContainer } from "~/components/ui/chart";
|
||||||
|
import { formatNumber } from "~/lib/utils";
|
||||||
import { MetricCard } from "~/components/ui/metric-card";
|
import { MetricCard } from "~/components/ui/metric-card";
|
||||||
import { Progress } from "~/components/ui/progress";
|
import { BaseCard } from "~/components/ui/base-card";
|
||||||
import { Tabs, TabsContent, TabsList, TabsTrigger } from "~/components/ui/tabs";
|
|
||||||
import { useStoredDate } from "~/hooks/useStoredDate";
|
|
||||||
import apiService from "~/lib/api";
|
|
||||||
import { EventBus, formatNumber } from "~/lib/utils";
|
|
||||||
import type { CalendarDate } from "~/types/util.type";
|
|
||||||
import { D3ImageInfo } from "./d3-image-info";
|
|
||||||
import { DashboardCustomBarChart } from "./dashboard-custom-bar-chart";
|
|
||||||
import { InteractiveBarChart } from "./interactive-bar-chart";
|
|
||||||
import { DashboardLayout } from "./layout";
|
|
||||||
|
|
||||||
export function DashboardHome() {
|
export function DashboardHome() {
|
||||||
const [dashboardData, setDashboardData] = useState<any | null>(null);
|
const [dashboardData, setDashboardData] = useState<any | null>(null);
|
||||||
|
|
@ -30,54 +51,35 @@ export function DashboardHome() {
|
||||||
const [error, setError] = useState<string | null>(null);
|
const [error, setError] = useState<string | null>(null);
|
||||||
// Chart and schematic data from select API
|
// Chart and schematic data from select API
|
||||||
const [companyChartData, setCompanyChartData] = useState<
|
const [companyChartData, setCompanyChartData] = useState<
|
||||||
{
|
{ category: string; capacity: number; revenue: number; cost: number , costI : number,
|
||||||
category: string;
|
capacityI : number,
|
||||||
capacity: number;
|
revenueI : number }[]
|
||||||
revenue: number;
|
|
||||||
cost: number;
|
|
||||||
costI: number;
|
|
||||||
capacityI: number;
|
|
||||||
revenueI: number;
|
|
||||||
}[]
|
|
||||||
>([]);
|
>([]);
|
||||||
|
const [totalIncreasedCapacity, setTotalIncreasedCapacity] = useState<number>(0);
|
||||||
const [date, setDate] = useStoredDate();
|
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const handler = (date: CalendarDate) => {
|
fetchDashboardData();
|
||||||
if (date) setDate(date);
|
|
||||||
};
|
|
||||||
|
|
||||||
EventBus.on("dateSelected", handler);
|
|
||||||
|
|
||||||
return () => {
|
|
||||||
EventBus.off("dateSelected", handler);
|
|
||||||
};
|
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
if (date?.end && date?.start) fetchDashboardData();
|
|
||||||
}, [date]);
|
|
||||||
|
|
||||||
const fetchDashboardData = async () => {
|
const fetchDashboardData = async () => {
|
||||||
try {
|
try {
|
||||||
setLoading(true);
|
setLoading(true);
|
||||||
setError(null);
|
setError(null);
|
||||||
|
|
||||||
|
// First authenticate if needed
|
||||||
|
const token = localStorage.getItem("auth_token");
|
||||||
|
if (!token) {
|
||||||
|
await apiService.login("inogen_admin", "123456");
|
||||||
|
}
|
||||||
|
|
||||||
// Fetch top cards data
|
// Fetch top cards data
|
||||||
const topCardsResponse = await apiService.call({
|
const topCardsResponse = await apiService.call({
|
||||||
main_page_first_function: {
|
main_page_first_function: {},
|
||||||
start_date: date.start || null,
|
|
||||||
end_date: date.end || null,
|
|
||||||
},
|
|
||||||
});
|
});
|
||||||
|
|
||||||
// Fetch left section data
|
// Fetch left section data
|
||||||
const leftCardsResponse = await apiService.call({
|
const leftCardsResponse = await apiService.call({
|
||||||
main_page_second_function: {
|
main_page_second_function: {},
|
||||||
start_date: date.start || null,
|
|
||||||
end_date: date.end || null,
|
|
||||||
},
|
|
||||||
});
|
});
|
||||||
|
|
||||||
const topCardsResponseData = JSON.parse(topCardsResponse?.data);
|
const topCardsResponseData = JSON.parse(topCardsResponse?.data);
|
||||||
|
|
@ -110,10 +112,6 @@ export function DashboardHome() {
|
||||||
"sum(pre_project_income)",
|
"sum(pre_project_income)",
|
||||||
"sum(increased_income_after_innovation)",
|
"sum(increased_income_after_innovation)",
|
||||||
],
|
],
|
||||||
Conditions: [
|
|
||||||
["start_date", ">=", date.start || null, "and"],
|
|
||||||
["start_date", "<=", date.end || null],
|
|
||||||
],
|
|
||||||
GroupBy: ["related_company"],
|
GroupBy: ["related_company"],
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
@ -132,30 +130,12 @@ export function DashboardHome() {
|
||||||
let incCapacityTotal = 0;
|
let incCapacityTotal = 0;
|
||||||
const chartRows = rows.map((r) => {
|
const chartRows = rows.map((r) => {
|
||||||
const rel = r?.related_company ?? "-";
|
const rel = r?.related_company ?? "-";
|
||||||
const preFee =
|
const preFee = Number(r?.pre_innovation_fee_sum ?? 0) >= 0 ? r?.pre_innovation_fee_sum : 0;
|
||||||
Number(r?.pre_innovation_fee_sum ?? 0) >= 0
|
const costRed = Number(r?.innovation_cost_reduction_sum ?? 0) >= 0 ? r?.innovation_cost_reduction_sum : 0;
|
||||||
? r?.pre_innovation_fee_sum
|
const preCap = Number(r?.pre_project_production_capacity_sum ?? 0) >= 0 ? r?.pre_project_production_capacity_sum : 0;
|
||||||
: 0;
|
const incCap = Number(r?.increased_capacity_after_innovation_sum ?? 0) >= 0 ? r?.increased_capacity_after_innovation_sum : 0;
|
||||||
const costRed =
|
const preInc = Number(r?.pre_project_income_sum ?? 0) >= 0 ? r?.pre_project_income_sum : 0;
|
||||||
Number(r?.innovation_cost_reduction_sum ?? 0) >= 0
|
const incInc = Number(r?.increased_income_after_innovation_sum ?? 0) >= 0 ? r?.increased_income_after_innovation_sum : 0;
|
||||||
? r?.innovation_cost_reduction_sum
|
|
||||||
: 0;
|
|
||||||
const preCap =
|
|
||||||
Number(r?.pre_project_production_capacity_sum ?? 0) >= 0
|
|
||||||
? r?.pre_project_production_capacity_sum
|
|
||||||
: 0;
|
|
||||||
const incCap =
|
|
||||||
Number(r?.increased_capacity_after_innovation_sum ?? 0) >= 0
|
|
||||||
? r?.increased_capacity_after_innovation_sum
|
|
||||||
: 0;
|
|
||||||
const preInc =
|
|
||||||
Number(r?.pre_project_income_sum ?? 0) >= 0
|
|
||||||
? r?.pre_project_income_sum
|
|
||||||
: 0;
|
|
||||||
const incInc =
|
|
||||||
Number(r?.increased_income_after_innovation_sum ?? 0) >= 0
|
|
||||||
? r?.increased_income_after_innovation_sum
|
|
||||||
: 0;
|
|
||||||
|
|
||||||
incCapacityTotal += incCap;
|
incCapacityTotal += incCap;
|
||||||
|
|
||||||
|
|
@ -167,14 +147,14 @@ export function DashboardHome() {
|
||||||
capacity: isFinite(capacityPct) ? capacityPct : 0,
|
capacity: isFinite(capacityPct) ? capacityPct : 0,
|
||||||
revenue: isFinite(revenuePct) ? revenuePct : 0,
|
revenue: isFinite(revenuePct) ? revenuePct : 0,
|
||||||
cost: isFinite(costPct) ? costPct : 0,
|
cost: isFinite(costPct) ? costPct : 0,
|
||||||
costI: costRed,
|
costI : costRed,
|
||||||
capacityI: incCap,
|
capacityI : incCap,
|
||||||
revenueI: incInc,
|
revenueI : incInc
|
||||||
};
|
};
|
||||||
});
|
});
|
||||||
|
|
||||||
setCompanyChartData(chartRows);
|
setCompanyChartData(chartRows);
|
||||||
// setTotalIncreasedCapacity(incCapacityTotal);
|
setTotalIncreasedCapacity(incCapacityTotal);
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error("Error fetching dashboard data:", error);
|
console.error("Error fetching dashboard data:", error);
|
||||||
const errorMessage =
|
const errorMessage =
|
||||||
|
|
@ -187,24 +167,25 @@ export function DashboardHome() {
|
||||||
};
|
};
|
||||||
|
|
||||||
// RadialBarChart data for ideas visualization
|
// RadialBarChart data for ideas visualization
|
||||||
// const getIdeasChartData = () => {
|
const getIdeasChartData = () => {
|
||||||
// if (!dashboardData?.topData)
|
if (!dashboardData?.topData)
|
||||||
// return [{ browser: "safari", visitors: 0, fill: "var(--color-safari)" }];
|
return [{ browser: "safari", visitors: 0, fill: "var(--color-safari)" }];
|
||||||
|
|
||||||
// const registered = parseFloat(
|
const registered = parseFloat(
|
||||||
// dashboardData.topData.registered_innovation_technology_idea || "0"
|
dashboardData.topData.registered_innovation_technology_idea || "0",
|
||||||
// );
|
);
|
||||||
// const ongoing = parseFloat(
|
const ongoing = parseFloat(
|
||||||
// dashboardData.topData.ongoing_innovation_technology_ideas || "0"
|
dashboardData.topData.ongoing_innovation_technology_ideas || "0",
|
||||||
// );
|
);
|
||||||
// const percentage = registered > 0 ? (ongoing / registered) * 100 : 0;
|
const percentage =
|
||||||
|
registered > 0 ? (ongoing / registered) * 100 : 0;
|
||||||
|
|
||||||
// return [
|
return [
|
||||||
// { browser: "safari", visitors: percentage, fill: "var(--color-safari)" },
|
{ browser: "safari", visitors: percentage, fill: "var(--color-safari)" },
|
||||||
// ];
|
];
|
||||||
// };
|
};
|
||||||
|
|
||||||
// const chartData = getIdeasChartData();
|
const chartData = getIdeasChartData();
|
||||||
|
|
||||||
const chartConfig = {
|
const chartConfig = {
|
||||||
visitors: {
|
visitors: {
|
||||||
|
|
@ -253,7 +234,7 @@ export function DashboardHome() {
|
||||||
style={{ height: `${Math.random() * 80 + 20}%` }}
|
style={{ height: `${Math.random() * 80 + 20}%` }}
|
||||||
></div>
|
></div>
|
||||||
<div
|
<div
|
||||||
className="w-full bg-pr-red rounded-t-sm"
|
className="w-full bg-red-400/30 rounded-t-sm"
|
||||||
style={{ height: `${Math.random() * 80 + 20}%` }}
|
style={{ height: `${Math.random() * 80 + 20}%` }}
|
||||||
></div>
|
></div>
|
||||||
</div>
|
</div>
|
||||||
|
|
@ -270,7 +251,7 @@ export function DashboardHome() {
|
||||||
if (loading) {
|
if (loading) {
|
||||||
return (
|
return (
|
||||||
<DashboardLayout>
|
<DashboardLayout>
|
||||||
<div className="grid grid-cols-3 gap-4 animate-pulse">
|
<div className="p-3 pb-0 grid grid-cols-3 gap-4 animate-pulse">
|
||||||
{/* Top Cards Row */}
|
{/* Top Cards Row */}
|
||||||
<div className="flex justify-between gap-6 [&>*]:w-full col-span-3">
|
<div className="flex justify-between gap-6 [&>*]:w-full col-span-3">
|
||||||
<SkeletonCard />
|
<SkeletonCard />
|
||||||
|
|
@ -331,7 +312,7 @@ export function DashboardHome() {
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<DashboardLayout>
|
<DashboardLayout>
|
||||||
<div className="grid grid-cols-3 gap-4">
|
<div className="grid grid-cols-3 p-3 pb-0 gap-4">
|
||||||
{/* Top Cards Row - Redesigned to match other components */}
|
{/* Top Cards Row - Redesigned to match other components */}
|
||||||
<div className="flex justify-between gap-6 [&>*]:w-full col-span-3">
|
<div className="flex justify-between gap-6 [&>*]:w-full col-span-3">
|
||||||
{/* Ideas Card */}
|
{/* Ideas Card */}
|
||||||
|
|
@ -348,22 +329,23 @@ export function DashboardHome() {
|
||||||
visitors:
|
visitors:
|
||||||
parseFloat(
|
parseFloat(
|
||||||
dashboardData.topData
|
dashboardData.topData
|
||||||
?.registered_innovation_technology_idea || "0"
|
?.registered_innovation_technology_idea || "0",
|
||||||
) > 0
|
) > 0
|
||||||
? Math.round(
|
? Math.round(
|
||||||
(parseFloat(
|
(parseFloat(
|
||||||
dashboardData.topData
|
dashboardData.topData
|
||||||
?.ongoing_innovation_technology_ideas || "0"
|
?.ongoing_innovation_technology_ideas ||
|
||||||
|
"0",
|
||||||
) /
|
) /
|
||||||
parseFloat(
|
parseFloat(
|
||||||
dashboardData.topData
|
dashboardData.topData
|
||||||
?.registered_innovation_technology_idea ||
|
?.registered_innovation_technology_idea ||
|
||||||
"1"
|
"1",
|
||||||
)) *
|
)) *
|
||||||
100
|
100,
|
||||||
)
|
)
|
||||||
: 0,
|
: 0,
|
||||||
fill: "var(--color-green)",
|
fill: "green",
|
||||||
},
|
},
|
||||||
]}
|
]}
|
||||||
startAngle={90}
|
startAngle={90}
|
||||||
|
|
@ -371,18 +353,19 @@ export function DashboardHome() {
|
||||||
90 +
|
90 +
|
||||||
((parseFloat(
|
((parseFloat(
|
||||||
dashboardData.topData
|
dashboardData.topData
|
||||||
?.registered_innovation_technology_idea || "0"
|
?.registered_innovation_technology_idea || "0",
|
||||||
) > 0
|
) > 0
|
||||||
? Math.round(
|
? Math.round(
|
||||||
(parseFloat(
|
(parseFloat(
|
||||||
dashboardData.topData
|
dashboardData.topData
|
||||||
?.ongoing_innovation_technology_ideas || "0"
|
?.ongoing_innovation_technology_ideas || "0",
|
||||||
) /
|
) /
|
||||||
parseFloat(
|
parseFloat(
|
||||||
dashboardData.topData
|
dashboardData.topData
|
||||||
?.registered_innovation_technology_idea || "1"
|
?.registered_innovation_technology_idea ||
|
||||||
|
"1",
|
||||||
)) *
|
)) *
|
||||||
100
|
100,
|
||||||
)
|
)
|
||||||
: 0) /
|
: 0) /
|
||||||
100) *
|
100) *
|
||||||
|
|
@ -398,7 +381,11 @@ export function DashboardHome() {
|
||||||
className="first:fill-pr-red last:fill-[#24273A]"
|
className="first:fill-pr-red last:fill-[#24273A]"
|
||||||
polarRadius={[38, 31]}
|
polarRadius={[38, 31]}
|
||||||
/>
|
/>
|
||||||
<RadialBar dataKey="visitors" background cornerRadius={5} />
|
<RadialBar
|
||||||
|
dataKey="visitors"
|
||||||
|
background
|
||||||
|
cornerRadius={5}
|
||||||
|
/>
|
||||||
<PolarRadiusAxis
|
<PolarRadiusAxis
|
||||||
tick={false}
|
tick={false}
|
||||||
tickLine={false}
|
tickLine={false}
|
||||||
|
|
@ -424,22 +411,22 @@ export function DashboardHome() {
|
||||||
parseFloat(
|
parseFloat(
|
||||||
dashboardData.topData
|
dashboardData.topData
|
||||||
?.registered_innovation_technology_idea ||
|
?.registered_innovation_technology_idea ||
|
||||||
"0"
|
"0",
|
||||||
) > 0
|
) > 0
|
||||||
? Math.round(
|
? Math.round(
|
||||||
(parseFloat(
|
(parseFloat(
|
||||||
dashboardData.topData
|
dashboardData.topData
|
||||||
?.ongoing_innovation_technology_ideas ||
|
?.ongoing_innovation_technology_ideas ||
|
||||||
"0"
|
"0",
|
||||||
) /
|
) /
|
||||||
parseFloat(
|
parseFloat(
|
||||||
dashboardData.topData
|
dashboardData.topData
|
||||||
?.registered_innovation_technology_idea ||
|
?.registered_innovation_technology_idea ||
|
||||||
"1"
|
"1",
|
||||||
)) *
|
)) *
|
||||||
100
|
100,
|
||||||
)
|
)
|
||||||
: 0
|
: 0,
|
||||||
)}
|
)}
|
||||||
</tspan>
|
</tspan>
|
||||||
</text>
|
</text>
|
||||||
|
|
@ -456,14 +443,14 @@ export function DashboardHome() {
|
||||||
<div className="font-light text-sm">ثبت شده :</div>
|
<div className="font-light text-sm">ثبت شده :</div>
|
||||||
{formatNumber(
|
{formatNumber(
|
||||||
dashboardData.topData
|
dashboardData.topData
|
||||||
?.registered_innovation_technology_idea || "0"
|
?.registered_innovation_technology_idea || "0",
|
||||||
)}
|
)}
|
||||||
</span>
|
</span>
|
||||||
<span className="flex items-center gap-1 font-bold text-base">
|
<span className="flex items-center gap-1 font-bold text-base">
|
||||||
<div className="font-light text-sm">در حال اجرا :</div>
|
<div className="font-light text-sm">در حال اجرا :</div>
|
||||||
{formatNumber(
|
{formatNumber(
|
||||||
dashboardData.topData
|
dashboardData.topData
|
||||||
?.ongoing_innovation_technology_ideas || "0"
|
?.ongoing_innovation_technology_ideas || "0",
|
||||||
)}
|
)}
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
|
|
@ -473,162 +460,145 @@ export function DashboardHome() {
|
||||||
{/* Revenue Card */}
|
{/* Revenue Card */}
|
||||||
<MetricCard
|
<MetricCard
|
||||||
title="افزایش درآمد مبتنی بر فناوری و نوآوری"
|
title="افزایش درآمد مبتنی بر فناوری و نوآوری"
|
||||||
value={
|
value={dashboardData.topData?.technology_innovation_based_revenue_growth || "0"}
|
||||||
dashboardData.topData?.technology_innovation_based_revenue_growth?.replaceAll(
|
percentValue={dashboardData.topData?.technology_innovation_based_revenue_growth_percent}
|
||||||
",",
|
|
||||||
""
|
|
||||||
) || "0"
|
|
||||||
}
|
|
||||||
percentValue={
|
|
||||||
dashboardData.topData
|
|
||||||
?.technology_innovation_based_revenue_growth_percent
|
|
||||||
}
|
|
||||||
percentLabel="درصد به کل درآمد"
|
percentLabel="درصد به کل درآمد"
|
||||||
/>
|
/>
|
||||||
|
|
||||||
{/* Cost Reduction Card */}
|
{/* Cost Reduction Card */}
|
||||||
<MetricCard
|
<MetricCard
|
||||||
title="کاهش هزینه ها مبتنی بر فناوری و نوآوری"
|
title="کاهش هزینه ها مبتنی بر فناوری و نوآوری"
|
||||||
value={Math.round(
|
value={Math.round(parseFloat(dashboardData.topData?.technology_innovation_based_cost_reduction?.replace(/,/g, "") || "0") / 1000000)}
|
||||||
parseFloat(
|
percentValue={dashboardData.topData?.technology_innovation_based_cost_reduction_percent || "0"}
|
||||||
dashboardData.topData?.technology_innovation_based_cost_reduction?.replace(
|
|
||||||
/,/g,
|
|
||||||
""
|
|
||||||
) || "0"
|
|
||||||
)
|
|
||||||
)}
|
|
||||||
percentValue={
|
|
||||||
dashboardData.topData
|
|
||||||
?.technology_innovation_based_cost_reduction_percent || "0"
|
|
||||||
}
|
|
||||||
percentLabel="درصد به کل هزینه"
|
percentLabel="درصد به کل هزینه"
|
||||||
/>
|
/>
|
||||||
|
|
||||||
{/* Budget Ratio Card */}
|
{/* Budget Ratio Card */}
|
||||||
<BaseCard title="نسبت تحقق بودجه فناوی و نوآوری">
|
<BaseCard title="نسبت تحقق بودجه فناوی و نوآوری">
|
||||||
<div className="flex items-center gap-2 justify-center flex-row-reverse">
|
<div className="flex items-center gap-2 justify-center flex-row-reverse">
|
||||||
<ChartContainer
|
<ChartContainer
|
||||||
config={chartConfig}
|
config={chartConfig}
|
||||||
className="aspect-square w-[6rem] h-auto"
|
className="aspect-square w-[6rem] h-auto"
|
||||||
>
|
|
||||||
<RadialBarChart
|
|
||||||
data={[
|
|
||||||
{
|
|
||||||
browser: "budget",
|
|
||||||
visitors: parseFloat(
|
|
||||||
dashboardData.topData
|
|
||||||
?.innovation_budget_achievement_percent || "0"
|
|
||||||
),
|
|
||||||
fill: "var(--color-green)",
|
|
||||||
},
|
|
||||||
]}
|
|
||||||
startAngle={90}
|
|
||||||
endAngle={
|
|
||||||
90 +
|
|
||||||
(dashboardData.topData
|
|
||||||
?.innovation_budget_achievement_percent /
|
|
||||||
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
|
<RadialBarChart
|
||||||
content={({ viewBox }) => {
|
data={[
|
||||||
if (viewBox && "cx" in viewBox && "cy" in viewBox) {
|
{
|
||||||
return (
|
browser: "budget",
|
||||||
<text
|
visitors: parseFloat(
|
||||||
x={viewBox.cx}
|
dashboardData.topData
|
||||||
y={viewBox.cy}
|
?.innovation_budget_achievement_percent || "0",
|
||||||
textAnchor="middle"
|
),
|
||||||
dominantBaseline="middle"
|
fill: "green",
|
||||||
>
|
},
|
||||||
<tspan
|
]}
|
||||||
x={viewBox.cx}
|
startAngle={90}
|
||||||
y={viewBox.cy}
|
endAngle={
|
||||||
className="fill-foreground text-lg font-bold"
|
90 +
|
||||||
>
|
(dashboardData.topData
|
||||||
%
|
?.innovation_budget_achievement_percent /
|
||||||
{formatNumber(
|
100) *
|
||||||
Math.round(
|
360
|
||||||
dashboardData.topData
|
}
|
||||||
?.innovation_budget_achievement_percent ||
|
innerRadius={35}
|
||||||
0
|
outerRadius={55}
|
||||||
)
|
>
|
||||||
)}
|
<PolarGrid
|
||||||
</tspan>
|
gridType="circle"
|
||||||
</text>
|
radialLines={false}
|
||||||
);
|
stroke="none"
|
||||||
}
|
className="first:fill-pr-red last:fill-[#24273A]"
|
||||||
}}
|
polarRadius={[38, 31]}
|
||||||
/>
|
/>
|
||||||
</PolarRadiusAxis>
|
<RadialBar
|
||||||
</RadialBarChart>
|
dataKey="visitors"
|
||||||
</ChartContainer>
|
background
|
||||||
<div className="font-bold font-persian text-center">
|
cornerRadius={5}
|
||||||
<div className="flex flex-col justify-between items-center gap-2">
|
/>
|
||||||
<span className="flex font-bold items-center text-base gap-1 mr-auto">
|
<PolarRadiusAxis
|
||||||
<div className="font-light text-sm">مصوب :</div>
|
tick={false}
|
||||||
{formatNumber(
|
tickLine={false}
|
||||||
Math.round(
|
axisLine={false}
|
||||||
parseFloat(
|
>
|
||||||
dashboardData.topData?.approved_innovation_budget_achievement_ratio?.replace(
|
<Label
|
||||||
/,/g,
|
content={({ viewBox }) => {
|
||||||
""
|
if (viewBox && "cx" in viewBox && "cy" in viewBox) {
|
||||||
) || "0"
|
return (
|
||||||
)
|
<text
|
||||||
)
|
x={viewBox.cx}
|
||||||
)}
|
y={viewBox.cy}
|
||||||
</span>
|
textAnchor="middle"
|
||||||
<span className="flex items-center gap-1 text-base font-bold mr-auto">
|
dominantBaseline="middle"
|
||||||
<div className="font-light text-sm">جذب شده :</div>
|
>
|
||||||
{formatNumber(
|
<tspan
|
||||||
Math.round(
|
x={viewBox.cx}
|
||||||
parseFloat(
|
y={viewBox.cy}
|
||||||
dashboardData.topData?.allocated_innovation_budget_achievement_ratio?.replace(
|
className="fill-foreground text-lg font-bold"
|
||||||
/,/g,
|
>
|
||||||
""
|
%
|
||||||
) || "0"
|
{formatNumber(
|
||||||
)
|
Math.round(
|
||||||
)
|
dashboardData.topData
|
||||||
)}
|
?.innovation_budget_achievement_percent ||
|
||||||
</span>
|
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 text-base gap-1 mr-auto">
|
||||||
|
<div className="font-light text-sm">مصوب :</div>
|
||||||
|
{formatNumber(
|
||||||
|
Math.round(
|
||||||
|
parseFloat(
|
||||||
|
dashboardData.topData?.approved_innovation_budget_achievement_ratio?.replace(
|
||||||
|
/,/g,
|
||||||
|
"",
|
||||||
|
) || "0",
|
||||||
|
) / 1000000000,
|
||||||
|
),
|
||||||
|
)}
|
||||||
|
</span>
|
||||||
|
<span className="flex items-center gap-1 text-base font-bold mr-auto">
|
||||||
|
<div className="font-light text-sm">جذب شده :</div>
|
||||||
|
{formatNumber(
|
||||||
|
Math.round(
|
||||||
|
parseFloat(
|
||||||
|
dashboardData.topData?.allocated_innovation_budget_achievement_ratio?.replace(
|
||||||
|
/,/g,
|
||||||
|
"",
|
||||||
|
) || "0",
|
||||||
|
) / 1000000000,
|
||||||
|
),
|
||||||
|
)}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</BaseCard>
|
||||||
</div>
|
</div>
|
||||||
</BaseCard>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{/* Main Content with Tabs */}
|
{/* Main Content with Tabs */}
|
||||||
<Tabs
|
<Tabs
|
||||||
defaultValue="canvas"
|
defaultValue="charts"
|
||||||
className="grid overflow-hidden rounded-lg grid-rows-[max-content] items-center col-span-2 row-start-2 bg-[linear-gradient(to_bottom_left,#464861,50%,#111628)]"
|
className="grid overflow-hidden rounded-lg grid-rows-[max-content] items-center col-span-2 row-start-2 bg-[linear-gradient(to_bottom_left,#464861,50%,#111628)]"
|
||||||
>
|
>
|
||||||
<div className="flex items-center border-b border-gray-600 justify-between gap-2">
|
<div className="flex items-center border-b border-gray-600 justify-between gap-2">
|
||||||
<p className="p-6 font-persian font-semibold text-lg ">
|
<p className="p-6 font-persian font-semibold text-lg ">
|
||||||
تحقق ارزش ها
|
تحقق ارزش ها
|
||||||
</p>
|
</p>
|
||||||
<TabsList className="bg-transparent py-2 m-6 border-[1px] border-[#5F6284]">
|
<TabsList className="bg-transparent py-2 border m-6 border-gray-600">
|
||||||
<TabsTrigger value="canvas" className="cursor-pointer">
|
<TabsTrigger value="canvas" className="cursor-pointer">
|
||||||
شماتیک
|
شماتیک
|
||||||
</TabsTrigger>
|
</TabsTrigger>
|
||||||
<TabsTrigger
|
<TabsTrigger value="charts" className=" text-white cursor-pointer font-light ">
|
||||||
value="charts"
|
|
||||||
className=" text-white cursor-pointer font-light "
|
|
||||||
>
|
|
||||||
مقایسه ای
|
مقایسه ای
|
||||||
</TabsTrigger>
|
</TabsTrigger>
|
||||||
</TabsList>
|
</TabsList>
|
||||||
|
|
@ -641,48 +611,27 @@ export function DashboardHome() {
|
||||||
<TabsContent value="canvas" className="w-ful h-full">
|
<TabsContent value="canvas" className="w-ful h-full">
|
||||||
<div className="p-4 h-full w-full">
|
<div className="p-4 h-full w-full">
|
||||||
<D3ImageInfo
|
<D3ImageInfo
|
||||||
|
companies={
|
||||||
//پتروشیمی بندر امام
|
companyChartData.map((item) => {
|
||||||
// companies={companyChartData.map((item) => {
|
const imageMap: Record<string, string> = {
|
||||||
// const imageMap: Record<string, string> = {
|
"بسپاران": "/besparan.png",
|
||||||
// بسپاران: "/besparan.png",
|
"خوارزمی": "/khwarazmi.png",
|
||||||
// خوارزمی: "/khwarazmi.png",
|
"فراورش 1": "/faravash1.png",
|
||||||
// "فراورش 1": "/faravash1.png",
|
"فراورش 2": "/faravash2.png",
|
||||||
// "فراورش 2": "/faravash2.png",
|
"کیمیا": "/kimia.png",
|
||||||
// کیمیا: "/kimia.png",
|
"آب نیرو": "/abniro.png",
|
||||||
// "آب نیرو": "/abniro.png",
|
};
|
||||||
// };
|
|
||||||
|
|
||||||
|
return {
|
||||||
//پتروشیمی آپادانا
|
id: item.category,
|
||||||
companies={companyChartData.map((item) => {
|
name: item.category,
|
||||||
const imageMap: Record<string, string> = {
|
imageUrl: imageMap[item.category] || "/placeholder.png",
|
||||||
"واحد 100": "/abniro.png" ,
|
cost: item?.costI || 0,
|
||||||
"واحد 200": "/besparan.png" ,
|
capacity: item?.capacityI || 0,
|
||||||
"واحد 300": "/khwarazmi.png" ,
|
revenue: item?.revenueI || 0,
|
||||||
"واحد 400": "/faravash1.png"
|
};
|
||||||
};
|
})
|
||||||
|
}
|
||||||
//پتروشیمی نوری
|
|
||||||
// companies={companyChartData.map((item) => {
|
|
||||||
// const imageMap: Record<string, string> = {
|
|
||||||
// "واحد 100": "/abniro.png" ,
|
|
||||||
// "واحد 200": "/besparan.png" ,
|
|
||||||
// "واحد 300": "/khwarazmi.png" ,
|
|
||||||
// "واحد 400": "/faravash1.png"
|
|
||||||
// };
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
return {
|
|
||||||
id: item.category,
|
|
||||||
name: item.category,
|
|
||||||
imageUrl: imageMap[item.category] || "/placeholder.png",
|
|
||||||
cost: item?.costI || 0,
|
|
||||||
capacity: item?.capacityI || 0,
|
|
||||||
revenue: item?.revenueI || 0,
|
|
||||||
};
|
|
||||||
})}
|
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
</TabsContent>
|
</TabsContent>
|
||||||
|
|
@ -700,7 +649,7 @@ export function DashboardHome() {
|
||||||
|
|
||||||
<Progress
|
<Progress
|
||||||
value={parseFloat(
|
value={parseFloat(
|
||||||
dashboardData.leftData?.technology_intensity
|
dashboardData.leftData?.technology_intensity,
|
||||||
)}
|
)}
|
||||||
className="h-4 flex-1"
|
className="h-4 flex-1"
|
||||||
/>
|
/>
|
||||||
|
|
@ -718,21 +667,21 @@ export function DashboardHome() {
|
||||||
{
|
{
|
||||||
label: "اجرا شده",
|
label: "اجرا شده",
|
||||||
value: parseFloat(
|
value: parseFloat(
|
||||||
dashboardData?.leftData?.executed_project || "0"
|
dashboardData?.leftData?.executed_project || "0",
|
||||||
),
|
),
|
||||||
color: "bg-pr-green",
|
color: "bg-pr-green",
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
label: "در حال اجرا",
|
label: "در حال اجرا",
|
||||||
value: parseFloat(
|
value: parseFloat(
|
||||||
dashboardData?.leftData?.in_progress_project || "0"
|
dashboardData?.leftData?.in_progress_project || "0",
|
||||||
),
|
),
|
||||||
color: "bg-pr-blue",
|
color: "bg-pr-blue",
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
label: "برنامهریزی شده",
|
label: "برنامهریزی شده",
|
||||||
value: parseFloat(
|
value: parseFloat(
|
||||||
dashboardData?.leftData?.planned_project || "0"
|
dashboardData?.leftData?.planned_project || "0",
|
||||||
),
|
),
|
||||||
color: "bg-pr-red",
|
color: "bg-pr-red",
|
||||||
},
|
},
|
||||||
|
|
@ -757,7 +706,7 @@ export function DashboardHome() {
|
||||||
</div>
|
</div>
|
||||||
<span className="text-base font-bold ">
|
<span className="text-base font-bold ">
|
||||||
{formatNumber(
|
{formatNumber(
|
||||||
dashboardData.leftData?.printed_books_count || "0"
|
dashboardData.leftData?.printed_books_count || "0",
|
||||||
)}
|
)}
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
|
|
@ -768,7 +717,7 @@ export function DashboardHome() {
|
||||||
</div>
|
</div>
|
||||||
<span className="text-base font-bold ">
|
<span className="text-base font-bold ">
|
||||||
{formatNumber(
|
{formatNumber(
|
||||||
dashboardData.leftData?.registered_patents_count || "0"
|
dashboardData.leftData?.registered_patents_count || "0",
|
||||||
)}
|
)}
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
|
|
@ -779,7 +728,7 @@ export function DashboardHome() {
|
||||||
</div>
|
</div>
|
||||||
<span className="text-base font-bold ">
|
<span className="text-base font-bold ">
|
||||||
{formatNumber(
|
{formatNumber(
|
||||||
dashboardData.leftData?.published_reports_count || "0"
|
dashboardData.leftData?.published_reports_count || "0",
|
||||||
)}
|
)}
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
|
|
@ -790,7 +739,7 @@ export function DashboardHome() {
|
||||||
</div>
|
</div>
|
||||||
<span className="text-base font-bold ">
|
<span className="text-base font-bold ">
|
||||||
{formatNumber(
|
{formatNumber(
|
||||||
dashboardData.leftData?.printed_articles_count || "0"
|
dashboardData.leftData?.printed_articles_count || "0",
|
||||||
)}
|
)}
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
|
|
@ -814,7 +763,7 @@ export function DashboardHome() {
|
||||||
</div>
|
</div>
|
||||||
<span className="text-base font-bold ">
|
<span className="text-base font-bold ">
|
||||||
{formatNumber(
|
{formatNumber(
|
||||||
dashboardData.leftData?.attended_conferences_count || "0"
|
dashboardData.leftData?.attended_conferences_count || "0",
|
||||||
)}
|
)}
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
|
|
@ -825,7 +774,7 @@ export function DashboardHome() {
|
||||||
</div>
|
</div>
|
||||||
<span className="text-base font-bold ">
|
<span className="text-base font-bold ">
|
||||||
{formatNumber(
|
{formatNumber(
|
||||||
dashboardData.leftData?.attended_events_count || "0"
|
dashboardData.leftData?.attended_events_count || "0",
|
||||||
)}
|
)}
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
|
|
@ -836,7 +785,7 @@ export function DashboardHome() {
|
||||||
</div>
|
</div>
|
||||||
<span className="text-base font-bold ">
|
<span className="text-base font-bold ">
|
||||||
{formatNumber(
|
{formatNumber(
|
||||||
dashboardData.leftData?.attended_exhibitions_count || "0"
|
dashboardData.leftData?.attended_exhibitions_count || "0",
|
||||||
)}
|
)}
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
|
|
@ -847,7 +796,7 @@ export function DashboardHome() {
|
||||||
</div>
|
</div>
|
||||||
<span className="text-base font-bold ">
|
<span className="text-base font-bold ">
|
||||||
{formatNumber(
|
{formatNumber(
|
||||||
dashboardData.leftData?.organized_events_count || "0"
|
dashboardData.leftData?.organized_events_count || "0",
|
||||||
)}
|
)}
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
|
|
@ -855,8 +804,9 @@ export function DashboardHome() {
|
||||||
</CardContent>
|
</CardContent>
|
||||||
</Card>
|
</Card>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</DashboardLayout>
|
</DashboardLayout>
|
||||||
|
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -1,22 +1,21 @@
|
||||||
import { saveAs } from "file-saver";
|
import React, { useEffect, useState } from "react";
|
||||||
import jalaali from "jalaali-js";
|
|
||||||
import {
|
|
||||||
Calendar,
|
|
||||||
ChevronLeft,
|
|
||||||
FileChartColumnIncreasing,
|
|
||||||
Menu,
|
|
||||||
PanelLeft,
|
|
||||||
Server,
|
|
||||||
User,
|
|
||||||
} from "lucide-react";
|
|
||||||
import React, { useEffect, useRef, useState } from "react";
|
|
||||||
import { useLocation } from "react-router";
|
|
||||||
import XLSX from "xlsx-js-style";
|
|
||||||
import { Button } from "~/components/ui/button";
|
|
||||||
import { Calendar as CustomCalendar } from "~/components/ui/Calendar";
|
|
||||||
import { useAuth } from "~/contexts/auth-context";
|
import { useAuth } from "~/contexts/auth-context";
|
||||||
|
import { Link } from "react-router";
|
||||||
|
import { cn } from "~/lib/utils";
|
||||||
|
import { Button } from "~/components/ui/button";
|
||||||
|
import {
|
||||||
|
PanelLeft,
|
||||||
|
|
||||||
|
Settings,
|
||||||
|
User,
|
||||||
|
|
||||||
|
Menu,
|
||||||
|
ChevronDown,
|
||||||
|
Server,
|
||||||
|
ChevronLeft ,
|
||||||
|
|
||||||
|
} from "lucide-react";
|
||||||
import apiService from "~/lib/api";
|
import apiService from "~/lib/api";
|
||||||
import { cn, EventBus, handleDataValue } from "~/lib/utils";
|
|
||||||
|
|
||||||
interface HeaderProps {
|
interface HeaderProps {
|
||||||
onToggleSidebar?: () => void;
|
onToggleSidebar?: () => void;
|
||||||
|
|
@ -25,161 +24,6 @@ interface HeaderProps {
|
||||||
titleIcon?: React.ComponentType<{ className?: string }> | null;
|
titleIcon?: React.ComponentType<{ className?: string }> | null;
|
||||||
}
|
}
|
||||||
|
|
||||||
interface MonthItem {
|
|
||||||
id: string;
|
|
||||||
label: string;
|
|
||||||
start: string;
|
|
||||||
end: string;
|
|
||||||
}
|
|
||||||
|
|
||||||
interface CurrentDay {
|
|
||||||
start?: string;
|
|
||||||
end?: string;
|
|
||||||
sinceMonth?: string;
|
|
||||||
fromMonth?: string;
|
|
||||||
}
|
|
||||||
|
|
||||||
interface SelectedDate {
|
|
||||||
since?: number;
|
|
||||||
until?: number;
|
|
||||||
}
|
|
||||||
|
|
||||||
const monthList: Array<MonthItem> = [
|
|
||||||
{
|
|
||||||
id: "month-1",
|
|
||||||
label: "بهار",
|
|
||||||
start: "01/01",
|
|
||||||
end: "03/31",
|
|
||||||
},
|
|
||||||
{
|
|
||||||
id: "month-2",
|
|
||||||
label: "تابستان",
|
|
||||||
start: "04/01",
|
|
||||||
end: "06/31",
|
|
||||||
},
|
|
||||||
{
|
|
||||||
id: "month-3",
|
|
||||||
label: "پاییز",
|
|
||||||
start: "07/01",
|
|
||||||
end: "09/31",
|
|
||||||
},
|
|
||||||
{
|
|
||||||
id: "month-4",
|
|
||||||
label: "زمستان",
|
|
||||||
start: "10/01",
|
|
||||||
end: "12/30",
|
|
||||||
},
|
|
||||||
];
|
|
||||||
|
|
||||||
const columns: Array<any> = [
|
|
||||||
{ key: "title", label: "عنوان پروژه", sortable: true, width: "300px" },
|
|
||||||
{
|
|
||||||
key: "importance_project",
|
|
||||||
label: "میزان اهمیت",
|
|
||||||
sortable: true,
|
|
||||||
width: "160px",
|
|
||||||
},
|
|
||||||
{
|
|
||||||
key: "strategic_theme",
|
|
||||||
label: "مضمون راهبردی",
|
|
||||||
sortable: true,
|
|
||||||
width: "200px",
|
|
||||||
},
|
|
||||||
{
|
|
||||||
key: "value_technology_and_innovation",
|
|
||||||
label: "ارزش فناوری و نوآوری",
|
|
||||||
sortable: true,
|
|
||||||
width: "220px",
|
|
||||||
},
|
|
||||||
{
|
|
||||||
key: "type_of_innovation",
|
|
||||||
label: "انواع نوآوری",
|
|
||||||
sortable: true,
|
|
||||||
width: "160px",
|
|
||||||
},
|
|
||||||
{
|
|
||||||
key: "innovation",
|
|
||||||
label: "میزان نوآوری",
|
|
||||||
sortable: true,
|
|
||||||
width: "140px",
|
|
||||||
},
|
|
||||||
{
|
|
||||||
key: "person_executing",
|
|
||||||
label: "مسئول اجرا",
|
|
||||||
sortable: true,
|
|
||||||
width: "180px",
|
|
||||||
},
|
|
||||||
{
|
|
||||||
key: "excellent_observer",
|
|
||||||
label: "ناطر عالی",
|
|
||||||
sortable: true,
|
|
||||||
width: "180px",
|
|
||||||
},
|
|
||||||
{ key: "observer", label: "ناظر پروژه", sortable: true, width: "180px" },
|
|
||||||
{ key: "moderator", label: "مجری", sortable: true, width: "180px" },
|
|
||||||
{
|
|
||||||
key: "executive_phase",
|
|
||||||
label: "فاز اجرایی",
|
|
||||||
sortable: true,
|
|
||||||
width: "160px",
|
|
||||||
},
|
|
||||||
{
|
|
||||||
key: "start_date",
|
|
||||||
label: "تاریخ شروع",
|
|
||||||
sortable: true,
|
|
||||||
width: "120px",
|
|
||||||
},
|
|
||||||
{
|
|
||||||
key: "remaining_time",
|
|
||||||
label: "زمان باقی مانده",
|
|
||||||
sortable: true,
|
|
||||||
width: "140px",
|
|
||||||
computed: true,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
key: "end_date",
|
|
||||||
label: "تاریخ پایان (برنامهریزی)",
|
|
||||||
sortable: true,
|
|
||||||
width: "160px",
|
|
||||||
},
|
|
||||||
{
|
|
||||||
key: "renewed_duration",
|
|
||||||
label: "مدت زمان تمدید",
|
|
||||||
sortable: true,
|
|
||||||
width: "140px",
|
|
||||||
},
|
|
||||||
{
|
|
||||||
key: "done_date",
|
|
||||||
label: "تاریخ پایان (واقعی)",
|
|
||||||
sortable: true,
|
|
||||||
width: "160px",
|
|
||||||
},
|
|
||||||
{
|
|
||||||
key: "deviation_from_program",
|
|
||||||
label: "متوسط انحراف برنامهای",
|
|
||||||
sortable: true,
|
|
||||||
width: "160px",
|
|
||||||
},
|
|
||||||
{
|
|
||||||
key: "approved_budget",
|
|
||||||
label: "بودجه مصوب",
|
|
||||||
sortable: true,
|
|
||||||
width: "150px",
|
|
||||||
},
|
|
||||||
{
|
|
||||||
key: "budget_spent",
|
|
||||||
label: "بودجه صرف شده",
|
|
||||||
sortable: true,
|
|
||||||
width: "150px",
|
|
||||||
},
|
|
||||||
{
|
|
||||||
key: "cost_deviation",
|
|
||||||
label: "متوسط انحراف هزینهای",
|
|
||||||
sortable: true,
|
|
||||||
width: "160px",
|
|
||||||
},
|
|
||||||
];
|
|
||||||
|
|
||||||
export function Header({
|
export function Header({
|
||||||
onToggleSidebar,
|
onToggleSidebar,
|
||||||
className,
|
className,
|
||||||
|
|
@ -187,217 +31,25 @@ export function Header({
|
||||||
titleIcon,
|
titleIcon,
|
||||||
}: HeaderProps) {
|
}: HeaderProps) {
|
||||||
const { user } = useAuth();
|
const { user } = useAuth();
|
||||||
const { jy } = jalaali.toJalaali(new Date());
|
const [isProfileMenuOpen, setIsProfileMenuOpen] = useState(false);
|
||||||
|
const [isNotificationOpen, setIsNotificationOpen] = useState(false);
|
||||||
const calendarRef = useRef<HTMLDivElement>(null);
|
|
||||||
const [isProfileMenuOpen, setIsProfileMenuOpen] = useState<boolean>(false);
|
|
||||||
const [isNotificationOpen, setIsNotificationOpen] = useState<boolean>(false);
|
|
||||||
const [openCalendar, setOpenCalendar] = useState<boolean>(false);
|
|
||||||
const [excelLoading, setExcelLoading] = useState<boolean>(false);
|
|
||||||
const location = useLocation();
|
|
||||||
const projectManagerRoute = "/dashboard/project-management";
|
|
||||||
const [currentYear, setCurrentYear] = useState<SelectedDate>({
|
|
||||||
since: jy,
|
|
||||||
until: jy,
|
|
||||||
});
|
|
||||||
|
|
||||||
const [selectedDate, setSelectedDate] = useState<CurrentDay>({});
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
const storedDate = localStorage.getItem("dateSelected");
|
|
||||||
if (storedDate) {
|
|
||||||
const parsedDate = JSON.parse(storedDate);
|
|
||||||
setSelectedDate(parsedDate);
|
|
||||||
|
|
||||||
const sinceYear = parsedDate.start
|
|
||||||
? parseInt(parsedDate.start.split("/")[0], 10)
|
|
||||||
: jy;
|
|
||||||
const untilYear = parsedDate.end
|
|
||||||
? parseInt(parsedDate.end.split("/")[0], 10)
|
|
||||||
: jy;
|
|
||||||
|
|
||||||
setCurrentYear({ since: sinceYear, until: untilYear });
|
|
||||||
} else {
|
|
||||||
const defaultDate = {
|
|
||||||
sinceMonth: "بهار",
|
|
||||||
fromMonth: "زمستان",
|
|
||||||
start: `${jy}/01/01`,
|
|
||||||
end: `${jy}/12/30`,
|
|
||||||
};
|
|
||||||
setSelectedDate(defaultDate);
|
|
||||||
localStorage.setItem("dateSelected", JSON.stringify(defaultDate));
|
|
||||||
setCurrentYear({ since: jy, until: jy });
|
|
||||||
}
|
|
||||||
}, []);
|
|
||||||
|
|
||||||
const redirectHandler = async () => {
|
const redirectHandler = async () => {
|
||||||
try {
|
try {
|
||||||
const getData = await apiService.post("/GenerateSsoCode");
|
const getData = await apiService.post('/GenerateSsoCode')
|
||||||
|
//const url = `http://localhost:3000/redirect/${getData.data}`;
|
||||||
//بندر امام
|
const url = `https://inogen-bpms.pelekan.org/redirect/${getData.data}`;
|
||||||
// const url = `https://inogen-bpms.pelekan.org/redirect/${getData.data}`;
|
|
||||||
//آپادانا
|
|
||||||
const url = `https://APADANA-IATM-bpms.pelekan.org/redirect/${getData.data}`;
|
|
||||||
//نوری
|
|
||||||
// const url = `https://NOPC-IATM-bpms.pelekan.org/redirect/${getData.data}`;
|
|
||||||
|
|
||||||
window.open(url, "_blank");
|
window.open(url, "_blank");
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.log(error);
|
console.log(error);
|
||||||
}
|
}
|
||||||
};
|
}
|
||||||
|
|
||||||
const changeSinceYear = (delta: number) => {
|
|
||||||
if (!currentYear) return;
|
|
||||||
|
|
||||||
const newSince = (currentYear.since ?? 0) + delta;
|
|
||||||
|
|
||||||
if (newSince > (currentYear.until ?? Infinity) || newSince < 0) return;
|
|
||||||
|
|
||||||
const updatedYear = { ...currentYear, since: newSince };
|
|
||||||
setCurrentYear(updatedYear);
|
|
||||||
|
|
||||||
const updatedDate = {
|
|
||||||
...selectedDate,
|
|
||||||
start: `${newSince}/${selectedDate.start?.split("/").slice(1).join("/")}`,
|
|
||||||
};
|
|
||||||
setSelectedDate(updatedDate);
|
|
||||||
localStorage.setItem("dateSelected", JSON.stringify(updatedDate));
|
|
||||||
EventBus.emit("dateSelected", updatedDate);
|
|
||||||
};
|
|
||||||
|
|
||||||
const nextFromYearHandler = () => changeSinceYear(1);
|
|
||||||
const prevFromYearHandler = () => changeSinceYear(-1);
|
|
||||||
|
|
||||||
const selectFromDateHandler = (val: MonthItem) => {
|
|
||||||
const data = {
|
|
||||||
...selectedDate,
|
|
||||||
start: `${currentYear.since}/${val.start}`,
|
|
||||||
sinceMonth: val.label,
|
|
||||||
};
|
|
||||||
setSelectedDate(data);
|
|
||||||
localStorage.setItem("dateSelected", JSON.stringify(data));
|
|
||||||
EventBus.emit("dateSelected", data);
|
|
||||||
};
|
|
||||||
|
|
||||||
const changeUntilYear = (delta: number) => {
|
|
||||||
if (!currentYear) return;
|
|
||||||
|
|
||||||
const newUntil = (currentYear.until ?? 0) + delta;
|
|
||||||
|
|
||||||
if (newUntil < (currentYear.since ?? 0)) return;
|
|
||||||
|
|
||||||
const updatedYear = { ...currentYear, until: newUntil };
|
|
||||||
setCurrentYear(updatedYear);
|
|
||||||
|
|
||||||
const updatedDate = {
|
|
||||||
...selectedDate,
|
|
||||||
end: `${newUntil}/${selectedDate.end?.split("/").slice(1).join("/")}`,
|
|
||||||
};
|
|
||||||
setSelectedDate(updatedDate);
|
|
||||||
localStorage.setItem("dateSelected", JSON.stringify(updatedDate));
|
|
||||||
EventBus.emit("dateSelected", updatedDate);
|
|
||||||
};
|
|
||||||
|
|
||||||
const nextUntilYearHandler = () => changeUntilYear(1);
|
|
||||||
const prevUntilYearHandler = () => changeUntilYear(-1);
|
|
||||||
|
|
||||||
const selectUntilDateHandler = (val: MonthItem) => {
|
|
||||||
const data = {
|
|
||||||
...selectedDate,
|
|
||||||
end: `${currentYear.until}/${val.end}`,
|
|
||||||
fromMonth: val.label,
|
|
||||||
};
|
|
||||||
setSelectedDate(data);
|
|
||||||
localStorage.setItem("dateSelected", JSON.stringify(data));
|
|
||||||
EventBus.emit("dateSelected", data);
|
|
||||||
toggleCalendar();
|
|
||||||
};
|
|
||||||
|
|
||||||
const toggleCalendar = () => {
|
|
||||||
setOpenCalendar(!openCalendar);
|
|
||||||
};
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
const handleClickOutside = (event: MouseEvent) => {
|
|
||||||
if (
|
|
||||||
calendarRef.current &&
|
|
||||||
!calendarRef.current.contains(event.target as Node)
|
|
||||||
) {
|
|
||||||
setOpenCalendar(false);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
document.addEventListener("mousedown", handleClickOutside);
|
|
||||||
return () => {
|
|
||||||
document.removeEventListener("mousedown", handleClickOutside);
|
|
||||||
};
|
|
||||||
}, []);
|
|
||||||
|
|
||||||
const exportToExcel = async () => {
|
|
||||||
let arr = [];
|
|
||||||
const data: any = await fetchExcelData();
|
|
||||||
for (let i = 0; i < data.length; i++) {
|
|
||||||
let obj: Record<string, any> = {};
|
|
||||||
const project = data[i];
|
|
||||||
|
|
||||||
Object.entries(project).forEach(([pKey, pValue]: [any, any]) => {
|
|
||||||
Object.values(columns).forEach((col) => {
|
|
||||||
if (pKey === col?.key) {
|
|
||||||
``;
|
|
||||||
obj[col?.label] = handleDataValue(
|
|
||||||
pValue?.includes(",") ? pValue.replaceAll(",", "") : pValue
|
|
||||||
);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
arr.push(obj);
|
|
||||||
}
|
|
||||||
|
|
||||||
const worksheet = XLSX.utils.json_to_sheet(arr);
|
|
||||||
|
|
||||||
const workbook = XLSX.utils.book_new();
|
|
||||||
XLSX.utils.book_append_sheet(workbook, worksheet, "People");
|
|
||||||
|
|
||||||
const excelBuffer = XLSX.write(workbook, {
|
|
||||||
bookType: "xlsx",
|
|
||||||
type: "array",
|
|
||||||
});
|
|
||||||
|
|
||||||
const blob = new Blob([excelBuffer], {
|
|
||||||
type: "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet",
|
|
||||||
});
|
|
||||||
saveAs(blob, "reports.xls");
|
|
||||||
};
|
|
||||||
|
|
||||||
const fetchExcelData = async () => {
|
|
||||||
setExcelLoading(true);
|
|
||||||
const fetchableColumns = columns.filter((c) => !c.computed);
|
|
||||||
const outputFields = fetchableColumns.map((c) => c.apiField ?? c.key);
|
|
||||||
|
|
||||||
const response = await apiService.select({
|
|
||||||
ProcessName: "project",
|
|
||||||
OutputFields: outputFields,
|
|
||||||
Conditions: [
|
|
||||||
["start_date", ">=", selectedDate?.start || null, "and"],
|
|
||||||
["start_date", "<=", selectedDate?.end || null],
|
|
||||||
],
|
|
||||||
});
|
|
||||||
const parsedData = JSON.parse(response.data);
|
|
||||||
setExcelLoading(false);
|
|
||||||
return parsedData;
|
|
||||||
};
|
|
||||||
|
|
||||||
const handleDownloadFile = () => {
|
|
||||||
if (excelLoading) return null;
|
|
||||||
else exportToExcel();
|
|
||||||
};
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<header
|
<header
|
||||||
className={cn(
|
className={cn(
|
||||||
"backdrop-blur-sm border-b border-gray-400/30 h-16 flex items-center justify-between px-4 lg:px-6 shadow-sm relative z-30",
|
"backdrop-blur-sm border-b border-gray-400/30 h-16 flex items-center justify-between px-4 lg:px-6 shadow-sm relative z-30",
|
||||||
className
|
className,
|
||||||
)}
|
)}
|
||||||
>
|
>
|
||||||
{/* Left Section */}
|
{/* Left Section */}
|
||||||
|
|
@ -417,78 +69,24 @@ export function Header({
|
||||||
{/* Page Title */}
|
{/* Page Title */}
|
||||||
<h1 className="text-xl flex items-center justify-center gap-4 font-bold text-white font-persian">
|
<h1 className="text-xl flex items-center justify-center gap-4 font-bold text-white font-persian">
|
||||||
{/* Right-side icon for current page */}
|
{/* Right-side icon for current page */}
|
||||||
{titleIcon ? (
|
{titleIcon ? (
|
||||||
<div className="flex items-center gap-2 mr-4">
|
<div className="flex items-center gap-2 mr-4">
|
||||||
{React.createElement(titleIcon, { className: "w-5 h-5 " })}
|
{React.createElement(titleIcon, { className: "w-5 h-5 " })}
|
||||||
</div>
|
</div>
|
||||||
) : (
|
) : (
|
||||||
<PanelLeft />
|
<PanelLeft />
|
||||||
)}
|
|
||||||
{title.includes("-") ? (
|
|
||||||
<div className="flex row items-center gap-4">
|
|
||||||
<div className="flex items-center gap-1">
|
|
||||||
{title.split("-")[0]}
|
|
||||||
<ChevronLeft className="inline-block w-4 h-4" />
|
|
||||||
{title.split("-")[1]}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
) : (
|
|
||||||
title
|
|
||||||
)}
|
)}
|
||||||
|
{title.includes("-") ? (
|
||||||
|
<span className="flex items-center gap-1">
|
||||||
|
{title.split("-")[0]}
|
||||||
|
<ChevronLeft className="inline-block w-4 h-4" />
|
||||||
|
{title.split("-")[1]}
|
||||||
|
</span>
|
||||||
|
) : (
|
||||||
|
title
|
||||||
|
)}
|
||||||
|
|
||||||
</h1>
|
</h1>
|
||||||
|
|
||||||
<div ref={calendarRef} className="flex flex-col gap-3 relative">
|
|
||||||
<div
|
|
||||||
onClick={toggleCalendar}
|
|
||||||
className="flex flex-row w-full gap-2 items-center border border-pr-gray p-1.5 rounded-md px-2.5 min-w-64 cursor-pointer hover:bg-pr-gray/50 transition-all duration-300"
|
|
||||||
>
|
|
||||||
<Calendar size={20} />
|
|
||||||
{selectedDate ? (
|
|
||||||
<div className="flex flex-row justify-between w-full min-w-36 font-bold gap-1">
|
|
||||||
<div className="flex flex-row gap-1.5 w-max">
|
|
||||||
<span className="text-md">از</span>
|
|
||||||
<span className="text-md">{selectedDate?.sinceMonth}</span>
|
|
||||||
<span className="text-md">
|
|
||||||
{handleDataValue(currentYear.since)}
|
|
||||||
</span>
|
|
||||||
</div>
|
|
||||||
<div className="flex flex-row gap-1.5 w-max">
|
|
||||||
<span className="text-md">تا</span>
|
|
||||||
<span className="text-md">{selectedDate?.fromMonth}</span>
|
|
||||||
<span className="text-md">
|
|
||||||
{handleDataValue(currentYear.until)}
|
|
||||||
</span>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
) : (
|
|
||||||
"تاریخ مورد نظر خود را انتخاب نمایید"
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{openCalendar && (
|
|
||||||
<div className="flex flex-row gap-2.5 absolute top-14 right-[-40px] p-2.5 !pt-3.5 w-80 rounded-3xl overflow-hidden bg-pr-gray border-2 border-[#5F6284]">
|
|
||||||
<CustomCalendar
|
|
||||||
title="از"
|
|
||||||
nextYearHandler={prevFromYearHandler}
|
|
||||||
prevYearHandler={nextFromYearHandler}
|
|
||||||
currentYear={handleDataValue(currentYear?.since)}
|
|
||||||
monthList={monthList}
|
|
||||||
selectedDate={selectedDate?.sinceMonth}
|
|
||||||
selectDateHandler={selectFromDateHandler}
|
|
||||||
/>
|
|
||||||
<span className="w-0.5 h-[12.5rem] border border-[#5F6284] block "></span>
|
|
||||||
<CustomCalendar
|
|
||||||
title="تا"
|
|
||||||
nextYearHandler={prevUntilYearHandler}
|
|
||||||
prevYearHandler={nextUntilYearHandler}
|
|
||||||
currentYear={handleDataValue(currentYear?.until)}
|
|
||||||
monthList={monthList}
|
|
||||||
selectedDate={selectedDate?.fromMonth}
|
|
||||||
selectDateHandler={selectUntilDateHandler}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Right Section */}
|
{/* Right Section */}
|
||||||
|
|
@ -496,29 +94,14 @@ export function Header({
|
||||||
{/* User Menu */}
|
{/* User Menu */}
|
||||||
<div className="relative">
|
<div className="relative">
|
||||||
<div className="flex items-center gap-2">
|
<div className="flex items-center gap-2">
|
||||||
{location.pathname === projectManagerRoute ? (
|
|
||||||
<div className="flex justify-end w-full mb-0 pl-2">
|
|
||||||
<span
|
|
||||||
className={`flex w-full cursor-pointer items-center gap-2 px-3 py-2 text-sm text-gray-300 hover:bg-gradient-to-r hover:from-emerald-500/10 hover:to-teal-500/10 hover:text-emerald-300 font-persian ${excelLoading ? "!cursor-not-allowed !opacity-10" : ""}`}
|
|
||||||
onClick={handleDownloadFile}
|
|
||||||
>
|
|
||||||
<FileChartColumnIncreasing className="h-4 w-4" />
|
|
||||||
دانلود فایل اکسل
|
|
||||||
</span>
|
|
||||||
</div>
|
|
||||||
) : (
|
|
||||||
""
|
|
||||||
)}
|
|
||||||
|
|
||||||
{user?.id === 2041 && (
|
{
|
||||||
<button
|
user?.id === 2041 && <button
|
||||||
className="flex w-full cursor-pointer items-center gap-2 px-3 py-2 text-sm text-gray-300 hover:bg-gradient-to-r hover:from-emerald-500/10 hover:to-teal-500/10 hover:text-emerald-300 font-persian"
|
className="flex w-full cursor-pointer items-center gap-2 px-3 py-2 text-sm text-gray-300 hover:bg-gradient-to-r hover:from-emerald-500/10 hover:to-teal-500/10 hover:text-emerald-300 font-persian"
|
||||||
onClick={redirectHandler}
|
onClick={redirectHandler}>
|
||||||
>
|
|
||||||
<Server className="h-4 w-4" />
|
<Server className="h-4 w-4" />
|
||||||
ورود به میزکار مدیریت
|
ورود به میزکار مدیریت</button>
|
||||||
</button>
|
}
|
||||||
)}
|
|
||||||
|
|
||||||
<Button
|
<Button
|
||||||
variant="ghost"
|
variant="ghost"
|
||||||
|
|
@ -526,6 +109,7 @@ export function Header({
|
||||||
onClick={() => setIsProfileMenuOpen(!isProfileMenuOpen)}
|
onClick={() => setIsProfileMenuOpen(!isProfileMenuOpen)}
|
||||||
className="flex items-center gap-2 text-gray-300"
|
className="flex items-center gap-2 text-gray-300"
|
||||||
>
|
>
|
||||||
|
|
||||||
<div className="hidden sm:block text-right">
|
<div className="hidden sm:block text-right">
|
||||||
<div className="text-sm font-medium font-persian">
|
<div className="text-sm font-medium font-persian">
|
||||||
{user?.name} {user?.family}
|
{user?.name} {user?.family}
|
||||||
|
|
@ -534,12 +118,11 @@ export function Header({
|
||||||
{user?.username}
|
{user?.username}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div className="w-8 h-8 bg-gradient-to-r from-emerald-500/20 to-teal-500/20 text-emerald-400 rounded-lg flex items-center justify-center">
|
<div className="w-8 h-8 bg-gradient-to-r from-emerald-500/20 to-teal-500/20 text-emerald-400 rounded-lg flex items-center justify-center">
|
||||||
<User className="h-4 w-4" />
|
<User className="h-4 w-4" />
|
||||||
</div>
|
</div>
|
||||||
</Button>
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Profile Dropdown */}
|
{/* Profile Dropdown */}
|
||||||
{isProfileMenuOpen && (
|
{isProfileMenuOpen && (
|
||||||
<div className="absolute left-0 top-full mt-2 w-48 bg-gray-800 border border-emerald-500/30 rounded-lg shadow-lg z-50">
|
<div className="absolute left-0 top-full mt-2 w-48 bg-gray-800 border border-emerald-500/30 rounded-lg shadow-lg z-50">
|
||||||
|
|
@ -551,7 +134,7 @@ export function Header({
|
||||||
{user?.email}
|
{user?.email}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
{/* <div className="py-1">
|
<div className="py-1">
|
||||||
<Link
|
<Link
|
||||||
to="/dashboard/profile"
|
to="/dashboard/profile"
|
||||||
className="flex items-center gap-2 px-3 py-2 text-sm text-gray-300 hover:bg-gradient-to-r hover:from-emerald-500/10 hover:to-teal-500/10 hover:text-emerald-300 font-persian"
|
className="flex items-center gap-2 px-3 py-2 text-sm text-gray-300 hover:bg-gradient-to-r hover:from-emerald-500/10 hover:to-teal-500/10 hover:text-emerald-300 font-persian"
|
||||||
|
|
@ -559,16 +142,16 @@ export function Header({
|
||||||
>
|
>
|
||||||
<User className="h-4 w-4" />
|
<User className="h-4 w-4" />
|
||||||
پروفایل کاربری
|
پروفایل کاربری
|
||||||
</Link>
|
</Link>
|
||||||
<Link
|
<Link
|
||||||
to="/dashboard/settings"
|
to="/dashboard/settings"
|
||||||
className="flex items-center gap-2 px-3 py-2 text-sm text-gray-300 hover:bg-gradient-to-r hover:from-emerald-500/10 hover:to-teal-500/10 hover:text-emerald-300 font-persian"
|
className="flex items-center gap-2 px-3 py-2 text-sm text-gray-300 hover:bg-gradient-to-r hover:from-emerald-500/10 hover:to-teal-500/10 hover:text-emerald-300 font-persian"
|
||||||
onClick={() => setIsProfileMenuOpen(false)}
|
onClick={() => setIsProfileMenuOpen(false)}
|
||||||
>
|
>
|
||||||
<Settings className="h-4 w-4" />
|
<Settings className="h-4 w-4" />
|
||||||
تنظیمات
|
تنظیمات
|
||||||
</Link>
|
</Link>
|
||||||
</div> */}
|
</div>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
|
|
||||||
|
|
@ -1,8 +1,9 @@
|
||||||
import { useState } from "react";
|
import { useState } from "react";
|
||||||
import { cn } from "~/lib/utils";
|
import { cn } from "~/lib/utils";
|
||||||
import { Header } from "./header";
|
|
||||||
import { Sidebar } from "./sidebar";
|
import { Sidebar } from "./sidebar";
|
||||||
|
import { Header } from "./header";
|
||||||
import { StrategicAlignmentPopup } from "./strategic-alignment-popup";
|
import { StrategicAlignmentPopup } from "./strategic-alignment-popup";
|
||||||
|
import apiService from "~/lib/api";
|
||||||
|
|
||||||
interface DashboardLayoutProps {
|
interface DashboardLayoutProps {
|
||||||
children: React.ReactNode;
|
children: React.ReactNode;
|
||||||
|
|
@ -17,14 +18,9 @@ export function DashboardLayout({
|
||||||
}: DashboardLayoutProps) {
|
}: DashboardLayoutProps) {
|
||||||
const [isSidebarCollapsed, setIsSidebarCollapsed] = useState(false);
|
const [isSidebarCollapsed, setIsSidebarCollapsed] = useState(false);
|
||||||
const [isMobileSidebarOpen, setIsMobileSidebarOpen] = useState(false);
|
const [isMobileSidebarOpen, setIsMobileSidebarOpen] = useState(false);
|
||||||
const [isStrategicAlignmentPopupOpen, setIsStrategicAlignmentPopupOpen] =
|
const [isStrategicAlignmentPopupOpen, setIsStrategicAlignmentPopupOpen] = useState(false);
|
||||||
useState(false);
|
const [currentTitle, setCurrentTitle] = useState<string | undefined>(title ?? "صفحه اول");
|
||||||
const [currentTitle, setCurrentTitle] = useState<string | undefined>(
|
const [currentTitleIcon, setCurrentTitleIcon] = useState<React.ComponentType<{ className?: string }> | null | undefined>(undefined);
|
||||||
title ?? "صفحه اول"
|
|
||||||
);
|
|
||||||
const [currentTitleIcon, setCurrentTitleIcon] = useState<
|
|
||||||
React.ComponentType<{ className?: string }> | null | undefined
|
|
||||||
>(undefined);
|
|
||||||
|
|
||||||
const toggleSidebarCollapse = () => {
|
const toggleSidebarCollapse = () => {
|
||||||
setIsSidebarCollapsed(!isSidebarCollapsed);
|
setIsSidebarCollapsed(!isSidebarCollapsed);
|
||||||
|
|
@ -34,6 +30,8 @@ export function DashboardLayout({
|
||||||
setIsMobileSidebarOpen(!isMobileSidebarOpen);
|
setIsMobileSidebarOpen(!isMobileSidebarOpen);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div
|
<div
|
||||||
className="h-screen flex overflow-hidden bg-[linear-gradient(to_bottom_left,#464861,20%,#111628)] relative overflow-x-hidden"
|
className="h-screen flex overflow-hidden bg-[linear-gradient(to_bottom_left,#464861,20%,#111628)] relative overflow-x-hidden"
|
||||||
|
|
@ -57,20 +55,19 @@ export function DashboardLayout({
|
||||||
"fixed inset-y-0 right-0 z-50 flex flex-col lg:static lg:inset-auto lg:translate-x-0 transition-transform duration-300 ease-in-out",
|
"fixed inset-y-0 right-0 z-50 flex flex-col lg:static lg:inset-auto lg:translate-x-0 transition-transform duration-300 ease-in-out",
|
||||||
isMobileSidebarOpen
|
isMobileSidebarOpen
|
||||||
? "translate-x-0"
|
? "translate-x-0"
|
||||||
: "translate-x-full lg:translate-x-0"
|
: "translate-x-full lg:translate-x-0",
|
||||||
)}
|
)}
|
||||||
>
|
>
|
||||||
<Sidebar
|
<Sidebar
|
||||||
isCollapsed={isSidebarCollapsed}
|
isCollapsed={isSidebarCollapsed}
|
||||||
onToggleCollapse={toggleSidebarCollapse}
|
onToggleCollapse={toggleSidebarCollapse}
|
||||||
className="h-full flex-shrink-0 relative z-10"
|
className="h-full flex-shrink-0 relative z-10"
|
||||||
onStrategicAlignmentClick={() =>
|
onStrategicAlignmentClick={() => setIsStrategicAlignmentPopupOpen(true)}
|
||||||
setIsStrategicAlignmentPopupOpen(true)
|
|
||||||
}
|
|
||||||
onTitleChange={(info) => {
|
onTitleChange={(info) => {
|
||||||
setCurrentTitle(info.title);
|
setCurrentTitle(info.title);
|
||||||
setCurrentTitleIcon(info.icon ?? null);
|
setCurrentTitleIcon(info.icon ?? null);
|
||||||
}}
|
}}
|
||||||
|
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|
@ -88,18 +85,15 @@ export function DashboardLayout({
|
||||||
<main
|
<main
|
||||||
className={cn(
|
className={cn(
|
||||||
"flex-1 overflow-x-hidden overflow-y-auto focus:outline-none transition-all duration-300 min-w-0",
|
"flex-1 overflow-x-hidden overflow-y-auto focus:outline-none transition-all duration-300 min-w-0",
|
||||||
className
|
className,
|
||||||
)}
|
)}
|
||||||
>
|
>
|
||||||
<div className="relative h-full min-w-0 w-full z-10 overflow-x-hidden p-5">
|
<div className="relative h-full min-w-0 w-full z-10 overflow-x-hidden">
|
||||||
{children}
|
{children}
|
||||||
</div>
|
</div>
|
||||||
</main>
|
</main>
|
||||||
</div>
|
</div>
|
||||||
<StrategicAlignmentPopup
|
<StrategicAlignmentPopup open={isStrategicAlignmentPopupOpen} onOpenChange={setIsStrategicAlignmentPopupOpen} />
|
||||||
open={isStrategicAlignmentPopupOpen}
|
|
||||||
onOpenChange={setIsStrategicAlignmentPopupOpen}
|
|
||||||
/>
|
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -12,10 +12,10 @@ import {
|
||||||
Zap,
|
Zap,
|
||||||
} from "lucide-react";
|
} from "lucide-react";
|
||||||
import moment from "moment-jalaali";
|
import moment from "moment-jalaali";
|
||||||
import { useCallback, useEffect, useRef, useState } from "react";
|
import { formatNumber } from "~/lib/utils";
|
||||||
|
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";
|
||||||
import { BaseCard } from "~/components/ui/base-card";
|
|
||||||
import { Button } from "~/components/ui/button";
|
import { Button } from "~/components/ui/button";
|
||||||
import { Card, CardContent } from "~/components/ui/card";
|
import { Card, CardContent } from "~/components/ui/card";
|
||||||
import { Checkbox } from "~/components/ui/checkbox";
|
import { Checkbox } from "~/components/ui/checkbox";
|
||||||
|
|
@ -34,10 +34,8 @@ import {
|
||||||
TableHeader,
|
TableHeader,
|
||||||
TableRow,
|
TableRow,
|
||||||
} from "~/components/ui/table";
|
} from "~/components/ui/table";
|
||||||
import { useStoredDate } from "~/hooks/useStoredDate";
|
|
||||||
import apiService from "~/lib/api";
|
import apiService from "~/lib/api";
|
||||||
import { EventBus, formatCurrency, formatNumber } from "~/lib/utils";
|
import { formatCurrency } from "~/lib/utils";
|
||||||
import type { CalendarDate } from "~/types/util.type";
|
|
||||||
import { DashboardLayout } from "../layout";
|
import { DashboardLayout } from "../layout";
|
||||||
|
|
||||||
moment.loadPersian({ usePersianDigits: true });
|
moment.loadPersian({ usePersianDigits: true });
|
||||||
|
|
@ -155,7 +153,7 @@ export function DigitalInnovationPage() {
|
||||||
const [currentPage, setCurrentPage] = useState(1);
|
const [currentPage, setCurrentPage] = useState(1);
|
||||||
const [pageSize] = useState(20);
|
const [pageSize] = useState(20);
|
||||||
const [hasMore, setHasMore] = useState(true);
|
const [hasMore, setHasMore] = useState(true);
|
||||||
const [date, setDate] = useStoredDate();
|
const [totalCount, setTotalCount] = useState(0);
|
||||||
const [actualTotalCount, setActualTotalCount] = useState(0);
|
const [actualTotalCount, setActualTotalCount] = useState(0);
|
||||||
const [statsLoading, setStatsLoading] = useState(false);
|
const [statsLoading, setStatsLoading] = useState(false);
|
||||||
const [rating, setRating] = useState<ListItem[]>([]);
|
const [rating, setRating] = useState<ListItem[]>([]);
|
||||||
|
|
@ -215,7 +213,7 @@ export function DigitalInnovationPage() {
|
||||||
value: formatNumber(stats.reduceCosts.toFixed?.(1) ?? stats.reduceCosts),
|
value: formatNumber(stats.reduceCosts.toFixed?.(1) ?? stats.reduceCosts),
|
||||||
description: "میلیون ریال کاهش یافته",
|
description: "میلیون ریال کاهش یافته",
|
||||||
icon: <TrendingDown />,
|
icon: <TrendingDown />,
|
||||||
color: "text-pr-green",
|
color: "text-emerald-400",
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
id: "bottleneck-removal",
|
id: "bottleneck-removal",
|
||||||
|
|
@ -223,7 +221,7 @@ export function DigitalInnovationPage() {
|
||||||
value: formatNumber(stats.increasedRevenue),
|
value: formatNumber(stats.increasedRevenue),
|
||||||
description: "میلیون ریال افزایش یافته",
|
description: "میلیون ریال افزایش یافته",
|
||||||
icon: <TrendingUp />,
|
icon: <TrendingUp />,
|
||||||
color: "text-pr-green",
|
color: "text-emerald-400",
|
||||||
},
|
},
|
||||||
|
|
||||||
{
|
{
|
||||||
|
|
@ -234,7 +232,7 @@ export function DigitalInnovationPage() {
|
||||||
),
|
),
|
||||||
description: "هزار تن صرفه جوریی شده",
|
description: "هزار تن صرفه جوریی شده",
|
||||||
icon: <Database />,
|
icon: <Database />,
|
||||||
color: "text-pr-green",
|
color: "text-emerald-400",
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
id: "frequent-failures-reduction",
|
id: "frequent-failures-reduction",
|
||||||
|
|
@ -245,7 +243,7 @@ export function DigitalInnovationPage() {
|
||||||
),
|
),
|
||||||
description: "مگاوات کاهش یافته",
|
description: "مگاوات کاهش یافته",
|
||||||
icon: <Zap />,
|
icon: <Zap />,
|
||||||
color: "text-pr-green",
|
color: "text-emerald-400",
|
||||||
},
|
},
|
||||||
];
|
];
|
||||||
|
|
||||||
|
|
@ -284,11 +282,7 @@ export function DigitalInnovationPage() {
|
||||||
"reduce_costs_percent",
|
"reduce_costs_percent",
|
||||||
],
|
],
|
||||||
Sorts: [[sortConfig.field, sortConfig.direction]],
|
Sorts: [[sortConfig.field, sortConfig.direction]],
|
||||||
Conditions: [
|
Conditions: [["type_of_innovation", "=", "نوآوری دیجیتال"]],
|
||||||
["type_of_innovation", "=", "نوآوری دیجیتال", "and"],
|
|
||||||
["start_date", ">=", date?.start || null, "and"],
|
|
||||||
["start_date", "<=", date?.end || null],
|
|
||||||
],
|
|
||||||
Pagination: { PageNumber: pageToFetch, PageSize: pageSize },
|
Pagination: { PageNumber: pageToFetch, PageSize: pageSize },
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
@ -301,16 +295,16 @@ export function DigitalInnovationPage() {
|
||||||
if (reset) {
|
if (reset) {
|
||||||
setProjects(parsedData);
|
setProjects(parsedData);
|
||||||
// calculateAverage(parsedData);
|
// calculateAverage(parsedData);
|
||||||
// setTotalCount(parsedData.length);
|
setTotalCount(parsedData.length);
|
||||||
} else {
|
} else {
|
||||||
setProjects((prev) => [...prev, ...parsedData]);
|
setProjects((prev) => [...prev, ...parsedData]);
|
||||||
// setTotalCount((prev) => prev + parsedData.length);
|
setTotalCount((prev) => prev + parsedData.length);
|
||||||
}
|
}
|
||||||
setHasMore(parsedData.length === pageSize);
|
setHasMore(parsedData.length === pageSize);
|
||||||
} else {
|
} else {
|
||||||
if (reset) {
|
if (reset) {
|
||||||
setProjects([]);
|
setProjects([]);
|
||||||
// setTotalCount(0);
|
setTotalCount(0);
|
||||||
}
|
}
|
||||||
setHasMore(false);
|
setHasMore(false);
|
||||||
}
|
}
|
||||||
|
|
@ -318,14 +312,14 @@ export function DigitalInnovationPage() {
|
||||||
console.error("Error parsing project data:", parseError);
|
console.error("Error parsing project data:", parseError);
|
||||||
if (reset) {
|
if (reset) {
|
||||||
setProjects([]);
|
setProjects([]);
|
||||||
// setTotalCount(0);
|
setTotalCount(0);
|
||||||
}
|
}
|
||||||
setHasMore(false);
|
setHasMore(false);
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
if (reset) {
|
if (reset) {
|
||||||
setProjects([]);
|
setProjects([]);
|
||||||
// setTotalCount(0);
|
setTotalCount(0);
|
||||||
}
|
}
|
||||||
setHasMore(false);
|
setHasMore(false);
|
||||||
}
|
}
|
||||||
|
|
@ -333,7 +327,7 @@ export function DigitalInnovationPage() {
|
||||||
toast.error(response.message || "خطا در دریافت اطلاعات پروژهها");
|
toast.error(response.message || "خطا در دریافت اطلاعات پروژهها");
|
||||||
if (reset) {
|
if (reset) {
|
||||||
setProjects([]);
|
setProjects([]);
|
||||||
// setTotalCount(0);
|
setTotalCount(0);
|
||||||
}
|
}
|
||||||
setHasMore(false);
|
setHasMore(false);
|
||||||
}
|
}
|
||||||
|
|
@ -342,7 +336,7 @@ export function DigitalInnovationPage() {
|
||||||
toast.error("خطا در دریافت اطلاعات پروژهها");
|
toast.error("خطا در دریافت اطلاعات پروژهها");
|
||||||
if (reset) {
|
if (reset) {
|
||||||
setProjects([]);
|
setProjects([]);
|
||||||
// setTotalCount(0);
|
setTotalCount(0);
|
||||||
}
|
}
|
||||||
setHasMore(false);
|
setHasMore(false);
|
||||||
} finally {
|
} finally {
|
||||||
|
|
@ -360,27 +354,13 @@ export function DigitalInnovationPage() {
|
||||||
}, [hasMore, loading, loadingMore]);
|
}, [hasMore, loading, loadingMore]);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (date?.start && date?.end) {
|
fetchTable(true);
|
||||||
fetchTable(true);
|
fetchTotalCount();
|
||||||
fetchTotalCount();
|
fetchStats();
|
||||||
fetchStats();
|
}, [sortConfig]);
|
||||||
}
|
|
||||||
}, [sortConfig, date]);
|
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const handler = (date: CalendarDate) => {
|
if (currentPage > 1) {
|
||||||
if (date) setDate(date);
|
|
||||||
};
|
|
||||||
|
|
||||||
EventBus.on("dateSelected", handler);
|
|
||||||
|
|
||||||
return () => {
|
|
||||||
EventBus.off("dateSelected", handler);
|
|
||||||
};
|
|
||||||
}, []);
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
if (currentPage > 1 && date?.start && date?.end) {
|
|
||||||
fetchTable(false);
|
fetchTable(false);
|
||||||
}
|
}
|
||||||
}, [currentPage]);
|
}, [currentPage]);
|
||||||
|
|
@ -390,8 +370,7 @@ export function DigitalInnovationPage() {
|
||||||
const scrollContainer = scrollContainerRef.current;
|
const scrollContainer = scrollContainerRef.current;
|
||||||
|
|
||||||
const handleScroll = () => {
|
const handleScroll = () => {
|
||||||
if (!scrollContainer || !hasMore || loadingMore || fetchingRef.current)
|
if (!scrollContainer || !hasMore || loadingMore || fetchingRef.current) return;
|
||||||
return;
|
|
||||||
|
|
||||||
// Clear previous timeout
|
// Clear previous timeout
|
||||||
if (scrollTimeoutRef.current) {
|
if (scrollTimeoutRef.current) {
|
||||||
|
|
@ -411,9 +390,7 @@ export function DigitalInnovationPage() {
|
||||||
};
|
};
|
||||||
|
|
||||||
if (scrollContainer) {
|
if (scrollContainer) {
|
||||||
scrollContainer.addEventListener("scroll", handleScroll, {
|
scrollContainer.addEventListener("scroll", handleScroll, { passive: true });
|
||||||
passive: true,
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return () => {
|
return () => {
|
||||||
|
|
@ -433,23 +410,19 @@ export function DigitalInnovationPage() {
|
||||||
direction:
|
direction:
|
||||||
prev.field === field && prev.direction === "asc" ? "desc" : "asc",
|
prev.field === field && prev.direction === "asc" ? "desc" : "asc",
|
||||||
}));
|
}));
|
||||||
fetchTotalCount(date?.start, date?.end);
|
fetchTotalCount();
|
||||||
fetchStats();
|
fetchStats();
|
||||||
setCurrentPage(1);
|
setCurrentPage(1);
|
||||||
setProjects([]);
|
setProjects([]);
|
||||||
setHasMore(true);
|
setHasMore(true);
|
||||||
};
|
};
|
||||||
|
|
||||||
const fetchTotalCount = async (startDate?: string, endDate?: string) => {
|
const fetchTotalCount = async () => {
|
||||||
try {
|
try {
|
||||||
const response = await apiService.select({
|
const response = await apiService.select({
|
||||||
ProcessName: "project",
|
ProcessName: "project",
|
||||||
OutputFields: ["count(project_no)"],
|
OutputFields: ["count(project_no)"],
|
||||||
Conditions: [
|
Conditions: [["type_of_innovation", "=", "نوآوری دیجیتال"]],
|
||||||
["type_of_innovation", "=", "نوآوری دیجیتال", "and"],
|
|
||||||
["start_date", ">=", date?.start || null, "and"],
|
|
||||||
["start_date", "<=", date?.end || null],
|
|
||||||
],
|
|
||||||
});
|
});
|
||||||
|
|
||||||
if (response.state === 0) {
|
if (response.state === 0) {
|
||||||
|
|
@ -476,18 +449,17 @@ export function DigitalInnovationPage() {
|
||||||
try {
|
try {
|
||||||
setStatsLoading(true);
|
setStatsLoading(true);
|
||||||
const raw = await apiService.call<any>({
|
const raw = await apiService.call<any>({
|
||||||
innovation_digital_function: {
|
innovation_digital_function: {},
|
||||||
start_date: date?.start || null,
|
|
||||||
end_date: date?.end || null,
|
|
||||||
},
|
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
// 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 {}
|
||||||
// }
|
// }
|
||||||
|
|
||||||
|
|
@ -497,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];
|
||||||
|
|
@ -510,6 +482,8 @@ 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;
|
||||||
|
|
@ -557,33 +531,33 @@ export function DigitalInnovationPage() {
|
||||||
// fetchStats();
|
// fetchStats();
|
||||||
// };
|
// };
|
||||||
|
|
||||||
// const renderProgress = useMemo(() => {
|
const renderProgress = useMemo(() => {
|
||||||
// const total = 10;
|
const total = 10;
|
||||||
// for (let i = 0; i < rating.length; i++) {
|
for (let i = 0; i < rating.length; i++) {
|
||||||
// const currentElm = rating[i];
|
const currentElm = rating[i];
|
||||||
// currentElm.house = [];
|
currentElm.house = [];
|
||||||
// const greenBoxes = Math.floor((total * currentElm.development) / 100);
|
const greenBoxes = Math.floor((total * currentElm.development) / 100);
|
||||||
// const partialPercent =
|
const partialPercent =
|
||||||
// (total * currentElm.development) / 100 - greenBoxes;
|
(total * currentElm.development) / 100 - greenBoxes;
|
||||||
// for (let j = 0; j < greenBoxes; j++) {
|
for (let j = 0; j < greenBoxes; j++) {
|
||||||
// currentElm.house.push({
|
currentElm.house.push({
|
||||||
// index: j,
|
index: j,
|
||||||
// color: "!bg-emerald-400",
|
color: "!bg-emerald-400",
|
||||||
// });
|
});
|
||||||
// }
|
}
|
||||||
// if (partialPercent != 0 && greenBoxes != 10)
|
if (partialPercent != 0 && greenBoxes != 10)
|
||||||
// currentElm.house.push({
|
currentElm.house.push({
|
||||||
// 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%
|
||||||
// )`,
|
)`,
|
||||||
// });
|
});
|
||||||
// }
|
}
|
||||||
// }, [rating]);
|
}, [rating]);
|
||||||
|
|
||||||
const statusColor = (status: projectStatus): any => {
|
const statusColor = (status: projectStatus): any => {
|
||||||
let el = null;
|
let el = null;
|
||||||
|
|
@ -627,14 +601,14 @@ export function DigitalInnovationPage() {
|
||||||
variant="ghost"
|
variant="ghost"
|
||||||
size="sm"
|
size="sm"
|
||||||
onClick={() => handleProjectDetails(item)}
|
onClick={() => handleProjectDetails(item)}
|
||||||
className="text-pr-green hover:text-pr-green underline-offset-4 underline font-normal hover:bg-emerald-500/20 p-2 h-auto"
|
className="text-emerald-400 hover:text-emerald-300 hover:bg-emerald-500/20 p-2 h-auto cursor-pointer"
|
||||||
>
|
>
|
||||||
جزئیات بیشتر
|
جزئیات بیشتر
|
||||||
</Button>
|
</Button>
|
||||||
);
|
);
|
||||||
case "amount_currency_reduction":
|
case "amount_currency_reduction":
|
||||||
return (
|
return (
|
||||||
<span className="font-medium text-pr-green">
|
<span className="font-medium text-emerald-400">
|
||||||
{formatCurrency(String(value))}
|
{formatCurrency(String(value))}
|
||||||
</span>
|
</span>
|
||||||
);
|
);
|
||||||
|
|
@ -645,9 +619,7 @@ export function DigitalInnovationPage() {
|
||||||
</Badge>
|
</Badge>
|
||||||
);
|
);
|
||||||
case "title":
|
case "title":
|
||||||
return (
|
return <span className="font-medium text-white">{String(value)}</span>;
|
||||||
<span className="font-light 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">
|
||||||
|
|
@ -682,7 +654,7 @@ export function DigitalInnovationPage() {
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<DashboardLayout title="نوآوری دیجیتال">
|
<DashboardLayout title="نوآوری دیجیتال">
|
||||||
<div className="space-y-4 grid justify-between gap-7 pl-6 sm:grid-cols-1 xl:grid-cols-[40%_60%]">
|
<div className="p-6 space-y-4 grid justify-between gap-8 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">
|
||||||
|
|
@ -739,7 +711,7 @@ export function DigitalInnovationPage() {
|
||||||
</div>
|
</div>
|
||||||
<div className="flex items-center justify-center flex-col p-2 pb-4">
|
<div className="flex items-center justify-center flex-col p-2 pb-4">
|
||||||
<p
|
<p
|
||||||
className={`text-3xl font-bold ${card.color} mb-1`}
|
className={`text-3xl font-bold ${card.color} mb-1`}
|
||||||
>
|
>
|
||||||
{card.value}
|
{card.value}
|
||||||
</p>
|
</p>
|
||||||
|
|
@ -755,49 +727,50 @@ export function DigitalInnovationPage() {
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Process Impacts Chart */}
|
{/* Process Impacts Chart */}
|
||||||
<BaseCard className="rounded-xl w-full overflow-hidden">
|
<Card className="bg-[linear-gradient(to_bottom_left,#464861,50%,#111628)] backdrop-blur-sm rounded-lg w-full overflow-hidden h-full ">
|
||||||
{/* <CardContent > */}
|
{/* <CardContent > */}
|
||||||
<CustomBarChart
|
<CustomBarChart
|
||||||
title="تاثیرات نوآوری دیجیتال به صورت درصد مقایسه ای"
|
title="تاثیرات نوآوری دیجیتال به صورت درصد مقایسه ای"
|
||||||
loading={statsLoading}
|
loading={statsLoading}
|
||||||
// height="100%"
|
height="100%"
|
||||||
data={[
|
data={[
|
||||||
{
|
{
|
||||||
label: DigitalCardLabel.decreasCost,
|
label: DigitalCardLabel.decreasCost,
|
||||||
value: stats.reduceCostsPercent || 0,
|
value: stats.reduceCostsPercent || 0,
|
||||||
color: "bg-pr-green",
|
color: "bg-emerald-400",
|
||||||
labelColor: "text-white",
|
labelColor: "text-white",
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
label: DigitalCardLabel.increaseRevenue,
|
label: DigitalCardLabel.increaseRevenue,
|
||||||
value: stats.increasedRevenuePercent || 0,
|
value: stats.increasedRevenuePercent || 0,
|
||||||
color: "bg-pr-green",
|
color: "bg-emerald-400",
|
||||||
labelColor: "text-white",
|
labelColor: "text-white",
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
label: DigitalCardLabel.performance,
|
label: DigitalCardLabel.performance,
|
||||||
value: stats.resourceProductivityPercent || 0,
|
value: stats.resourceProductivityPercent || 0,
|
||||||
color: "bg-pr-green",
|
color: "bg-emerald-400",
|
||||||
labelColor: "text-white",
|
labelColor: "text-white",
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
label: DigitalCardLabel.decreaseEnergy,
|
label: DigitalCardLabel.decreaseEnergy,
|
||||||
value: stats.reduceEnergyConsumptionPercent || 0,
|
value: stats.reduceEnergyConsumptionPercent || 0,
|
||||||
color: "bg-pr-green",
|
color: "bg-emerald-400",
|
||||||
labelColor: "text-white",
|
labelColor: "text-white",
|
||||||
},
|
},
|
||||||
]}
|
]}
|
||||||
barHeight="h-5"
|
barHeight="h-5"
|
||||||
showAxisLabels={true}
|
showAxisLabels={true}
|
||||||
/>
|
/>
|
||||||
</BaseCard>
|
{/* </CardContent> */}
|
||||||
|
</Card>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Data Table */}
|
{/* Data Table */}
|
||||||
<Card className="bg-transparent backdrop-blur-sm rounded-lg overflow-hidden w-full h-max">
|
<Card className="bg-transparent backdrop-blur-sm rounded-lg overflow-hidden w-full h-[39.7rem]">
|
||||||
<CardContent className="p-0">
|
<CardContent className="p-0">
|
||||||
<div className="relative h-full">
|
<div className="relative h-full">
|
||||||
<Table containerClassName="overflow-auto custom-scrollbar w-full h-[calc(100vh-160px)] ">
|
<Table containerClassName="overflow-auto custom-scrollbar w-full h-[36.8rem] ">
|
||||||
<TableHeader>
|
<TableHeader>
|
||||||
<TableRow className="bg-[#3F415A]">
|
<TableRow className="bg-[#3F415A]">
|
||||||
{columns.map((column) => (
|
{columns.map((column) => (
|
||||||
|
|
@ -976,13 +949,13 @@ export function DigitalInnovationPage() {
|
||||||
|
|
||||||
{/* Project Details Dialog */}
|
{/* Project Details Dialog */}
|
||||||
<Dialog open={detailsDialogOpen} onOpenChange={setDetailsDialogOpen}>
|
<Dialog open={detailsDialogOpen} onOpenChange={setDetailsDialogOpen}>
|
||||||
<DialogContent className="bg-[linear-gradient(to_bottom_left,#464861,50%,#111628)] max-w-6xl max-h-[80vh] overflow-y-auto">
|
<DialogContent className="bg-[linear-gradient(to_bottom_left,#464861,50%,#111628)] max-w-5xl max-h-[80vh] overflow-y-auto">
|
||||||
<DialogHeader>
|
<DialogHeader>
|
||||||
<DialogTitle className="text-white mr-4 border-b-2 border-gray-600 pb-4 font-persian text-right">
|
<DialogTitle className="text-white mr-4 border-b-2 border-gray-600 pb-4 font-persian text-right">
|
||||||
شرح پروژه
|
شرح پروژه
|
||||||
</DialogTitle>
|
</DialogTitle>
|
||||||
</DialogHeader>
|
</DialogHeader>
|
||||||
<div className="body grid grid-cols-[40%_20%_40%] pb-6">
|
<div className="body grid grid-cols-[40%_20%_40%]">
|
||||||
<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}
|
||||||
|
|
@ -1034,7 +1007,7 @@ export function DigitalInnovationPage() {
|
||||||
</div>
|
</div>
|
||||||
<div className="digitalAbilityDevelopment flex flex-col gap-10 border-l-2 border-l-gray-600 px-5">
|
<div className="digitalAbilityDevelopment flex flex-col gap-10 border-l-2 border-l-gray-600 px-5">
|
||||||
<div className="flex flex-col gap-4">
|
<div className="flex flex-col gap-4">
|
||||||
<span className="text-lg font-bold">
|
<span className="text-md font-bold">
|
||||||
توسعه قابلیت های دیجیتال:{" "}
|
توسعه قابلیت های دیجیتال:{" "}
|
||||||
</span>
|
</span>
|
||||||
<div className="flex flex-col gap-2">
|
<div className="flex flex-col gap-2">
|
||||||
|
|
@ -1097,10 +1070,10 @@ export function DigitalInnovationPage() {
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div className="flex flex-col px-6 gap-4">
|
<div className="flex flex-col pr-7 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="text-sm 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 ">
|
||||||
کاهش هزینه ها
|
کاهش هزینه ها
|
||||||
</span>
|
</span>
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -1,4 +1,6 @@
|
||||||
|
// 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,
|
||||||
|
|
@ -25,7 +27,6 @@ import {
|
||||||
TableHeader,
|
TableHeader,
|
||||||
TableRow,
|
TableRow,
|
||||||
} from "~/components/ui/table";
|
} from "~/components/ui/table";
|
||||||
import { EventBus, formatNumber } from "~/lib/utils";
|
|
||||||
|
|
||||||
import {
|
import {
|
||||||
Building2,
|
Building2,
|
||||||
|
|
@ -42,17 +43,12 @@ import {
|
||||||
UsersIcon,
|
UsersIcon,
|
||||||
Zap,
|
Zap,
|
||||||
} from "lucide-react";
|
} from "lucide-react";
|
||||||
import moment from "moment-jalaali";
|
|
||||||
import toast from "react-hot-toast";
|
import toast from "react-hot-toast";
|
||||||
import { MetricCard } from "~/components/ui/metric-card";
|
|
||||||
import { useStoredDate } from "~/hooks/useStoredDate";
|
|
||||||
import apiService from "~/lib/api";
|
import apiService from "~/lib/api";
|
||||||
import { formatCurrency } from "~/lib/utils";
|
import { formatCurrency } from "~/lib/utils";
|
||||||
import type { CalendarDate } from "~/types/util.type";
|
|
||||||
import DashboardLayout from "../layout";
|
import DashboardLayout from "../layout";
|
||||||
|
|
||||||
moment.loadPersian({ usePersianDigits: true });
|
// moment.loadPersian({ usePersianDigits: true });
|
||||||
|
|
||||||
interface GreenInnovationData {
|
interface GreenInnovationData {
|
||||||
WorkflowID: string;
|
WorkflowID: string;
|
||||||
approved_budget: string;
|
approved_budget: string;
|
||||||
|
|
@ -170,8 +166,6 @@ export function GreenInnovationPage() {
|
||||||
const [totalCount, setTotalCount] = useState(0);
|
const [totalCount, setTotalCount] = useState(0);
|
||||||
const [actualTotalCount, setActualTotalCount] = useState(0);
|
const [actualTotalCount, setActualTotalCount] = useState(0);
|
||||||
const [statsLoading, setStatsLoading] = useState(false);
|
const [statsLoading, setStatsLoading] = useState(false);
|
||||||
const [date, setDate] = useStoredDate();
|
|
||||||
|
|
||||||
const [stats, setStats] = useState<stateCounter>();
|
const [stats, setStats] = useState<stateCounter>();
|
||||||
const [sortConfig, setSortConfig] = useState<SortConfig>({
|
const [sortConfig, setSortConfig] = useState<SortConfig>({
|
||||||
field: "start_date",
|
field: "start_date",
|
||||||
|
|
@ -294,11 +288,7 @@ export function GreenInnovationPage() {
|
||||||
"observer",
|
"observer",
|
||||||
],
|
],
|
||||||
Sorts: [[sortConfig.field, sortConfig.direction]],
|
Sorts: [[sortConfig.field, sortConfig.direction]],
|
||||||
Conditions: [
|
Conditions: [["type_of_innovation", "=", "نوآوری سبز"]],
|
||||||
["type_of_innovation", "=", "نوآوری سبز", "and"],
|
|
||||||
["start_date", ">=", date?.start || null, "and"],
|
|
||||||
["start_date", "<=", date?.end || null],
|
|
||||||
],
|
|
||||||
Pagination: { PageNumber: pageToFetch, PageSize: pageSize },
|
Pagination: { PageNumber: pageToFetch, PageSize: pageSize },
|
||||||
});
|
});
|
||||||
if (response.state === 0) {
|
if (response.state === 0) {
|
||||||
|
|
@ -360,18 +350,6 @@ export function GreenInnovationPage() {
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
const handler = (date: CalendarDate) => {
|
|
||||||
if (date) setDate(date);
|
|
||||||
};
|
|
||||||
|
|
||||||
EventBus.on("dateSelected", handler);
|
|
||||||
|
|
||||||
return () => {
|
|
||||||
EventBus.off("dateSelected", handler);
|
|
||||||
};
|
|
||||||
}, []);
|
|
||||||
|
|
||||||
const loadMore = useCallback(() => {
|
const loadMore = useCallback(() => {
|
||||||
if (hasMore && !loading) {
|
if (hasMore && !loading) {
|
||||||
setCurrentPage((prev) => prev + 1);
|
setCurrentPage((prev) => prev + 1);
|
||||||
|
|
@ -379,15 +357,13 @@ export function GreenInnovationPage() {
|
||||||
}, [hasMore, loading]);
|
}, [hasMore, loading]);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (date.end && date.start) {
|
fetchProjects(true);
|
||||||
fetchProjects(true);
|
fetchTotalCount();
|
||||||
fetchTotalCount();
|
}, [sortConfig]);
|
||||||
}
|
|
||||||
}, [sortConfig, date]);
|
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (date.end && date.start) fetchStats();
|
fetchStats();
|
||||||
}, [selectedProjects, date]);
|
}, [selectedProjects]);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (currentPage > 1) {
|
if (currentPage > 1) {
|
||||||
|
|
@ -440,11 +416,7 @@ export function GreenInnovationPage() {
|
||||||
const response = await apiService.select({
|
const response = await apiService.select({
|
||||||
ProcessName: "project",
|
ProcessName: "project",
|
||||||
OutputFields: ["count(project_no)"],
|
OutputFields: ["count(project_no)"],
|
||||||
Conditions: [
|
Conditions: [["type_of_innovation", "=", "نوآوری سبز"]],
|
||||||
["type_of_innovation", "=", "نوآوری سبز", "and"],
|
|
||||||
["start_date", ">=", date?.start || null, "and"],
|
|
||||||
["start_date", "<=", date?.end || null],
|
|
||||||
],
|
|
||||||
});
|
});
|
||||||
if (response.state === 0) {
|
if (response.state === 0) {
|
||||||
const dataString = response.data;
|
const dataString = response.data;
|
||||||
|
|
@ -476,8 +448,6 @@ export function GreenInnovationPage() {
|
||||||
selectedProjects.size > 0
|
selectedProjects.size > 0
|
||||||
? Array.from(selectedProjects).join(" , ")
|
? Array.from(selectedProjects).join(" , ")
|
||||||
: "",
|
: "",
|
||||||
start_date: date?.start || null,
|
|
||||||
end_date: date?.end || null,
|
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
let payload: any = raw?.data;
|
let payload: any = raw?.data;
|
||||||
|
|
@ -524,13 +494,13 @@ export function GreenInnovationPage() {
|
||||||
},
|
},
|
||||||
|
|
||||||
pollution: {
|
pollution: {
|
||||||
value: parseNum(stats.pollution_reduction),
|
value: formatNumber(parseNum(stats.pollution_reduction)),
|
||||||
percent: parseNum(stats.pollution_reduction_percent),
|
percent: formatNumber(parseNum(stats.pollution_reduction_percent)),
|
||||||
},
|
},
|
||||||
|
|
||||||
waste: {
|
waste: {
|
||||||
value: parseNum(stats.waste_reduction),
|
value: formatNumber(parseNum(stats.waste_reduction)),
|
||||||
percent: parseNum(stats.waste_reductionn_percent),
|
percent: formatNumber(parseNum(stats.waste_reductionn_percent)),
|
||||||
},
|
},
|
||||||
avarage: stats.average_project_score,
|
avarage: stats.average_project_score,
|
||||||
countInnovationGreenProjects: stats.count_innovation_green_projects,
|
countInnovationGreenProjects: stats.count_innovation_green_projects,
|
||||||
|
|
@ -548,6 +518,7 @@ export function GreenInnovationPage() {
|
||||||
setStatsLoading(false);
|
setStatsLoading(false);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
const setPageData = (normalized: any) => {
|
const setPageData = (normalized: any) => {
|
||||||
setSustainabilityStats((prev) => ({
|
setSustainabilityStats((prev) => ({
|
||||||
...prev,
|
...prev,
|
||||||
|
|
@ -631,14 +602,14 @@ export function GreenInnovationPage() {
|
||||||
variant="ghost"
|
variant="ghost"
|
||||||
size="sm"
|
size="sm"
|
||||||
onClick={() => handleProjectDetails(item)}
|
onClick={() => handleProjectDetails(item)}
|
||||||
className="text-pr-green hover:text-pr-green underline-offset-4 underline font-normal hover:bg-emerald-500/20 p-2 h-auto"
|
className="text-emerald-400 hover:text-emerald-300 hover:bg-emerald-500/20 p-2 h-auto cursor-pointer"
|
||||||
>
|
>
|
||||||
جزئیات بیشتر
|
جزئیات بیشتر
|
||||||
</Button>
|
</Button>
|
||||||
);
|
);
|
||||||
case "amount_currency_reduction":
|
case "amount_currency_reduction":
|
||||||
return (
|
return (
|
||||||
<span className="font-medium text-pr-green">
|
<span className="font-medium text-emerald-400">
|
||||||
{formatCurrency(String(value))}
|
{formatCurrency(String(value))}
|
||||||
</span>
|
</span>
|
||||||
);
|
);
|
||||||
|
|
@ -649,9 +620,7 @@ export function GreenInnovationPage() {
|
||||||
</Badge>
|
</Badge>
|
||||||
);
|
);
|
||||||
case "title":
|
case "title":
|
||||||
return (
|
return <span className="font-medium text-white">{String(value)}</span>;
|
||||||
<span className="font-light 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">
|
||||||
|
|
@ -717,7 +686,7 @@ export function GreenInnovationPage() {
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<DashboardLayout title="نوآوری سبز">
|
<DashboardLayout title="نوآوری سبز">
|
||||||
<div className="space-y-4 h-[23.5rem]">
|
<div className="p-6 space-y-4 h-[23.5rem]">
|
||||||
{/* Stats Cards */}
|
{/* Stats Cards */}
|
||||||
<div className="flex gap-6 mb-5 md:flex-col xl:flex-row">
|
<div className="flex gap-6 mb-5 md:flex-col xl:flex-row">
|
||||||
<div className="flex flex-col justify-between xl:w-1/2 sm:w-full sm:gap-2">
|
<div className="flex flex-col justify-between xl:w-1/2 sm:w-full sm:gap-2">
|
||||||
|
|
@ -751,14 +720,39 @@ export function GreenInnovationPage() {
|
||||||
</Card>
|
</Card>
|
||||||
))
|
))
|
||||||
: Object.entries(sustainabilityStats).map(([key, value]) => (
|
: Object.entries(sustainabilityStats).map(([key, value]) => (
|
||||||
<MetricCard
|
<Card
|
||||||
key={key}
|
key={key}
|
||||||
title={value.title}
|
className="bg-[linear-gradient(to_bottom_left,#464861,50%,#111628)] rounded-lg backdrop-blur-sm border-gray-700/50"
|
||||||
value={Math.round(value.total.value || 0)}
|
>
|
||||||
valueLabel={value.total?.description}
|
<CardContent className="p-0 h-full">
|
||||||
percentValue={value.percent?.value || 0}
|
<div className="flex flex-col justify-between gap-2 h-full">
|
||||||
percentLabel={value.percent?.description}
|
<div className="flex justify-between items-center border-b-2 border-gray-500/20 ">
|
||||||
/>
|
<h3 className="text-lg font-bold text-white font-persian p-4">
|
||||||
|
{value.title}
|
||||||
|
</h3>
|
||||||
|
</div>
|
||||||
|
<div className="flex items-center justify-between p-6 flex-row-reverse">
|
||||||
|
<div className="flex flex-col">
|
||||||
|
<span className="text-3xl font-bold text-emerald-400 mb-1 font-persian">
|
||||||
|
% {value.percent?.value}
|
||||||
|
</span>
|
||||||
|
<span className="text-sm text-gray-400 font-persian">
|
||||||
|
{value.percent?.description}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
<b className="block w-0.5 h-8 bg-gray-600 rotate-45" />
|
||||||
|
<div className="flex flex-col">
|
||||||
|
<span className="text-3xl font-bold text-emerald-400 mb-1 font-persian">
|
||||||
|
{value.total?.value}
|
||||||
|
</span>
|
||||||
|
<span className="text-sm text-gray-400 font-persian">
|
||||||
|
{value.total?.description}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</CardContent>
|
||||||
|
</Card>
|
||||||
))}
|
))}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|
@ -819,10 +813,7 @@ 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
|
<div key={index} className="param flex flex-row justify-between items-center">
|
||||||
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">
|
||||||
|
|
@ -904,7 +895,7 @@ export function GreenInnovationPage() {
|
||||||
</Card>
|
</Card>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
<Card className="w-1/2 bg-pr-gray backdrop-blur-sm rounded-lg overflow-hidden">
|
<Card className="w-1/3 bg-[linear-gradient(to_bottom_left,#464861,50%,#111628)] backdrop-blur-sm rounded-lg overflow-hidden">
|
||||||
<CardContent className="p-0">
|
<CardContent className="p-0">
|
||||||
<div className="border-b-2 border-gray-500/20">
|
<div className="border-b-2 border-gray-500/20">
|
||||||
<div className="flex flex-row justify-between w-full p-4">
|
<div className="flex flex-row justify-between w-full p-4">
|
||||||
|
|
@ -955,7 +946,7 @@ export function GreenInnovationPage() {
|
||||||
<Card className="bg-transparent backdrop-blur-sm rounded-lg overflow-hidden">
|
<Card className="bg-transparent backdrop-blur-sm rounded-lg overflow-hidden">
|
||||||
<CardContent className="p-0">
|
<CardContent className="p-0">
|
||||||
<div className="relative">
|
<div className="relative">
|
||||||
<Table containerClassName="overflow-auto custom-scrollbar h-full">
|
<Table containerClassName="overflow-auto custom-scrollbar h-[25rem]">
|
||||||
<TableHeader>
|
<TableHeader>
|
||||||
<TableRow className="bg-[#3F415A]">
|
<TableRow className="bg-[#3F415A]">
|
||||||
{columns.map((column) => (
|
{columns.map((column) => (
|
||||||
|
|
@ -1119,7 +1110,7 @@ export function GreenInnovationPage() {
|
||||||
شرح پروژه
|
شرح پروژه
|
||||||
</DialogTitle>
|
</DialogTitle>
|
||||||
</DialogHeader>
|
</DialogHeader>
|
||||||
<div className="space-y-4 flex justify-between text-right p-6">
|
<div className="space-y-4 flex justify-between text-right px-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">{selectedProjectDetails?.title}</h2>
|
<h2 className="font-bold">{selectedProjectDetails?.title}</h2>
|
||||||
|
|
|
||||||
|
|
@ -39,11 +39,8 @@ import {
|
||||||
ResponsiveContainer,
|
ResponsiveContainer,
|
||||||
XAxis,
|
XAxis,
|
||||||
} from "recharts";
|
} from "recharts";
|
||||||
import { MetricCard } from "~/components/ui/metric-card";
|
|
||||||
import { useStoredDate } from "~/hooks/useStoredDate";
|
|
||||||
import apiService from "~/lib/api";
|
import apiService from "~/lib/api";
|
||||||
import { EventBus, formatCurrency, formatNumber } from "~/lib/utils";
|
import { formatCurrency, formatNumber } from "~/lib/utils";
|
||||||
import type { CalendarDate } from "~/types/util.type";
|
|
||||||
import DashboardLayout from "../layout";
|
import DashboardLayout from "../layout";
|
||||||
|
|
||||||
interface innovationBuiltInDate {
|
interface innovationBuiltInDate {
|
||||||
|
|
@ -155,8 +152,8 @@ enum projectStatus {
|
||||||
|
|
||||||
const columns = [
|
const columns = [
|
||||||
{ key: "select", label: "", sortable: false, width: "50px" },
|
{ key: "select", label: "", sortable: false, width: "50px" },
|
||||||
{ key: "project_no", label: "شماره پروژه", sortable: true, width: "120px" },
|
{ key: "project_no", label: "شماره پروژه", sortable: true, width: "140px" },
|
||||||
{ key: "title", label: "عنوان پروژه", sortable: true, width: "300px" },
|
{ key: "title", label: "عنوان پروژه", sortable: true, width: "400px" },
|
||||||
{
|
{
|
||||||
key: "project_status",
|
key: "project_status",
|
||||||
label: "وضعیت پروژه",
|
label: "وضعیت پروژه",
|
||||||
|
|
@ -167,7 +164,7 @@ const columns = [
|
||||||
key: "project_rating",
|
key: "project_rating",
|
||||||
label: "امتیاز پروژه",
|
label: "امتیاز پروژه",
|
||||||
sortable: true,
|
sortable: true,
|
||||||
width: "120px",
|
width: "140px",
|
||||||
},
|
},
|
||||||
{ key: "details", label: "جزئیات پروژه", sortable: false, width: "140px" },
|
{ key: "details", label: "جزئیات پروژه", sortable: false, width: "140px" },
|
||||||
];
|
];
|
||||||
|
|
@ -194,8 +191,6 @@ export function InnovationBuiltInsidePage() {
|
||||||
field: "start_date",
|
field: "start_date",
|
||||||
direction: "asc",
|
direction: "asc",
|
||||||
});
|
});
|
||||||
|
|
||||||
const [date, setDate] = useStoredDate();
|
|
||||||
const [tblAvarage, setTblAvarage] = useState<number>(0);
|
const [tblAvarage, setTblAvarage] = useState<number>(0);
|
||||||
const [selectedProjects, setSelectedProjects] =
|
const [selectedProjects, setSelectedProjects] =
|
||||||
useState<Set<string | number>>();
|
useState<Set<string | number>>();
|
||||||
|
|
@ -315,11 +310,7 @@ export function InnovationBuiltInsidePage() {
|
||||||
"technology_maturity_level",
|
"technology_maturity_level",
|
||||||
],
|
],
|
||||||
Sorts: [[sortConfig.field, sortConfig.direction]],
|
Sorts: [[sortConfig.field, sortConfig.direction]],
|
||||||
Conditions: [
|
Conditions: [["type_of_innovation", "=", "نوآوری ساخت داخل"]],
|
||||||
["type_of_innovation", "=", "نوآوری ساخت داخل", "and"],
|
|
||||||
["start_date", ">=", date?.start || null, "and"],
|
|
||||||
["start_date", "<=", date?.end || null],
|
|
||||||
],
|
|
||||||
Pagination: { PageNumber: pageToFetch, PageSize: pageSize },
|
Pagination: { PageNumber: pageToFetch, PageSize: pageSize },
|
||||||
});
|
});
|
||||||
if (response.state === 0) {
|
if (response.state === 0) {
|
||||||
|
|
@ -426,24 +417,12 @@ export function InnovationBuiltInsidePage() {
|
||||||
}, [hasMore, loading]);
|
}, [hasMore, loading]);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const handler = (date: CalendarDate) => {
|
fetchProjects(true);
|
||||||
if (date) setDate(date);
|
}, [sortConfig]);
|
||||||
};
|
|
||||||
|
|
||||||
EventBus.on("dateSelected", handler);
|
|
||||||
|
|
||||||
return () => {
|
|
||||||
EventBus.off("dateSelected", handler);
|
|
||||||
};
|
|
||||||
}, []);
|
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (date.start && date.end) fetchProjects(true);
|
fetchStats();
|
||||||
}, [sortConfig, date]);
|
}, [selectedProjects]);
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
if (date.end && date.start) fetchStats();
|
|
||||||
}, [selectedProjects, date]);
|
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (currentPage > 1) {
|
if (currentPage > 1) {
|
||||||
|
|
@ -501,8 +480,6 @@ export function InnovationBuiltInsidePage() {
|
||||||
selectedProjects && selectedProjects?.size > 0
|
selectedProjects && selectedProjects?.size > 0
|
||||||
? Array.from(selectedProjects).join(" , ")
|
? Array.from(selectedProjects).join(" , ")
|
||||||
: "",
|
: "",
|
||||||
start_date: date?.start || null,
|
|
||||||
end_date: date?.end || null,
|
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
let payload: any = raw?.data;
|
let payload: any = raw?.data;
|
||||||
|
|
@ -528,13 +505,15 @@ export function InnovationBuiltInsidePage() {
|
||||||
const stats = data[0];
|
const stats = data[0];
|
||||||
const normalized: any = {
|
const normalized: any = {
|
||||||
currencySaving: {
|
currencySaving: {
|
||||||
value: parseNum(stats?.foreign_currency_saving),
|
value: formatNumber(parseNum(stats?.foreign_currency_saving)),
|
||||||
percent: parseNum(stats?.foreign_currency_saving_percent),
|
percent: formatNumber(
|
||||||
|
parseNum(stats?.foreign_currency_saving_percent)
|
||||||
|
),
|
||||||
},
|
},
|
||||||
|
|
||||||
investmentAmount: {
|
investmentAmount: {
|
||||||
value: parseNum(stats?.investment_amount),
|
value: formatNumber(parseNum(stats?.investment_amount)),
|
||||||
percent: parseNum(stats?.investment_amount_percent),
|
percent: formatNumber(parseNum(stats?.investment_amount_percent)),
|
||||||
},
|
},
|
||||||
|
|
||||||
technology: {
|
technology: {
|
||||||
|
|
@ -645,14 +624,14 @@ export function InnovationBuiltInsidePage() {
|
||||||
variant="ghost"
|
variant="ghost"
|
||||||
size="sm"
|
size="sm"
|
||||||
onClick={() => handleProjectDetails(item)}
|
onClick={() => handleProjectDetails(item)}
|
||||||
className="text-pr-green hover:text-pr-green underline-offset-4 underline font-normal hover:bg-emerald-500/20 p-2 h-auto"
|
className="text-emerald-500 hover:text-emerald-300 hover:bg-emerald-500/20 p-2 h-auto cursor-pointer"
|
||||||
>
|
>
|
||||||
جزئیات بیشتر
|
جزئیات بیشتر
|
||||||
</Button>
|
</Button>
|
||||||
);
|
);
|
||||||
case "amount_currency_reduction":
|
case "amount_currency_reduction":
|
||||||
return (
|
return (
|
||||||
<span className="font-medium text-pr-green">
|
<span className="font-medium text-emerald-500">
|
||||||
{formatCurrency(String(value))}
|
{formatCurrency(String(value))}
|
||||||
</span>
|
</span>
|
||||||
);
|
);
|
||||||
|
|
@ -663,9 +642,7 @@ export function InnovationBuiltInsidePage() {
|
||||||
</Badge>
|
</Badge>
|
||||||
);
|
);
|
||||||
case "title":
|
case "title":
|
||||||
return (
|
return <span className="font-medium text-white">{String(value)}</span>;
|
||||||
<span className="font-light 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">
|
||||||
|
|
@ -724,10 +701,10 @@ export function InnovationBuiltInsidePage() {
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<DashboardLayout title="نوآوری ساخت داخل">
|
<DashboardLayout title="نوآوری ساخت داخل">
|
||||||
<div className="space-y-4 justify-between gap-8 grid pl-6 sm:grid-cols-1 xl:grid-cols-[35%_65%]">
|
<div className="p-6 space-y-4 justify-between gap-8 grid sm:grid-cols-1 xl:grid-cols-[40%_60%]">
|
||||||
{/* Stats Cards */}
|
{/* Stats Cards */}
|
||||||
<div className="flex w-full mb-0">
|
<div className="flex gap-6 w-full mb-0">
|
||||||
<div className="flex flex-col w-full justify-between gap-2">
|
<div className="flex flex-col justify-between w-full gap-6">
|
||||||
{statsLoading
|
{statsLoading
|
||||||
? // Loading skeleton for stats cards - matching new design
|
? // Loading skeleton for stats cards - matching new design
|
||||||
Array.from({ length: 2 }).map((_, index) => (
|
Array.from({ length: 2 }).map((_, index) => (
|
||||||
|
|
@ -758,47 +735,39 @@ export function InnovationBuiltInsidePage() {
|
||||||
</Card>
|
</Card>
|
||||||
))
|
))
|
||||||
: Object.entries(sustainabilityStats).map(([key, value]) => (
|
: Object.entries(sustainabilityStats).map(([key, value]) => (
|
||||||
<MetricCard
|
<Card
|
||||||
key={key}
|
key={key}
|
||||||
title={value.title}
|
className="bg-[linear-gradient(to_bottom_left,#464861,50%,#111628)] rounded-lg backdrop-blur-sm border-gray-700/50"
|
||||||
value={Math.round(value.total.value || 0)}
|
>
|
||||||
valueLabel={value.total?.description}
|
<CardContent className="p-0 h-full">
|
||||||
percentValue={value.percent?.value || 0}
|
<div className="flex flex-col justify-between gap-2 h-full">
|
||||||
percentLabel={value.percent?.description}
|
<div className="flex justify-between items-center border-b-2 border-gray-500/20 ">
|
||||||
/>
|
<h3 className="text-lg font-semibold text-white p-4">
|
||||||
// <Card
|
{value.title}
|
||||||
// key={key}
|
</h3>
|
||||||
// className="bg-[linear-gradient(to_bottom_left,#464861,50%,#111628)] rounded-lg backdrop-blur-sm border-gray-700/50"
|
</div>
|
||||||
// >
|
<div className="flex items-center justify-between p-6 flex-row-reverse">
|
||||||
// <CardContent className="p-0 h-full">
|
<div className="flex flex-col">
|
||||||
// <div className="flex flex-col justify-between gap-2 h-full">
|
<span className="text-3xl font-bold text-emerald-500 mb-1 font-persian">
|
||||||
// <div className="flex justify-between items-center border-b-2 border-gray-500/20 ">
|
% {value.percent?.value}
|
||||||
// <h3 className="text-lg font-semibold text-white p-4">
|
</span>
|
||||||
// {value.title}
|
<span className="text-sm text-gray-400 font-persian">
|
||||||
// </h3>
|
{value.percent?.description}
|
||||||
// </div>
|
</span>
|
||||||
// <div className="flex items-center justify-between p-6 flex-row-reverse">
|
</div>
|
||||||
// <div className="flex flex-col">
|
<b className="block w-0.5 h-8 bg-gray-600 rotate-45" />
|
||||||
// <span className="text-3xl font-bold text-pr-green mb-1 font-persian">
|
<div className="flex flex-col">
|
||||||
// % {value.percent?.value}
|
<span className="text-3xl font-bold text-emerald-500 mb-1 font-persian">
|
||||||
// </span>
|
{value.total?.value}
|
||||||
// <span className="text-sm text-gray-400 font-persian">
|
</span>
|
||||||
// {value.percent?.description}
|
<span className="text-sm text-gray-400 font-persian">
|
||||||
// </span>
|
{value.total?.description}
|
||||||
// </div>
|
</span>
|
||||||
// <b className="block w-0.5 h-8 bg-gray-600 rotate-45" />
|
</div>
|
||||||
// <div className="flex flex-col">
|
</div>
|
||||||
// <span className="text-3xl font-bold text-pr-green mb-1 font-persian">
|
</div>
|
||||||
// {value.total?.value}
|
</CardContent>
|
||||||
// </span>
|
</Card>
|
||||||
// <span className="text-sm text-gray-400 font-persian">
|
|
||||||
// {value.total?.description}
|
|
||||||
// </span>
|
|
||||||
// </div>
|
|
||||||
// </div>
|
|
||||||
// </div>
|
|
||||||
// </CardContent>
|
|
||||||
// </Card>
|
|
||||||
))}
|
))}
|
||||||
|
|
||||||
{statsLoading ? (
|
{statsLoading ? (
|
||||||
|
|
@ -898,7 +867,7 @@ export function InnovationBuiltInsidePage() {
|
||||||
<Card className="bg-transparent backdrop-blur-sm rounded-lg overflow-hidden w-full h-max">
|
<Card className="bg-transparent backdrop-blur-sm rounded-lg overflow-hidden w-full h-max">
|
||||||
<CardContent className="p-0">
|
<CardContent className="p-0">
|
||||||
<div className="relative ">
|
<div className="relative ">
|
||||||
<Table containerClassName="overflow-auto custom-scrollbar h-[calc(100vh-160px)]">
|
<Table containerClassName="overflow-auto custom-scrollbar h-[calc(90vh-15px)]">
|
||||||
<TableHeader>
|
<TableHeader>
|
||||||
<TableRow className="bg-[#3F415A]">
|
<TableRow className="bg-[#3F415A]">
|
||||||
{columns.map((column) => (
|
{columns.map((column) => (
|
||||||
|
|
@ -1061,7 +1030,7 @@ export function InnovationBuiltInsidePage() {
|
||||||
|
|
||||||
{/* Project Details Dialog */}
|
{/* Project Details Dialog */}
|
||||||
<Dialog open={detailsDialogOpen} onOpenChange={setDetailsDialogOpen}>
|
<Dialog open={detailsDialogOpen} onOpenChange={setDetailsDialogOpen}>
|
||||||
<DialogContent className="bg-[linear-gradient(to_bottom_left,#464861,50%,#111628)] max-w-5xl overflow-y-auto">
|
<DialogContent className="bg-[linear-gradient(to_bottom_left,#464861,50%,#111628)] max-w-6xl overflow-y-auto">
|
||||||
<DialogHeader>
|
<DialogHeader>
|
||||||
<DialogTitle className="text-white mr-4 border-b-2 border-gray-600 pb-4 font-persian text-right">
|
<DialogTitle className="text-white mr-4 border-b-2 border-gray-600 pb-4 font-persian text-right">
|
||||||
شرح پروژه
|
شرح پروژه
|
||||||
|
|
@ -1123,6 +1092,7 @@ export function InnovationBuiltInsidePage() {
|
||||||
<div className="flex flex-col justify-center items-center">
|
<div className="flex flex-col justify-center items-center">
|
||||||
<span className="block w-0.5 h-14 bg-white"></span>
|
<span className="block w-0.5 h-14 bg-white"></span>
|
||||||
<span className="text-white border border-white p-1 px-2 text-xs rounded-lg">
|
<span className="text-white border border-white p-1 px-2 text-xs rounded-lg">
|
||||||
|
{" "}
|
||||||
سطح تکنولوژی
|
سطح تکنولوژی
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
|
|
@ -1286,7 +1256,7 @@ export function InnovationBuiltInsidePage() {
|
||||||
))}
|
))}
|
||||||
</div>
|
</div>
|
||||||
) : (
|
) : (
|
||||||
<ResponsiveContainer width="100%" height={400}>
|
<ResponsiveContainer width="100%" height={420}>
|
||||||
<LineChart
|
<LineChart
|
||||||
data={dialogChartData}
|
data={dialogChartData}
|
||||||
margin={{ top: 20, right: 70, left: 30, bottom: 80 }}
|
margin={{ top: 20, right: 70, left: 30, bottom: 80 }}
|
||||||
|
|
|
||||||
File diff suppressed because it is too large
Load Diff
|
|
@ -15,9 +15,8 @@ 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 { BaseCard } from "~/components/ui/base-card";
|
|
||||||
import { Button } from "~/components/ui/button";
|
import { Button } from "~/components/ui/button";
|
||||||
import { Card, CardContent } from "~/components/ui/card";
|
import { BaseCard } from "~/components/ui/base-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 {
|
||||||
|
|
@ -34,11 +33,10 @@ import {
|
||||||
TableHeader,
|
TableHeader,
|
||||||
TableRow,
|
TableRow,
|
||||||
} from "~/components/ui/table";
|
} from "~/components/ui/table";
|
||||||
import { useStoredDate } from "~/hooks/useStoredDate";
|
|
||||||
import apiService from "~/lib/api";
|
import apiService from "~/lib/api";
|
||||||
import { EventBus, formatNumber } from "~/lib/utils";
|
import { formatNumber } from "~/lib/utils";
|
||||||
import type { CalendarDate } from "~/types/util.type";
|
|
||||||
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 {
|
||||||
|
|
@ -67,11 +65,9 @@ interface ProjectStats {
|
||||||
percent_reduction_value_currency: string;
|
percent_reduction_value_currency: string;
|
||||||
percent_sum_stopping_production: string;
|
percent_sum_stopping_production: string;
|
||||||
percent_throat_removal: string;
|
percent_throat_removal: string;
|
||||||
percent_operating_cost_before_innovation: string;
|
|
||||||
sum_reducing_breakdowns: number;
|
sum_reducing_breakdowns: number;
|
||||||
sum_reduction_value_currency: number;
|
sum_reduction_value_currency: number;
|
||||||
sum_stopping_production: number;
|
sum_stopping_production: number;
|
||||||
sum_operating_cost_reduction: number;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
interface SortConfig {
|
interface SortConfig {
|
||||||
|
|
@ -96,11 +92,9 @@ interface InnovationStats {
|
||||||
currencyReductionSum: number; // مجموع کاهش ارز بری (میلیون ریال)
|
currencyReductionSum: number; // مجموع کاهش ارز بری (میلیون ریال)
|
||||||
frequentFailuresReductionSum: number; // مجموع کاهش خرابی های پرتکرار
|
frequentFailuresReductionSum: number; // مجموع کاهش خرابی های پرتکرار
|
||||||
percentProductionStops: number | string; // درصد مقایسهای جلوگیری از توقفات تولید
|
percentProductionStops: number | string; // درصد مقایسهای جلوگیری از توقفات تولید
|
||||||
reductionCostOprationSum: number; // مجموع کاهش هزینه عملیاتی
|
|
||||||
percentBottleneckRemoval: number | string; // درصد مقایسهای رفع گلوگاه
|
percentBottleneckRemoval: number | string; // درصد مقایسهای رفع گلوگاه
|
||||||
percentCurrencyReduction: number | string; // درصد مقایسهای کاهش ارز بری
|
percentCurrencyReduction: number | string; // درصد مقایسهای کاهش ارز بری
|
||||||
percentFailuresReduction: number | string; // درصد مقایسهای کاهش خرابیهای پرتکرار
|
percentFailuresReduction: number | string; // درصد مقایسهای کاهش خرابیهای پرتکرار
|
||||||
percentOperatingCostBeforeInnovation: number | string; // درصد مقایسهای کاهش هزینه عملیاتی
|
|
||||||
}
|
}
|
||||||
|
|
||||||
const columns = [
|
const columns = [
|
||||||
|
|
@ -129,14 +123,13 @@ export function ProcessInnovationPage() {
|
||||||
const [currentPage, setCurrentPage] = useState(1);
|
const [currentPage, setCurrentPage] = useState(1);
|
||||||
const [pageSize] = useState(20);
|
const [pageSize] = useState(20);
|
||||||
const [hasMore, setHasMore] = useState(true);
|
const [hasMore, setHasMore] = useState(true);
|
||||||
const [date, setDate] = useStoredDate();
|
const [totalCount, setTotalCount] = useState(0);
|
||||||
const [actualTotalCount, setActualTotalCount] = useState(0);
|
const [actualTotalCount, setActualTotalCount] = useState(0);
|
||||||
const [statsLoading, setStatsLoading] = useState(false);
|
const [statsLoading, setStatsLoading] = useState(false);
|
||||||
const [stats, setStats] = useState<InnovationStats>({
|
const [stats, setStats] = useState<InnovationStats>({
|
||||||
totalProjects: 0,
|
totalProjects: 0,
|
||||||
averageScore: 0,
|
averageScore: 0,
|
||||||
productionStopsPreventionSum: 0,
|
productionStopsPreventionSum: 0,
|
||||||
reductionCostOprationSum: 0,
|
|
||||||
bottleneckRemovalCount: 0,
|
bottleneckRemovalCount: 0,
|
||||||
currencyReductionSum: 0,
|
currencyReductionSum: 0,
|
||||||
frequentFailuresReductionSum: 0,
|
frequentFailuresReductionSum: 0,
|
||||||
|
|
@ -144,7 +137,6 @@ export function ProcessInnovationPage() {
|
||||||
percentBottleneckRemoval: 0,
|
percentBottleneckRemoval: 0,
|
||||||
percentCurrencyReduction: 0,
|
percentCurrencyReduction: 0,
|
||||||
percentFailuresReduction: 0,
|
percentFailuresReduction: 0,
|
||||||
percentOperatingCostBeforeInnovation: 0,
|
|
||||||
});
|
});
|
||||||
const [sortConfig, setSortConfig] = useState<SortConfig>({
|
const [sortConfig, setSortConfig] = useState<SortConfig>({
|
||||||
field: "start_date",
|
field: "start_date",
|
||||||
|
|
@ -160,60 +152,58 @@ export function ProcessInnovationPage() {
|
||||||
const [stateCard, setStateCard] = useState({
|
const [stateCard, setStateCard] = useState({
|
||||||
productionstopsprevention: {
|
productionstopsprevention: {
|
||||||
id: "productionstopsprevention",
|
id: "productionstopsprevention",
|
||||||
title: "توقفات تولید",
|
title: "جلوگیری از توقفات تولید",
|
||||||
value: formatNumber(
|
value: formatNumber(
|
||||||
stats.productionStopsPreventionSum.toFixed?.(1) ??
|
stats.productionStopsPreventionSum.toFixed?.(1) ??
|
||||||
stats.productionStopsPreventionSum
|
stats.productionStopsPreventionSum
|
||||||
),
|
),
|
||||||
description: "تن افزایش یافته",
|
description: "تن افزایش یافته",
|
||||||
icon: CirclePause,
|
icon: CirclePause,
|
||||||
color: "text-pr-green",
|
color: "text-emerald-400",
|
||||||
},
|
},
|
||||||
bottleneckremoval: {
|
bottleneckremoval: {
|
||||||
id: "bottleneckremoval",
|
id: "bottleneckremoval",
|
||||||
title: "گلوگاه ها",
|
title: "رفع گلوگاه",
|
||||||
value: formatNumber(stats.bottleneckRemovalCount),
|
value: formatNumber(stats.bottleneckRemovalCount),
|
||||||
description: "تعداد رفع گلوگاه",
|
description: "تعداد رفع گلوگاه",
|
||||||
icon: Funnel,
|
icon: Funnel,
|
||||||
color: "text-pr-green",
|
color: "text-emerald-400",
|
||||||
},
|
},
|
||||||
currencyreduction: {
|
currencyreduction: {
|
||||||
id: "currencyreduction",
|
id: "currencyreduction",
|
||||||
title: "ارز بری",
|
title: "کاهش ارز بری",
|
||||||
value: formatNumber(
|
value: formatNumber(
|
||||||
stats.currencyReductionSum.toFixed?.(0) ?? stats.currencyReductionSum
|
stats.currencyReductionSum.toFixed?.(0) ?? stats.currencyReductionSum
|
||||||
),
|
),
|
||||||
description: "دلار کاهش یافته",
|
description: "دلار کاهش یافته",
|
||||||
icon: DollarSign,
|
icon: DollarSign ,
|
||||||
color: "text-pr-green",
|
color: "text-emerald-400",
|
||||||
},
|
|
||||||
decreaseCurrencyOperation: {
|
|
||||||
id: "decreaseCurrencyOperation",
|
|
||||||
title: "هزینه های عملیاتی",
|
|
||||||
value: formatNumber(
|
|
||||||
stats.reductionCostOprationSum.toFixed?.(0) ??
|
|
||||||
stats.reductionCostOprationSum
|
|
||||||
),
|
|
||||||
description: "میلیون ریال کاهش یافته",
|
|
||||||
icon: DollarSign,
|
|
||||||
color: "text-pr-green",
|
|
||||||
},
|
},
|
||||||
frequentfailuresreduction: {
|
frequentfailuresreduction: {
|
||||||
id: "frequentfailuresreduction",
|
id: "frequentfailuresreduction",
|
||||||
title: "خرابی های پرتکرار",
|
title: "کاهش خرابی های پرتکرار",
|
||||||
value: formatNumber(
|
value: formatNumber(
|
||||||
stats.frequentFailuresReductionSum.toFixed?.(1) ??
|
stats.frequentFailuresReductionSum.toFixed?.(1) ??
|
||||||
stats.frequentFailuresReductionSum
|
stats.frequentFailuresReductionSum
|
||||||
),
|
),
|
||||||
description: "خرابی پر تکرار کاهش یافته",
|
description: "مجموع درصد کاهش خرابی",
|
||||||
icon: Wrench,
|
icon: Wrench,
|
||||||
color: "text-pr-green",
|
color: "text-emerald-400",
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
const observerRef = useRef<HTMLDivElement>(null);
|
const observerRef = useRef<HTMLDivElement>(null);
|
||||||
const fetchingRef = useRef(false);
|
const fetchingRef = useRef(false);
|
||||||
|
|
||||||
|
// Selection handlers
|
||||||
|
const handleSelectAll = () => {
|
||||||
|
if (selectedProjects.size === projects.length) {
|
||||||
|
setSelectedProjects(new Set());
|
||||||
|
} else {
|
||||||
|
setSelectedProjects(new Set(projects.map((p) => p.project_no)));
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
const handleSelectProject = (projectNo: string) => {
|
const handleSelectProject = (projectNo: string) => {
|
||||||
const newSelected = new Set(selectedProjects);
|
const newSelected = new Set(selectedProjects);
|
||||||
if (newSelected.has(projectNo)) {
|
if (newSelected.has(projectNo)) {
|
||||||
|
|
@ -266,11 +256,7 @@ export function ProcessInnovationPage() {
|
||||||
"observer",
|
"observer",
|
||||||
],
|
],
|
||||||
Sorts: [["start_date", "asc"]],
|
Sorts: [["start_date", "asc"]],
|
||||||
Conditions: [
|
Conditions: [["type_of_innovation", "=", "نوآوری در فرآیند"]],
|
||||||
["type_of_innovation", "=", "نوآوری در فرآیند", "and"],
|
|
||||||
["start_date", ">=", date?.start || null, "and"],
|
|
||||||
["start_date", "<=", date?.end || null],
|
|
||||||
],
|
|
||||||
Pagination: { PageNumber: pageToFetch, PageSize: pageSize },
|
Pagination: { PageNumber: pageToFetch, PageSize: pageSize },
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
@ -282,16 +268,16 @@ export function ProcessInnovationPage() {
|
||||||
if (Array.isArray(parsedData)) {
|
if (Array.isArray(parsedData)) {
|
||||||
if (reset) {
|
if (reset) {
|
||||||
setProjects(parsedData);
|
setProjects(parsedData);
|
||||||
// setTotalCount(parsedData.length);
|
setTotalCount(parsedData.length);
|
||||||
} else {
|
} else {
|
||||||
setProjects((prev) => [...prev, ...parsedData]);
|
setProjects((prev) => [...prev, ...parsedData]);
|
||||||
// setTotalCount((prev) => prev + parsedData.length);
|
setTotalCount((prev) => prev + parsedData.length);
|
||||||
}
|
}
|
||||||
setHasMore(parsedData.length === pageSize);
|
setHasMore(parsedData.length === pageSize);
|
||||||
} else {
|
} else {
|
||||||
if (reset) {
|
if (reset) {
|
||||||
setProjects([]);
|
setProjects([]);
|
||||||
// setTotalCount(0);
|
setTotalCount(0);
|
||||||
}
|
}
|
||||||
setHasMore(false);
|
setHasMore(false);
|
||||||
}
|
}
|
||||||
|
|
@ -299,14 +285,14 @@ export function ProcessInnovationPage() {
|
||||||
console.error("Error parsing project data:", parseError);
|
console.error("Error parsing project data:", parseError);
|
||||||
if (reset) {
|
if (reset) {
|
||||||
setProjects([]);
|
setProjects([]);
|
||||||
// setTotalCount(0);
|
setTotalCount(0);
|
||||||
}
|
}
|
||||||
setHasMore(false);
|
setHasMore(false);
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
if (reset) {
|
if (reset) {
|
||||||
setProjects([]);
|
setProjects([]);
|
||||||
// setTotalCount(0);
|
setTotalCount(0);
|
||||||
}
|
}
|
||||||
setHasMore(false);
|
setHasMore(false);
|
||||||
}
|
}
|
||||||
|
|
@ -314,7 +300,7 @@ export function ProcessInnovationPage() {
|
||||||
toast.error(response.message || "خطا در دریافت اطلاعات پروژهها");
|
toast.error(response.message || "خطا در دریافت اطلاعات پروژهها");
|
||||||
if (reset) {
|
if (reset) {
|
||||||
setProjects([]);
|
setProjects([]);
|
||||||
// setTotalCount(0);
|
setTotalCount(0);
|
||||||
}
|
}
|
||||||
setHasMore(false);
|
setHasMore(false);
|
||||||
}
|
}
|
||||||
|
|
@ -323,7 +309,7 @@ export function ProcessInnovationPage() {
|
||||||
toast.error("خطا در دریافت اطلاعات پروژهها");
|
toast.error("خطا در دریافت اطلاعات پروژهها");
|
||||||
if (reset) {
|
if (reset) {
|
||||||
setProjects([]);
|
setProjects([]);
|
||||||
// setTotalCount(0);
|
setTotalCount(0);
|
||||||
}
|
}
|
||||||
setHasMore(false);
|
setHasMore(false);
|
||||||
} finally {
|
} finally {
|
||||||
|
|
@ -340,27 +326,13 @@ export function ProcessInnovationPage() {
|
||||||
}, [hasMore, loading]);
|
}, [hasMore, loading]);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const handler = (date: CalendarDate) => {
|
fetchProjects(true);
|
||||||
if (date) setDate(date);
|
fetchTotalCount();
|
||||||
};
|
}, [sortConfig]);
|
||||||
|
|
||||||
EventBus.on("dateSelected", handler);
|
|
||||||
|
|
||||||
return () => {
|
|
||||||
EventBus.off("dateSelected", handler);
|
|
||||||
};
|
|
||||||
}, []);
|
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (date?.start && date?.end) {
|
fetchStats();
|
||||||
fetchProjects(true);
|
}, [selectedProjects]);
|
||||||
fetchTotalCount();
|
|
||||||
}
|
|
||||||
}, [sortConfig, date]);
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
if (date?.start && date?.end) fetchStats();
|
|
||||||
}, [selectedProjects, date]);
|
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (currentPage > 1) {
|
if (currentPage > 1) {
|
||||||
|
|
@ -410,11 +382,7 @@ export function ProcessInnovationPage() {
|
||||||
const response = await apiService.select({
|
const response = await apiService.select({
|
||||||
ProcessName: "project",
|
ProcessName: "project",
|
||||||
OutputFields: ["count(project_no)"],
|
OutputFields: ["count(project_no)"],
|
||||||
Conditions: [
|
Conditions: [["type_of_innovation", "=", "نوآوری در فرآیند"]],
|
||||||
["type_of_innovation", "=", "نوآوری در فرآیند", "and"],
|
|
||||||
["start_date", ">=", date?.start || null, "and"],
|
|
||||||
["start_date", "<=", date?.end || null],
|
|
||||||
],
|
|
||||||
});
|
});
|
||||||
|
|
||||||
if (response.state === 0) {
|
if (response.state === 0) {
|
||||||
|
|
@ -448,8 +416,6 @@ export function ProcessInnovationPage() {
|
||||||
selectedProjects.size > 0
|
selectedProjects.size > 0
|
||||||
? Array.from(selectedProjects).join(" , ")
|
? Array.from(selectedProjects).join(" , ")
|
||||||
: "",
|
: "",
|
||||||
start_date: date?.start || null,
|
|
||||||
end_date: date?.end || null,
|
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
@ -480,13 +446,10 @@ export function ProcessInnovationPage() {
|
||||||
totalProjects: parseNum(stats?.count_innovation_process_projects),
|
totalProjects: parseNum(stats?.count_innovation_process_projects),
|
||||||
averageScore: parseFloat(data[0].average_project_score),
|
averageScore: parseFloat(data[0].average_project_score),
|
||||||
productionStopsPreventionSum: parseNum(stats?.sum_stopping_production),
|
productionStopsPreventionSum: parseNum(stats?.sum_stopping_production),
|
||||||
reductionCostOprationSum: parseNum(stats?.sum_operating_cost_reduction),
|
|
||||||
bottleneckRemovalCount: parseNum(stats?.count_throat_removal),
|
bottleneckRemovalCount: parseNum(stats?.count_throat_removal),
|
||||||
currencyReductionSum: parseNum(stats?.sum_reduction_value_currency),
|
currencyReductionSum: parseNum(stats?.sum_reduction_value_currency),
|
||||||
frequentFailuresReductionSum: parseNum(stats?.sum_reducing_breakdowns),
|
frequentFailuresReductionSum: parseNum(stats?.sum_reducing_breakdowns),
|
||||||
percentProductionStops: stats?.percent_sum_stopping_production,
|
percentProductionStops: stats?.percent_sum_stopping_production,
|
||||||
percentOperatingCostBeforeInnovation:
|
|
||||||
stats?.percent_operating_cost_before_innovation,
|
|
||||||
percentBottleneckRemoval: stats?.percent_throat_removal,
|
percentBottleneckRemoval: stats?.percent_throat_removal,
|
||||||
percentCurrencyReduction: stats?.percent_reduction_value_currency,
|
percentCurrencyReduction: stats?.percent_reduction_value_currency,
|
||||||
percentFailuresReduction: stats?.percent_reducing_breakdowns,
|
percentFailuresReduction: stats?.percent_reducing_breakdowns,
|
||||||
|
|
@ -509,10 +472,6 @@ export function ProcessInnovationPage() {
|
||||||
...prev.currencyreduction,
|
...prev.currencyreduction,
|
||||||
value: formatNumber(normalized.currencyReductionSum),
|
value: formatNumber(normalized.currencyReductionSum),
|
||||||
},
|
},
|
||||||
decreaseCurrencyOperation: {
|
|
||||||
...prev.decreaseCurrencyOperation,
|
|
||||||
value: formatNumber(normalized.reductionCostOprationSum),
|
|
||||||
},
|
|
||||||
}));
|
}));
|
||||||
setStats(normalized);
|
setStats(normalized);
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
|
|
@ -565,7 +524,7 @@ export function ProcessInnovationPage() {
|
||||||
<Checkbox
|
<Checkbox
|
||||||
checked={selectedProjects.has(item.project_id)}
|
checked={selectedProjects.has(item.project_id)}
|
||||||
onCheckedChange={() => handleSelectProject(item.project_id)}
|
onCheckedChange={() => handleSelectProject(item.project_id)}
|
||||||
className="data-[state=checked]:bg-pr-green data-[state=checked]:border-pr-green"
|
className="data-[state=checked]:bg-emerald-600 data-[state=checked]:border-emerald-600"
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
case "details":
|
case "details":
|
||||||
|
|
@ -574,14 +533,14 @@ export function ProcessInnovationPage() {
|
||||||
variant="ghost"
|
variant="ghost"
|
||||||
size="sm"
|
size="sm"
|
||||||
onClick={() => handleProjectDetails(item)}
|
onClick={() => handleProjectDetails(item)}
|
||||||
className="text-pr-green underline-offset-4 underline font-normal p-2 h-auto"
|
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>
|
||||||
);
|
);
|
||||||
case "amount_currency_reduction":
|
case "amount_currency_reduction":
|
||||||
return (
|
return (
|
||||||
<span className="font-medium text-pr-green">
|
<span className="font-medium text-emerald-400">
|
||||||
{formatCurrency(String(value))}
|
{formatCurrency(String(value))}
|
||||||
</span>
|
</span>
|
||||||
);
|
);
|
||||||
|
|
@ -592,11 +551,7 @@ export function ProcessInnovationPage() {
|
||||||
</Badge>
|
</Badge>
|
||||||
);
|
);
|
||||||
case "title":
|
case "title":
|
||||||
return (
|
return <span className="font-normal text-sm text-white">{String(value)}</span>;
|
||||||
<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">
|
||||||
|
|
@ -612,10 +567,7 @@ export function ProcessInnovationPage() {
|
||||||
);
|
);
|
||||||
case "project_rating":
|
case "project_rating":
|
||||||
return (
|
return (
|
||||||
<Badge
|
<Badge variant="outline" className="text-base font-semibold text-center border-none">
|
||||||
variant="outline"
|
|
||||||
className="text-base font-semibold text-center border-none"
|
|
||||||
>
|
|
||||||
{formatNumber(String(value))}
|
{formatNumber(String(value))}
|
||||||
</Badge>
|
</Badge>
|
||||||
);
|
);
|
||||||
|
|
@ -634,27 +586,23 @@ export function ProcessInnovationPage() {
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<DashboardLayout title="نوآوری در فرآیند">
|
<DashboardLayout title="نوآوری در فرآیند">
|
||||||
<div className="flex flex-col gap-4">
|
<div className="p-6 py-2 space-y-4">
|
||||||
{/* Stats Cards */}
|
{/* Stats Cards */}
|
||||||
<div className="flex gap-4">
|
<div className="flex gap-6">
|
||||||
<div className="space-y-4 w-full">
|
<div className="space-y-4 w-full">
|
||||||
{/* Stats Grid */}
|
{/* Stats Grid */}
|
||||||
<div className="h-full">
|
<div className="grid grid-cols-2 gap-3">
|
||||||
{loading || statsLoading ? (
|
{loading || statsLoading
|
||||||
// Skeleton cards
|
? // Loading skeleton for stats cards - matching new design
|
||||||
<div className="flex flex-wrap justify-between gap-3">
|
Array.from({ length: 4 }).map((_, index) => (
|
||||||
{Array.from({ length: 6 }).map((_, index) => (
|
<BaseCard key={`skeleton-${index}`} className="rounded-2xl overflow-hidden">
|
||||||
<BaseCard
|
|
||||||
key={`skeleton-${index}`}
|
|
||||||
className="rounded-2xl overflow-hidden w-full sm:w-[48%] md:w-[30%]"
|
|
||||||
>
|
|
||||||
<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
|
||||||
className="h-6 bg-gray-600 rounded animate-pulse"
|
className="h-6 bg-gray-600 rounded animate-pulse"
|
||||||
style={{ width: "60%" }}
|
style={{ width: "60%" }}
|
||||||
/>
|
/>
|
||||||
<div className="p-3 rounded-full w-fit">
|
<div className="p-3 bg-emerald-500/20 rounded-full w-fit">
|
||||||
<div className="w-6 h-6 bg-gray-600 rounded animate-pulse" />
|
<div className="w-6 h-6 bg-gray-600 rounded animate-pulse" />
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
@ -670,112 +618,39 @@ export function ProcessInnovationPage() {
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</BaseCard>
|
</BaseCard>
|
||||||
))}
|
))
|
||||||
</div>
|
: Object.entries(stateCard).map(([key, card]) => {
|
||||||
) : (
|
// map percent values for each card key
|
||||||
<div className="flex flex-col h-full gap-5">
|
const percentMap: Record<string, number | string | undefined> = {
|
||||||
<div className="flex flex-row gap-4 h-full">
|
productionstopsprevention: stats.percentProductionStops,
|
||||||
<BaseCard
|
bottleneckremoval: stats.percentBottleneckRemoval,
|
||||||
key={stateCard.productionstopsprevention.id}
|
currencyreduction: stats.percentCurrencyReduction,
|
||||||
title={stateCard.productionstopsprevention.title}
|
frequentfailuresreduction: stats.percentFailuresReduction,
|
||||||
className="border-gray-700/50 w-full"
|
};
|
||||||
icon={stateCard.productionstopsprevention.icon}
|
const percentValue = percentMap[key];
|
||||||
>
|
|
||||||
<div className="flex items-center justify-center flex-col">
|
|
||||||
<div className="flex items-center gap-4">
|
|
||||||
<div className="text-center">
|
|
||||||
<p className="text-3xl text-pr-green font-bold mb-1">
|
|
||||||
{stateCard.productionstopsprevention.value}
|
|
||||||
</p>
|
|
||||||
<div className="text-[11px] text-[#ACACAC] font-light font-persian">
|
|
||||||
{stateCard.productionstopsprevention.description}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</BaseCard>
|
|
||||||
|
|
||||||
<BaseCard
|
return (
|
||||||
key={stateCard.frequentfailuresreduction.id}
|
<BaseCard
|
||||||
title={stateCard.frequentfailuresreduction.title}
|
key={card.id}
|
||||||
className="border-gray-700/50 w-full"
|
title={card.title}
|
||||||
icon={stateCard.frequentfailuresreduction.icon}
|
className="border-gray-700/50"
|
||||||
>
|
icon={card.icon}
|
||||||
<div className="flex items-center justify-center flex-col">
|
>
|
||||||
<div className="flex items-center gap-4">
|
<div className="flex items-center justify-center flex-col">
|
||||||
<div className="text-center">
|
<div className="flex items-center gap-4">
|
||||||
<p className="text-3xl text-pr-green font-bold mb-1">
|
<div className="text-center">
|
||||||
{stateCard.frequentfailuresreduction.value}
|
<p className="text-3xl text-pr-green font-bold mb-1">
|
||||||
</p>
|
{(card.value)}
|
||||||
<div className="text-[11px] text-[#ACACAC] font-light font-persian">
|
</p>
|
||||||
{stateCard.frequentfailuresreduction.description}
|
<div className="text-[11px] text-[#ACACAC] font-light font-persian">
|
||||||
|
{card.description}
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</BaseCard>
|
||||||
</BaseCard>
|
);
|
||||||
</div>
|
})}
|
||||||
<div className="flex flex-row gap-4 h-full">
|
|
||||||
<BaseCard
|
|
||||||
key={stateCard.currencyreduction.id}
|
|
||||||
title={stateCard.currencyreduction.title}
|
|
||||||
className="border-gray-700/50 w-full"
|
|
||||||
icon={stateCard.currencyreduction.icon}
|
|
||||||
>
|
|
||||||
<div className="flex items-center justify-center flex-col">
|
|
||||||
<div className="flex items-center gap-4">
|
|
||||||
<div className="text-center">
|
|
||||||
<p className="text-3xl text-pr-green font-bold mb-1">
|
|
||||||
{stateCard.currencyreduction.value}
|
|
||||||
</p>
|
|
||||||
<div className="text-[11px] text-[#ACACAC] font-light font-persian">
|
|
||||||
{stateCard.currencyreduction.description}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</BaseCard>
|
|
||||||
<BaseCard
|
|
||||||
key={stateCard.decreaseCurrencyOperation.id}
|
|
||||||
title={stateCard.decreaseCurrencyOperation.title}
|
|
||||||
className="border-gray-700/50 w-full"
|
|
||||||
icon={stateCard.decreaseCurrencyOperation.icon}
|
|
||||||
>
|
|
||||||
<div className="flex items-center justify-center flex-col">
|
|
||||||
<div className="flex items-center gap-4">
|
|
||||||
<div className="text-center">
|
|
||||||
<p className="text-3xl text-pr-green font-bold mb-1">
|
|
||||||
{stateCard.decreaseCurrencyOperation.value}
|
|
||||||
</p>
|
|
||||||
<div className="text-[11px] text-[#ACACAC] font-light font-persian">
|
|
||||||
{stateCard.decreaseCurrencyOperation.description}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</BaseCard>
|
|
||||||
<BaseCard
|
|
||||||
key={stateCard.bottleneckremoval.id}
|
|
||||||
title={stateCard.bottleneckremoval.title}
|
|
||||||
className="border-gray-700/50 w-full"
|
|
||||||
icon={stateCard.bottleneckremoval.icon}
|
|
||||||
>
|
|
||||||
<div className="flex items-center justify-center flex-col">
|
|
||||||
<div className="flex items-center gap-4">
|
|
||||||
<div className="text-center">
|
|
||||||
<p className="text-3xl text-pr-green font-bold mb-1">
|
|
||||||
{stateCard.bottleneckremoval.value}
|
|
||||||
</p>
|
|
||||||
<div className="text-[11px] text-[#ACACAC] font-light font-persian">
|
|
||||||
{stateCard.bottleneckremoval.description}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</BaseCard>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|
@ -783,16 +658,16 @@ export function ProcessInnovationPage() {
|
||||||
{/* نمودار با الگوریتم Nice Numbers:
|
{/* نمودار با الگوریتم Nice Numbers:
|
||||||
مثلاً اگر دادهها [10, 35, 63, 18] باشند:
|
مثلاً اگر دادهها [10, 35, 63, 18] باشند:
|
||||||
- حداکثر: 63، با حاشیه 5% = 66.15
|
- حداکثر: 63، با حاشیه 5% = 66.15
|
||||||
- Nice Max: 75 (گرد و خوانا)
|
- Nice Max: 75 (گرد و خوانا)
|
||||||
- Ticks: [0, 20, 40, 60, 75]
|
- Ticks: [0, 20, 40, 60, 75]
|
||||||
این باعث میشود نمودار زیباتر و خواناتر باشد */}
|
این باعث میشود نمودار زیباتر و خواناتر باشد */}
|
||||||
<BaseCard className="rounded-xl w-full overflow-hidden">
|
<BaseCard className="rounded-2xl w-full overflow-hidden">
|
||||||
<CustomBarChart
|
<CustomBarChart
|
||||||
title="تاثیرات فرآیندی به صورت درصد مقایسه ای"
|
title="تاثیرات فرآیندی به صورت درصد مقایسه ای"
|
||||||
loading={statsLoading}
|
loading={statsLoading}
|
||||||
data={[
|
data={[
|
||||||
{
|
{
|
||||||
label: "توقفات تولید",
|
label: "کاهش توقفات تولید",
|
||||||
value: Number(stats.percentProductionStops) || 0,
|
value: Number(stats.percentProductionStops) || 0,
|
||||||
labelColor: "text-white",
|
labelColor: "text-white",
|
||||||
},
|
},
|
||||||
|
|
@ -802,23 +677,17 @@ export function ProcessInnovationPage() {
|
||||||
labelColor: "text-white",
|
labelColor: "text-white",
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
label: "ارز بری",
|
label: "کاهش ارز بری",
|
||||||
value: Number(stats.percentCurrencyReduction) || 0,
|
value: Number(stats.percentCurrencyReduction) || 0,
|
||||||
labelColor: "text-white",
|
labelColor: "text-white",
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
label: "خرابی پر تکرار",
|
label: "کاهش خرابی پر تکرار",
|
||||||
value: Number(stats.percentFailuresReduction) || 0,
|
value: Number(stats.percentFailuresReduction) || 0,
|
||||||
labelColor: "text-white",
|
labelColor: "text-white",
|
||||||
},
|
},
|
||||||
{
|
|
||||||
label: "هزینه های عملیاتی",
|
|
||||||
value:
|
|
||||||
Number(stats.percentOperatingCostBeforeInnovation) || 0,
|
|
||||||
labelColor: "text-white",
|
|
||||||
},
|
|
||||||
]}
|
]}
|
||||||
barHeight="h-5"
|
barHeight="h-6"
|
||||||
showAxisLabels={true}
|
showAxisLabels={true}
|
||||||
/>
|
/>
|
||||||
</BaseCard>
|
</BaseCard>
|
||||||
|
|
@ -828,7 +697,7 @@ export function ProcessInnovationPage() {
|
||||||
<Card className="bg-transparent backdrop-blur-sm rounded-2xl overflow-hidden">
|
<Card className="bg-transparent backdrop-blur-sm rounded-2xl overflow-hidden">
|
||||||
<CardContent className="p-0">
|
<CardContent className="p-0">
|
||||||
<div className="relative">
|
<div className="relative">
|
||||||
<Table containerClassName="overflow-auto custom-scrollbar max-h-[calc(90vh-420px)]">
|
<Table containerClassName="overflow-auto custom-scrollbar max-h-[calc(90vh-400px)]">
|
||||||
<TableHeader>
|
<TableHeader>
|
||||||
<TableRow className="bg-[#3F415A]">
|
<TableRow className="bg-[#3F415A]">
|
||||||
{columns.map((column) => (
|
{columns.map((column) => (
|
||||||
|
|
@ -839,7 +708,14 @@ export function ProcessInnovationPage() {
|
||||||
>
|
>
|
||||||
{column.key === "select" ? (
|
{column.key === "select" ? (
|
||||||
<div className="flex items-center justify-center">
|
<div className="flex items-center justify-center">
|
||||||
<span></span>
|
<Checkbox
|
||||||
|
checked={
|
||||||
|
selectedProjects.size === projects.length &&
|
||||||
|
projects.length > 0
|
||||||
|
}
|
||||||
|
onCheckedChange={handleSelectAll}
|
||||||
|
className="data-[state=checked]:bg-emerald-600 data-[state=checked]:border-emerald-600"
|
||||||
|
/>
|
||||||
</div>
|
</div>
|
||||||
) : column.sortable ? (
|
) : column.sortable ? (
|
||||||
<button
|
<button
|
||||||
|
|
@ -875,7 +751,7 @@ export function ProcessInnovationPage() {
|
||||||
{columns.map((column) => (
|
{columns.map((column) => (
|
||||||
<TableCell
|
<TableCell
|
||||||
key={column.key}
|
key={column.key}
|
||||||
className="text-right whitespace-nowrap border-pr-green py-1 px-2"
|
className="text-right whitespace-nowrap border-emerald-500/20 py-1 px-2"
|
||||||
>
|
>
|
||||||
<div className="flex items-center gap-2">
|
<div className="flex items-center gap-2">
|
||||||
<div className="w-2.5 h-2.5 bg-gray-600 rounded-full animate-pulse" />
|
<div className="w-2.5 h-2.5 bg-gray-600 rounded-full animate-pulse" />
|
||||||
|
|
@ -908,7 +784,7 @@ export function ProcessInnovationPage() {
|
||||||
{columns.map((column) => (
|
{columns.map((column) => (
|
||||||
<TableCell
|
<TableCell
|
||||||
key={column.key}
|
key={column.key}
|
||||||
className={`text-right whitespace-nowrap border-pr-green py-1 px-2 ${column.key === "select" ? "flex justify-center items-center" : ""}`}
|
className={`text-right whitespace-nowrap border-emerald-500/20 py-1 px-2 ${column.key === "select" ? "flex justify-center items-center" : ""}`}
|
||||||
>
|
>
|
||||||
{renderCellContent(project, column)}
|
{renderCellContent(project, column)}
|
||||||
</TableCell>
|
</TableCell>
|
||||||
|
|
@ -925,7 +801,7 @@ export function ProcessInnovationPage() {
|
||||||
{loadingMore && (
|
{loadingMore && (
|
||||||
<div className="flex items-center justify-center py-1">
|
<div className="flex items-center justify-center py-1">
|
||||||
<div className="flex items-center gap-2">
|
<div className="flex items-center gap-2">
|
||||||
<RefreshCw className="w-4 h-4 animate-spin text-pr-green" />
|
<RefreshCw className="w-4 h-4 animate-spin text-emerald-400" />
|
||||||
<span className="font-persian text-gray-300 text-xs"></span>
|
<span className="font-persian text-gray-300 text-xs"></span>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
@ -970,12 +846,10 @@ export function ProcessInnovationPage() {
|
||||||
شرح پروژه
|
شرح پروژه
|
||||||
</DialogTitle>
|
</DialogTitle>
|
||||||
</DialogHeader>
|
</DialogHeader>
|
||||||
<div className="space-y-4 flex justify-between text-right p-6">
|
<div className="space-y-4 flex justify-between text-right px-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">
|
<h2 className="font-bold text-base">{selectedProjectDetails?.title}</h2>
|
||||||
{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>
|
||||||
|
|
@ -987,7 +861,7 @@ export function ProcessInnovationPage() {
|
||||||
|
|
||||||
<div className="flex items-center justify-between">
|
<div className="flex items-center justify-between">
|
||||||
<h4 className="font-light text-sm text-white font-persian mb-2 flex items-center gap-1">
|
<h4 className="font-light text-sm text-white font-persian mb-2 flex items-center gap-1">
|
||||||
<Building2 className="h-4 text-pr-green text-sm font-light" />
|
<Building2 className="h-4 text-green-500 text-sm font-light" />
|
||||||
زمان شروع:
|
زمان شروع:
|
||||||
</h4>
|
</h4>
|
||||||
<span className="text-white font-normal text-base font-persian">
|
<span className="text-white font-normal text-base font-persian">
|
||||||
|
|
@ -1002,7 +876,7 @@ export function ProcessInnovationPage() {
|
||||||
|
|
||||||
<div className="flex items-center justify-between">
|
<div className="flex items-center justify-between">
|
||||||
<h4 className="font-light text-sm text-white font-persian mb-2 flex items-center gap-1">
|
<h4 className="font-light text-sm text-white font-persian mb-2 flex items-center gap-1">
|
||||||
<PickaxeIcon className="h-4 text-pr-green text-sm font-light" />
|
<PickaxeIcon className="h-4 text-green-500 text-sm font-light" />
|
||||||
زمان پایان:
|
زمان پایان:
|
||||||
</h4>
|
</h4>
|
||||||
<span className="text-white font-normal text-base font-persian">
|
<span className="text-white font-normal text-base font-persian">
|
||||||
|
|
@ -1017,7 +891,7 @@ export function ProcessInnovationPage() {
|
||||||
|
|
||||||
<div className="flex items-center justify-between">
|
<div className="flex items-center justify-between">
|
||||||
<h4 className="font-light text-sm text-white font-persian mb-2 flex items-center gap-1">
|
<h4 className="font-light text-sm text-white font-persian mb-2 flex items-center gap-1">
|
||||||
<UsersIcon className="h-4 text-pr-green text-sm font-light" />
|
<UsersIcon className="h-4 text-green-500 text-sm font-light" />
|
||||||
هزینه برآورد شده:
|
هزینه برآورد شده:
|
||||||
</h4>
|
</h4>
|
||||||
<span className="text-white font-normal text-base font-persian">
|
<span className="text-white font-normal text-base font-persian">
|
||||||
|
|
@ -1035,7 +909,7 @@ export function ProcessInnovationPage() {
|
||||||
</div>
|
</div>
|
||||||
<div className="flex items-center justify-between">
|
<div className="flex items-center justify-between">
|
||||||
<h4 className="font-light text-sm text-white font-persian mb-2 flex items-center gap-1">
|
<h4 className="font-light text-sm text-white font-persian mb-2 flex items-center gap-1">
|
||||||
<UserIcon className="h-4 text-pr-green text-sm font-light" />
|
<UserIcon className="h-4 text-green-500 text-sm font-light" />
|
||||||
نفر مرتبط:
|
نفر مرتبط:
|
||||||
</h4>
|
</h4>
|
||||||
<span className="text-white font-normal text-base font-persian">
|
<span className="text-white font-normal text-base font-persian">
|
||||||
|
|
|
||||||
File diff suppressed because it is too large
Load Diff
|
|
@ -1,8 +1,6 @@
|
||||||
import { saveAs } from "file-saver";
|
|
||||||
import { ChevronDown, ChevronUp, RefreshCw } from "lucide-react";
|
import { ChevronDown, ChevronUp, RefreshCw } from "lucide-react";
|
||||||
import { useCallback, useEffect, useMemo, useRef, useState } from "react";
|
import { useCallback, useEffect, useRef, useState, useMemo } from "react";
|
||||||
import toast from "react-hot-toast";
|
import toast from "react-hot-toast";
|
||||||
import XLSX from "xlsx-js-style";
|
|
||||||
import { Badge } from "~/components/ui/badge";
|
import { Badge } from "~/components/ui/badge";
|
||||||
import { Card, CardContent } from "~/components/ui/card";
|
import { Card, CardContent } from "~/components/ui/card";
|
||||||
import {
|
import {
|
||||||
|
|
@ -14,15 +12,9 @@ import {
|
||||||
TableHeader,
|
TableHeader,
|
||||||
TableRow,
|
TableRow,
|
||||||
} from "~/components/ui/table";
|
} from "~/components/ui/table";
|
||||||
import { useStoredDate } from "~/hooks/useStoredDate";
|
|
||||||
import apiService from "~/lib/api";
|
import apiService from "~/lib/api";
|
||||||
import {
|
import { formatCurrency } from "~/lib/utils";
|
||||||
EventBus,
|
import { formatNumber } from "~/lib/utils";
|
||||||
formatCurrency,
|
|
||||||
formatNumber,
|
|
||||||
handleDataValue,
|
|
||||||
} from "~/lib/utils";
|
|
||||||
import type { CalendarDate } from "~/types/util.type";
|
|
||||||
import { DashboardLayout } from "../layout";
|
import { DashboardLayout } from "../layout";
|
||||||
|
|
||||||
interface ProjectData {
|
interface ProjectData {
|
||||||
|
|
@ -177,12 +169,6 @@ export function ProjectManagementPage() {
|
||||||
const fetchingRef = useRef(false);
|
const fetchingRef = useRef(false);
|
||||||
const scrollTimeoutRef = useRef<NodeJS.Timeout | null>(null);
|
const scrollTimeoutRef = useRef<NodeJS.Timeout | null>(null);
|
||||||
const scrollContainerRef = useRef<HTMLDivElement>(null);
|
const scrollContainerRef = useRef<HTMLDivElement>(null);
|
||||||
// const [date, setDate] = useState<CalendarDate>({
|
|
||||||
// start: `${jy}/01/01`,
|
|
||||||
// end: `${jy}/12/30`,
|
|
||||||
// });
|
|
||||||
|
|
||||||
const [date, setDate] = useStoredDate();
|
|
||||||
|
|
||||||
const fetchProjects = async (reset = false) => {
|
const fetchProjects = async (reset = false) => {
|
||||||
// Prevent concurrent API calls
|
// Prevent concurrent API calls
|
||||||
|
|
@ -214,10 +200,7 @@ export function ProjectManagementPage() {
|
||||||
OutputFields: outputFields,
|
OutputFields: outputFields,
|
||||||
Pagination: { PageNumber: pageToFetch, PageSize: pageSize },
|
Pagination: { PageNumber: pageToFetch, PageSize: pageSize },
|
||||||
Sorts: sortField ? [[sortField, sortConfig.direction]] : [],
|
Sorts: sortField ? [[sortField, sortConfig.direction]] : [],
|
||||||
Conditions: [
|
Conditions: [],
|
||||||
["start_date", ">=", date?.start || null, "and"],
|
|
||||||
["start_date", "<=", date?.end || null],
|
|
||||||
],
|
|
||||||
});
|
});
|
||||||
|
|
||||||
if (response.state === 0) {
|
if (response.state === 0) {
|
||||||
|
|
@ -282,17 +265,6 @@ export function ProjectManagementPage() {
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
const handler = (date: CalendarDate) => {
|
|
||||||
if (date) setDate(date);
|
|
||||||
};
|
|
||||||
|
|
||||||
EventBus.on("dateSelected", handler);
|
|
||||||
|
|
||||||
return () => {
|
|
||||||
EventBus.off("dateSelected", handler);
|
|
||||||
};
|
|
||||||
}, []);
|
|
||||||
const loadMore = useCallback(() => {
|
const loadMore = useCallback(() => {
|
||||||
if (hasMore && !loading && !loadingMore && !fetchingRef.current) {
|
if (hasMore && !loading && !loadingMore && !fetchingRef.current) {
|
||||||
setCurrentPage((prev) => prev + 1);
|
setCurrentPage((prev) => prev + 1);
|
||||||
|
|
@ -300,11 +272,9 @@ export function ProjectManagementPage() {
|
||||||
}, [hasMore, loading, loadingMore]);
|
}, [hasMore, loading, loadingMore]);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (date.end && date.start) {
|
fetchProjects(true);
|
||||||
fetchProjects(true);
|
fetchTotalCount();
|
||||||
fetchTotalCount();
|
}, [sortConfig]);
|
||||||
}
|
|
||||||
}, [sortConfig, date]);
|
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (currentPage > 1) {
|
if (currentPage > 1) {
|
||||||
|
|
@ -317,8 +287,7 @@ export function ProjectManagementPage() {
|
||||||
const scrollContainer = scrollContainerRef.current;
|
const scrollContainer = scrollContainerRef.current;
|
||||||
|
|
||||||
const handleScroll = () => {
|
const handleScroll = () => {
|
||||||
if (!scrollContainer || !hasMore || loadingMore || fetchingRef.current)
|
if (!scrollContainer || !hasMore || loadingMore || fetchingRef.current) return;
|
||||||
return;
|
|
||||||
|
|
||||||
// Clear previous timeout
|
// Clear previous timeout
|
||||||
if (scrollTimeoutRef.current) {
|
if (scrollTimeoutRef.current) {
|
||||||
|
|
@ -338,9 +307,7 @@ export function ProjectManagementPage() {
|
||||||
};
|
};
|
||||||
|
|
||||||
if (scrollContainer) {
|
if (scrollContainer) {
|
||||||
scrollContainer.addEventListener("scroll", handleScroll, {
|
scrollContainer.addEventListener("scroll", handleScroll, { passive: true });
|
||||||
passive: true,
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return () => {
|
return () => {
|
||||||
|
|
@ -370,10 +337,7 @@ export function ProjectManagementPage() {
|
||||||
const response = await apiService.select({
|
const response = await apiService.select({
|
||||||
ProcessName: "project",
|
ProcessName: "project",
|
||||||
OutputFields: ["count(project_no)"],
|
OutputFields: ["count(project_no)"],
|
||||||
Conditions: [
|
Conditions: [],
|
||||||
["start_date", ">=", date?.start || null, "and"],
|
|
||||||
["start_date", "<=", date?.end || null],
|
|
||||||
],
|
|
||||||
});
|
});
|
||||||
|
|
||||||
if (response.state === 0) {
|
if (response.state === 0) {
|
||||||
|
|
@ -394,14 +358,14 @@ export function ProjectManagementPage() {
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
// const handleRefresh = () => {
|
const handleRefresh = () => {
|
||||||
// fetchingRef.current = false; // Reset fetching state on refresh
|
fetchingRef.current = false; // Reset fetching state on refresh
|
||||||
// setCurrentPage(1);
|
setCurrentPage(1);
|
||||||
// setProjects([]);
|
setProjects([]);
|
||||||
// setHasMore(true);
|
setHasMore(true);
|
||||||
// fetchProjects(true);
|
fetchProjects(true);
|
||||||
// fetchTotalCount();
|
fetchTotalCount();
|
||||||
// };
|
};
|
||||||
|
|
||||||
// ...existing code...
|
// ...existing code...
|
||||||
|
|
||||||
|
|
@ -666,7 +630,7 @@ export function ProjectManagementPage() {
|
||||||
.filter((v) => v !== null) as number[];
|
.filter((v) => v !== null) as number[];
|
||||||
res["remaining_time"] = remainingValues.length
|
res["remaining_time"] = remainingValues.length
|
||||||
? Math.round(
|
? Math.round(
|
||||||
remainingValues.reduce((a, b) => a + b, 0) / remainingValues.length
|
remainingValues.reduce((a, b) => a + b, 0) / remainingValues.length,
|
||||||
)
|
)
|
||||||
: null;
|
: null;
|
||||||
|
|
||||||
|
|
@ -680,7 +644,7 @@ export function ProjectManagementPage() {
|
||||||
const num = Number(
|
const num = Number(
|
||||||
String(raw)
|
String(raw)
|
||||||
.toString()
|
.toString()
|
||||||
.replace(/[^0-9.-]/g, "")
|
.replace(/[^0-9.-]/g, ""),
|
||||||
);
|
);
|
||||||
return Number.isFinite(num) ? num : NaN;
|
return Number.isFinite(num) ? num : NaN;
|
||||||
})
|
})
|
||||||
|
|
@ -797,94 +761,16 @@ export function ProjectManagementPage() {
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
// const totalPages = Math.ceil(totalCount / pageSize);
|
const totalPages = Math.ceil(totalCount / pageSize);
|
||||||
|
|
||||||
const exportToExcel = async () => {
|
|
||||||
let arr = [];
|
|
||||||
const data = await fetchExcelData();
|
|
||||||
debugger;
|
|
||||||
for (let i = 0; i < data.length; i++) {
|
|
||||||
let obj: Record<string, any> = {};
|
|
||||||
const project = data[i];
|
|
||||||
|
|
||||||
Object.entries(project).forEach(([pKey, pValue]) => {
|
|
||||||
Object.values(columns).forEach((col) => {
|
|
||||||
if (pKey === col.key) {
|
|
||||||
``;
|
|
||||||
obj[col.label] = handleDataValue(
|
|
||||||
pValue?.includes(",") ? pValue.replaceAll(",", "") : pValue
|
|
||||||
);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
arr.push(obj);
|
|
||||||
}
|
|
||||||
|
|
||||||
// تبدیل دادهها به worksheet
|
|
||||||
const worksheet = XLSX.utils.json_to_sheet(arr);
|
|
||||||
|
|
||||||
// ساخت workbook
|
|
||||||
const workbook = XLSX.utils.book_new();
|
|
||||||
XLSX.utils.book_append_sheet(workbook, worksheet, "People");
|
|
||||||
|
|
||||||
// تبدیل به فایل باینری
|
|
||||||
const excelBuffer = XLSX.write(workbook, {
|
|
||||||
bookType: "xlsx",
|
|
||||||
type: "array",
|
|
||||||
});
|
|
||||||
|
|
||||||
const blob = new Blob([excelBuffer], {
|
|
||||||
type: "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet",
|
|
||||||
});
|
|
||||||
saveAs(blob, "people.xls");
|
|
||||||
};
|
|
||||||
|
|
||||||
const fetchExcelData = async () => {
|
|
||||||
const fetchableColumns = columns.filter((c) => !c.computed);
|
|
||||||
const outputFields = fetchableColumns.map((c) => c.apiField ?? c.key);
|
|
||||||
const sortCol = columns.find((c) => c.key === sortConfig.field);
|
|
||||||
const sortField = sortCol?.computed
|
|
||||||
? undefined
|
|
||||||
: (sortCol?.apiField ?? sortCol?.key);
|
|
||||||
|
|
||||||
const response = await apiService.select({
|
|
||||||
ProcessName: "project",
|
|
||||||
OutputFields: outputFields,
|
|
||||||
Sorts: sortField ? [[sortField, sortConfig.direction]] : [],
|
|
||||||
Conditions: [
|
|
||||||
["start_date", ">=", date?.start || null, "and"],
|
|
||||||
["start_date", "<=", date?.end || null],
|
|
||||||
],
|
|
||||||
});
|
|
||||||
const parsedData = JSON.parse(response.data);
|
|
||||||
return parsedData;
|
|
||||||
};
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<DashboardLayout title="مدیریت پروژهها">
|
<DashboardLayout title="مدیریت پروژهها">
|
||||||
<div className="space-y-6">
|
<div className="p-6 space-y-6">
|
||||||
{/* <div className="flex justify-end w-full mb-0 pl-2">
|
|
||||||
<Button
|
|
||||||
className="flex w-max justify-center rounded-xl mb-4 border-gray-500/20 border-2 cursor-pointer transition-all hover:bg-[#3F415A]/50 bg-[#3F415A] py-3 text-center items-center gap-3 "
|
|
||||||
variant="ghost"
|
|
||||||
size="sm"
|
|
||||||
onClick={exportToExcel}
|
|
||||||
>
|
|
||||||
<FileChartColumnIncreasing />
|
|
||||||
دانلود فایل اکسل
|
|
||||||
</Button>
|
|
||||||
</div> */}
|
|
||||||
|
|
||||||
{/* Data Table */}
|
{/* Data Table */}
|
||||||
<Card className="bg-transparent backdrop-blur-sm rounded-2xl overflow-hidden">
|
<Card className="bg-transparent backdrop-blur-sm rounded-2xl overflow-hidden">
|
||||||
{/* <div onClick={exportToExcel}>DownloadExcle</div> */}
|
|
||||||
<CardContent className="p-0">
|
<CardContent className="p-0">
|
||||||
<div className="relative">
|
<div className="relative">
|
||||||
<div
|
<div ref={scrollContainerRef} className="relative overflow-auto custom-scrollbar max-h-[calc(100vh-120px)]">
|
||||||
ref={scrollContainerRef}
|
|
||||||
className="relative overflow-auto custom-scrollbar max-h-[calc(100vh-120px)]"
|
|
||||||
>
|
|
||||||
<Table className="table-fixed">
|
<Table className="table-fixed">
|
||||||
<TableHeader className="sticky top-0 z-50 bg-[#3F415A]">
|
<TableHeader className="sticky top-0 z-50 bg-[#3F415A]">
|
||||||
<TableRow className="bg-[#3F415A]">
|
<TableRow className="bg-[#3F415A]">
|
||||||
|
|
|
||||||
|
|
@ -1,16 +1,25 @@
|
||||||
import {
|
import {
|
||||||
|
Box,
|
||||||
|
Building2,
|
||||||
ChevronDown,
|
ChevronDown,
|
||||||
|
ChevronRight,
|
||||||
|
FolderKanban,
|
||||||
GalleryVerticalEnd,
|
GalleryVerticalEnd,
|
||||||
House,
|
Globe,
|
||||||
LightbulbIcon,
|
LayoutDashboard,
|
||||||
ListTodo,
|
Leaf,
|
||||||
|
Lightbulb,
|
||||||
LogOut,
|
LogOut,
|
||||||
Radar,
|
MonitorSmartphone,
|
||||||
|
Package,
|
||||||
Settings,
|
Settings,
|
||||||
Star,
|
Star,
|
||||||
Workflow,
|
Workflow,
|
||||||
DiscAlbum,
|
DiscAlbum,
|
||||||
LucideLightbulb
|
House,
|
||||||
|
ListTodo,
|
||||||
|
LightbulbIcon,
|
||||||
|
Radar
|
||||||
} from "lucide-react";
|
} from "lucide-react";
|
||||||
import React, { useState } from "react";
|
import React, { useState } from "react";
|
||||||
import { Link, useLocation } from "react-router";
|
import { Link, useLocation } from "react-router";
|
||||||
|
|
@ -40,6 +49,7 @@ interface MenuItem {
|
||||||
}
|
}
|
||||||
|
|
||||||
const menuItems: MenuItem[] = [
|
const menuItems: MenuItem[] = [
|
||||||
|
|
||||||
{
|
{
|
||||||
id: "dashboard",
|
id: "dashboard",
|
||||||
label: "صفحه اصلی",
|
label: "صفحه اصلی",
|
||||||
|
|
@ -98,24 +108,31 @@ const menuItems: MenuItem[] = [
|
||||||
{
|
{
|
||||||
id: "ideas",
|
id: "ideas",
|
||||||
label: "ایدههای فناوری و نوآوری",
|
label: "ایدههای فناوری و نوآوری",
|
||||||
icon: LucideLightbulb,
|
icon: House,
|
||||||
href: "/dashboard/manage-ideas-tech",
|
href: "/dashboard/manage-ideas-tech",
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
|
id: "top-innovations",
|
||||||
|
label: "نوآور برتر",
|
||||||
|
icon: Star,
|
||||||
|
href: "/dashboard/top-innovations",
|
||||||
|
},
|
||||||
|
{
|
||||||
id: "strategic-alignment",
|
id: "strategic-alignment",
|
||||||
label: "میزان انطباق راهبردی",
|
label: "میزان انطباق راهبردی",
|
||||||
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[] = [
|
||||||
// {
|
{
|
||||||
// id: "settings",
|
id: "settings",
|
||||||
// label: "تنظیمات",
|
label: "تنظیمات",
|
||||||
// icon: Settings,
|
icon: Settings,
|
||||||
// href: "/dashboard/settings",
|
href: "/dashboard/settings",
|
||||||
// },
|
},
|
||||||
{
|
{
|
||||||
id: "logout",
|
id: "logout",
|
||||||
label: "خروج",
|
label: "خروج",
|
||||||
|
|
@ -155,10 +172,7 @@ 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:
|
let activeIcon: React.ComponentType<{ className?: string }> | null | undefined = undefined;
|
||||||
| 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(
|
||||||
|
|
@ -176,10 +190,7 @@ export function Sidebar({
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
if (onTitleChange) {
|
if (onTitleChange) {
|
||||||
onTitleChange({
|
onTitleChange({ title: activeTitle ?? "صفحه اول", icon: activeIcon ?? null });
|
||||||
title: activeTitle ?? "صفحه اول",
|
|
||||||
icon: activeIcon ?? null,
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
@ -250,7 +261,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}
|
||||||
>
|
>
|
||||||
|
|
@ -260,7 +271,8 @@ export function Sidebar({
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
</button>
|
</button>
|
||||||
);
|
)
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
|
|
@ -319,10 +331,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}
|
||||||
>
|
>
|
||||||
|
|
@ -423,9 +435,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,80 +1,33 @@
|
||||||
import { useEffect, useReducer, useRef, useState } from "react";
|
import React, { useEffect, useState } from "react";
|
||||||
|
import {
|
||||||
|
Dialog,
|
||||||
|
DialogContent,
|
||||||
|
DialogHeader,
|
||||||
|
DialogTitle,
|
||||||
|
} from "~/components/ui/dialog";
|
||||||
import {
|
import {
|
||||||
Bar,
|
|
||||||
BarChart,
|
BarChart,
|
||||||
CartesianGrid,
|
Bar,
|
||||||
Cell,
|
|
||||||
LabelList,
|
|
||||||
ResponsiveContainer,
|
|
||||||
XAxis,
|
XAxis,
|
||||||
YAxis,
|
YAxis,
|
||||||
|
CartesianGrid,
|
||||||
|
Tooltip,
|
||||||
|
ResponsiveContainer,
|
||||||
|
LabelList,
|
||||||
|
Cell,
|
||||||
} from "recharts";
|
} from "recharts";
|
||||||
import { Dialog, DialogContent, DialogHeader } from "~/components/ui/dialog";
|
|
||||||
import { Skeleton } from "~/components/ui/skeleton";
|
|
||||||
import { useStoredDate } from "~/hooks/useStoredDate";
|
|
||||||
import apiService from "~/lib/api";
|
import apiService from "~/lib/api";
|
||||||
import { EventBus, formatNumber } from "~/lib/utils";
|
import { Skeleton } from "~/components/ui/skeleton";
|
||||||
import type { CalendarDate } from "~/types/util.type";
|
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 {
|
||||||
strategic_theme: string;
|
strategic_theme: string;
|
||||||
operational_fee_count: number;
|
operational_fee_sum: number;
|
||||||
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;
|
||||||
|
|
@ -88,10 +41,11 @@ 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">
|
||||||
|
|
@ -104,7 +58,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" />
|
||||||
|
|
@ -120,14 +74,6 @@ 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: [],
|
|
||||||
});
|
|
||||||
|
|
||||||
const [date, setDate] = useStoredDate();
|
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (open) {
|
if (open) {
|
||||||
|
|
@ -135,29 +81,16 @@ export function StrategicAlignmentPopup({
|
||||||
}
|
}
|
||||||
}, [open]);
|
}, [open]);
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
const handler = (date: CalendarDate) => {
|
|
||||||
if (date) setDate(date);
|
|
||||||
};
|
|
||||||
|
|
||||||
EventBus.on("dateSelected", handler);
|
|
||||||
|
|
||||||
return () => {
|
|
||||||
EventBus.off("dateSelected", handler);
|
|
||||||
};
|
|
||||||
}, []);
|
|
||||||
|
|
||||||
const fetchData = async () => {
|
const fetchData = async () => {
|
||||||
setLoading(true);
|
setLoading(true);
|
||||||
try {
|
try {
|
||||||
const response = await apiService.select({
|
const response = await apiService.select({
|
||||||
ProcessName: "project",
|
ProcessName: "project",
|
||||||
OutputFields: ["strategic_theme", "count(operational_fee)"],
|
OutputFields: [
|
||||||
GroupBy: ["strategic_theme"],
|
"strategic_theme",
|
||||||
Conditions: [
|
"sum(operational_fee) as operational_fee_sum",
|
||||||
["start_date", ">=", date?.start || null, "and"],
|
|
||||||
["start_date", "<=", date?.end || null],
|
|
||||||
],
|
],
|
||||||
|
GroupBy: ["strategic_theme"],
|
||||||
});
|
});
|
||||||
|
|
||||||
const responseData =
|
const responseData =
|
||||||
|
|
@ -165,12 +98,29 @@ export function StrategicAlignmentPopup({
|
||||||
? JSON.parse(response.data)
|
? JSON.parse(response.data)
|
||||||
: response.data;
|
: response.data;
|
||||||
|
|
||||||
setBarItems(responseData);
|
const processedData = responseData
|
||||||
const dropDownItems = responseData.map(
|
.map((item: any) => ({
|
||||||
(item: any) => item.strategic_theme
|
strategic_theme: item.strategic_theme || "N/A",
|
||||||
|
operational_fee_sum: Math.max(0, Number(item.operational_fee_sum)),
|
||||||
|
}))
|
||||||
|
.filter((item: StrategicAlignmentData) => item.strategic_theme !== "");
|
||||||
|
|
||||||
|
const total = processedData.reduce(
|
||||||
|
(acc: number, item: StrategicAlignmentData) =>
|
||||||
|
acc + item.operational_fee_sum,
|
||||||
|
0
|
||||||
);
|
);
|
||||||
|
|
||||||
setDropDownValues(["همه مضامین", ...dropDownItems]);
|
const dataWithPercentage = processedData.map(
|
||||||
|
(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 {
|
||||||
|
|
@ -178,174 +128,19 @@ export function StrategicAlignmentPopup({
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
const fetchDropDownItems = async (item: string) => {
|
|
||||||
try {
|
|
||||||
if (item !== "همه مضامین") {
|
|
||||||
const response = await apiService.select({
|
|
||||||
ProcessName: "project",
|
|
||||||
OutputFields: [
|
|
||||||
"value_technology_and_innovation",
|
|
||||||
"count(operational_fee)",
|
|
||||||
],
|
|
||||||
Conditions: [
|
|
||||||
["strategic_theme", "=", item, "and"],
|
|
||||||
["start_date", ">=", date?.start || null, "and"],
|
|
||||||
["start_date", "<=", date?.end || null],
|
|
||||||
],
|
|
||||||
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_count: Math.max(0, Number(item.operational_fee_count)),
|
|
||||||
}))
|
|
||||||
.filter((item: StrategicAlignmentData) => item.strategic_theme !== "");
|
|
||||||
|
|
||||||
const total = processedData.reduce(
|
|
||||||
(acc: number, item: StrategicAlignmentData) =>
|
|
||||||
acc + item.operational_fee_count,
|
|
||||||
0
|
|
||||||
);
|
|
||||||
|
|
||||||
const dataWithPercentage = processedData.map(
|
|
||||||
(item: StrategicAlignmentData) => ({
|
|
||||||
...item,
|
|
||||||
percentage:
|
|
||||||
total > 0
|
|
||||||
? Math.round((item.operational_fee_count / 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={dialogHandler}>
|
<Dialog open={open} onOpenChange={onOpenChange}>
|
||||||
<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 w-full border-b-2 border-gray-500/20">
|
<DialogHeader className="mb-10 py-2 w-full pb-4 border-b-2 border-gray-500/20">
|
||||||
<div>
|
<DialogTitle className="ml-auto text-sm text-white ">میزان انطباق راهبردی</DialogTitle>
|
||||||
<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
|
<ChartContainer config={chartConfig} className="aspect-auto h-96 w-full">
|
||||||
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 }}
|
||||||
|
|
@ -354,7 +149,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}
|
||||||
|
|
@ -366,8 +161,11 @@ 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 maxWords={2} text={payload.value} />
|
<TruncatedText
|
||||||
</foreignObject>
|
maxWords={2}
|
||||||
|
text={payload.value}
|
||||||
|
/>
|
||||||
|
</foreignObject>
|
||||||
</g>
|
</g>
|
||||||
);
|
);
|
||||||
}}
|
}}
|
||||||
|
|
@ -381,38 +179,37 @@ export function StrategicAlignmentPopup({
|
||||||
tickFormatter={(value) =>
|
tickFormatter={(value) =>
|
||||||
`${formatNumber(Math.round(value))}`
|
`${formatNumber(Math.round(value))}`
|
||||||
}
|
}
|
||||||
label={{
|
|
||||||
value: "تعداد برنامه ها",
|
|
||||||
angle: -90,
|
label={{
|
||||||
position: "insideLeft",
|
value: "تعداد برنامه ها" ,
|
||||||
fill: "#94a3b8",
|
angle: -90,
|
||||||
fontSize: 11,
|
position: "insideLeft",
|
||||||
offset: 0,
|
fill: "#94a3b8",
|
||||||
dy: 0,
|
fontSize: 11,
|
||||||
style: { textAnchor: "middle" },
|
offset: 0,
|
||||||
}}
|
dy: 0,
|
||||||
|
style: { textAnchor: "middle" },
|
||||||
|
}}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<Bar dataKey="percentage" radius={[8, 8, 0, 0]}>
|
<Bar dataKey="percentage" radius={[8, 8, 0, 0]}>
|
||||||
{data.map((entry, index) => (
|
{data.map((entry, index) => (
|
||||||
<Cell
|
<Cell key={`cell-${index}`} fill={chartConfig.percentage.color} />
|
||||||
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) =>
|
formatter={(v: number) => `${formatNumber(Math.round(v))}`}
|
||||||
`${formatNumber(Math.round(v))}`
|
|
||||||
}
|
|
||||||
/>
|
/>
|
||||||
|
|
||||||
</Bar>
|
</Bar>
|
||||||
</BarChart>
|
</BarChart>
|
||||||
</ChartContainer>
|
</ChartContainer>
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,7 @@
|
||||||
"use client";
|
"use client";
|
||||||
|
|
||||||
import { useEffect, useState } from "react";
|
import React, { useEffect, useState } from "react";
|
||||||
|
import { Card, CardContent, CardHeader, CardTitle } from "~/components/ui/card";
|
||||||
import {
|
import {
|
||||||
Area,
|
Area,
|
||||||
AreaChart,
|
AreaChart,
|
||||||
|
|
@ -10,12 +11,9 @@ import {
|
||||||
XAxis,
|
XAxis,
|
||||||
YAxis,
|
YAxis,
|
||||||
} from "recharts";
|
} from "recharts";
|
||||||
import { Card, CardContent, CardHeader, CardTitle } from "~/components/ui/card";
|
|
||||||
import { CustomBarChart } from "~/components/ui/custom-bar-chart";
|
import { CustomBarChart } from "~/components/ui/custom-bar-chart";
|
||||||
import { useStoredDate } from "~/hooks/useStoredDate";
|
|
||||||
import apiService from "~/lib/api";
|
import apiService from "~/lib/api";
|
||||||
import { EventBus, formatNumber } from "~/lib/utils";
|
import { formatNumber } from "~/lib/utils";
|
||||||
import type { CalendarDate } from "~/types/util.type";
|
|
||||||
|
|
||||||
export interface CompanyDetails {
|
export interface CompanyDetails {
|
||||||
id: string;
|
id: string;
|
||||||
|
|
@ -64,59 +62,38 @@ export function InfoPanel({ selectedCompany }: InfoPanelProps) {
|
||||||
const [counts, setCounts] = useState<EcosystemCounts | null>(null);
|
const [counts, setCounts] = useState<EcosystemCounts | null>(null);
|
||||||
const [processData, setProcessData] = useState<ProcessActorsData[]>([]);
|
const [processData, setProcessData] = useState<ProcessActorsData[]>([]);
|
||||||
const [isLoading, setIsLoading] = useState(true);
|
const [isLoading, setIsLoading] = useState(true);
|
||||||
// const [date, setDate] = useState<CalendarDate>();
|
|
||||||
const [date, setDate] = useStoredDate();
|
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const handler = (date: CalendarDate) => {
|
const fetchCounts = async () => {
|
||||||
if (date) setDate(date);
|
setIsLoading(true);
|
||||||
};
|
try {
|
||||||
|
const [countsRes, processRes] = await Promise.all([
|
||||||
|
apiService.call<EcosystemCounts>({
|
||||||
|
ecosystem_count_function: {},
|
||||||
|
}),
|
||||||
|
apiService.call<ProcessActorsResponse[]>({
|
||||||
|
process_creating_actors_function: {},
|
||||||
|
}),
|
||||||
|
]);
|
||||||
|
|
||||||
EventBus.on("dateSelected", handler);
|
setCounts(
|
||||||
|
JSON.parse(JSON.parse(countsRes.data).ecosystem_count_function)[0],
|
||||||
|
);
|
||||||
|
|
||||||
return () => {
|
// Process the years data and fill missing years
|
||||||
EventBus.off("dateSelected", handler);
|
const processedData = processYearsData(
|
||||||
|
JSON.parse(JSON.parse(processRes?.data)?.process_creating_actors),
|
||||||
|
);
|
||||||
|
setProcessData(processedData);
|
||||||
|
} catch (err) {
|
||||||
|
console.error("Failed to fetch data:", err);
|
||||||
|
} finally {
|
||||||
|
setIsLoading(false);
|
||||||
|
}
|
||||||
};
|
};
|
||||||
|
fetchCounts();
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
if (date.end && date.start) fetchCounts();
|
|
||||||
}, [date]);
|
|
||||||
|
|
||||||
const fetchCounts = async () => {
|
|
||||||
setIsLoading(true);
|
|
||||||
try {
|
|
||||||
const [countsRes, processRes] = await Promise.all([
|
|
||||||
apiService.call<EcosystemCounts>({
|
|
||||||
ecosystem_count_function: {
|
|
||||||
start_date: date?.start || null,
|
|
||||||
end_date: date?.end || null,
|
|
||||||
},
|
|
||||||
}),
|
|
||||||
apiService.call<ProcessActorsResponse[]>({
|
|
||||||
process_creating_actors_function: {
|
|
||||||
start_date: date?.start || null,
|
|
||||||
end_date: date?.end || null,
|
|
||||||
},
|
|
||||||
}),
|
|
||||||
]);
|
|
||||||
|
|
||||||
setCounts(
|
|
||||||
JSON.parse(JSON.parse(countsRes.data).ecosystem_count_function)[0]
|
|
||||||
);
|
|
||||||
|
|
||||||
// Process the years data and fill missing years
|
|
||||||
const processedData = processYearsData(
|
|
||||||
JSON.parse(JSON.parse(processRes?.data)?.process_creating_actors)
|
|
||||||
);
|
|
||||||
setProcessData(processedData);
|
|
||||||
} catch (err) {
|
|
||||||
console.error("Failed to fetch data:", err);
|
|
||||||
} finally {
|
|
||||||
setIsLoading(false);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
// Helper function to safely parse numbers
|
// Helper function to safely parse numbers
|
||||||
const parseNumber = (value: string | undefined): number => {
|
const parseNumber = (value: string | undefined): number => {
|
||||||
if (!value || value === "") return 0;
|
if (!value || value === "") return 0;
|
||||||
|
|
@ -126,7 +103,7 @@ export function InfoPanel({ selectedCompany }: InfoPanelProps) {
|
||||||
|
|
||||||
// Helper function to process years data and fill missing years
|
// Helper function to process years data and fill missing years
|
||||||
const processYearsData = (
|
const processYearsData = (
|
||||||
data: ProcessActorsResponse[]
|
data: ProcessActorsResponse[],
|
||||||
): ProcessActorsData[] => {
|
): ProcessActorsData[] => {
|
||||||
if (!data || data.length === 0) return [];
|
if (!data || data.length === 0) return [];
|
||||||
|
|
||||||
|
|
@ -144,7 +121,7 @@ export function InfoPanel({ selectedCompany }: InfoPanelProps) {
|
||||||
acc[item.start_year] = item.total_count;
|
acc[item.start_year] = item.total_count;
|
||||||
return acc;
|
return acc;
|
||||||
},
|
},
|
||||||
{} as Record<string, number>
|
{} as Record<string, number>,
|
||||||
);
|
);
|
||||||
|
|
||||||
for (let year = minYear; year <= maxYear; year++) {
|
for (let year = minYear; year <= maxYear; year++) {
|
||||||
|
|
@ -190,7 +167,7 @@ export function InfoPanel({ selectedCompany }: InfoPanelProps) {
|
||||||
{ label: "شتابدهنده", value: parseNumber(counts.accelerator_count) },
|
{ label: "شتابدهنده", value: parseNumber(counts.accelerator_count) },
|
||||||
{ label: "دانشگاه", value: parseNumber(counts.university_count) },
|
{ label: "دانشگاه", value: parseNumber(counts.university_count) },
|
||||||
{ label: "صندوق های مالی", value: parseNumber(counts.fund_count) },
|
{ label: "صندوق های مالی", value: parseNumber(counts.fund_count) },
|
||||||
{ label: "تامین کننده", value: parseNumber(counts.company_count) },
|
{ label: "شرکت", value: parseNumber(counts.company_count) },
|
||||||
]
|
]
|
||||||
: [];
|
: [];
|
||||||
|
|
||||||
|
|
@ -279,7 +256,7 @@ export function InfoPanel({ selectedCompany }: InfoPanelProps) {
|
||||||
{Array.from({ length: 4 }).map((_, i) => (
|
{Array.from({ length: 4 }).map((_, i) => (
|
||||||
<div
|
<div
|
||||||
key={i}
|
key={i}
|
||||||
className="absolute w-2 h-2 bg-pr-green rounded-full animate-pulse"
|
className="absolute w-2 h-2 bg-green-400 rounded-full animate-pulse"
|
||||||
style={{
|
style={{
|
||||||
left: `${20 + i * 25}%`,
|
left: `${20 + i * 25}%`,
|
||||||
top: `${30 + Math.random() * 40}%`,
|
top: `${30 + Math.random() * 40}%`,
|
||||||
|
|
@ -310,7 +287,7 @@ export function InfoPanel({ selectedCompany }: InfoPanelProps) {
|
||||||
{/* Actor Count Skeleton */}
|
{/* Actor Count Skeleton */}
|
||||||
<CardHeader className="text-center pt-0 pb-4">
|
<CardHeader className="text-center pt-0 pb-4">
|
||||||
<div className="w-36 h-5 rounded animate-pulse mx-auto mb-2"></div>
|
<div className="w-36 h-5 rounded animate-pulse mx-auto mb-2"></div>
|
||||||
<div className="w-16 h-8 bg-pr-green bg-opacity-30 rounded animate-pulse mx-auto"></div>
|
<div className="w-16 h-8 bg-green-400 bg-opacity-30 rounded animate-pulse mx-auto"></div>
|
||||||
</CardHeader>
|
</CardHeader>
|
||||||
|
|
||||||
{/* Bar Chart Skeleton */}
|
{/* Bar Chart Skeleton */}
|
||||||
|
|
@ -385,7 +362,7 @@ export function InfoPanel({ selectedCompany }: InfoPanelProps) {
|
||||||
{Array.from({ length: 4 }).map((_, i) => (
|
{Array.from({ length: 4 }).map((_, i) => (
|
||||||
<div
|
<div
|
||||||
key={i}
|
key={i}
|
||||||
className="absolute w-2 h-2 bg-pr-green rounded-full animate-pulse"
|
className="absolute w-2 h-2 bg-green-400 rounded-full animate-pulse"
|
||||||
style={{
|
style={{
|
||||||
left: `${20 + i * 25}%`,
|
left: `${20 + i * 25}%`,
|
||||||
top: `${30 + Math.random() * 40}%`,
|
top: `${30 + Math.random() * 40}%`,
|
||||||
|
|
@ -401,7 +378,7 @@ export function InfoPanel({ selectedCompany }: InfoPanelProps) {
|
||||||
<CardContent className="pt-0 pb-6">
|
<CardContent className="pt-0 pb-6">
|
||||||
<div className="bg-[rgba(255,255,255,0.1)] rounded-lg p-4 text-center">
|
<div className="bg-[rgba(255,255,255,0.1)] rounded-lg p-4 text-center">
|
||||||
<div className="w-28 h-4 bg-gray-600 rounded animate-pulse mx-auto mb-1"></div>
|
<div className="w-28 h-4 bg-gray-600 rounded animate-pulse mx-auto mb-1"></div>
|
||||||
<div className="w-12 h-6 bg-pr-green bg-opacity-30 rounded animate-pulse mx-auto"></div>
|
<div className="w-12 h-6 bg-green-400 bg-opacity-30 rounded animate-pulse mx-auto"></div>
|
||||||
</div>
|
</div>
|
||||||
</CardContent>
|
</CardContent>
|
||||||
</Card>
|
</Card>
|
||||||
|
|
@ -431,8 +408,8 @@ export function InfoPanel({ selectedCompany }: InfoPanelProps) {
|
||||||
|
|
||||||
<CardHeader className="text-center pb-2 border-b-2 border-[#3F415A]">
|
<CardHeader className="text-center pb-2 border-b-2 border-[#3F415A]">
|
||||||
<CardTitle className="font-persian text-sm text-white flex justify-between px-4">
|
<CardTitle className="font-persian text-sm text-white flex justify-between px-4">
|
||||||
تعداد تفاهم نامه ها
|
تعداد تفاهم نامه ها
|
||||||
<span className="font-bold text-3xl">
|
<span className="font-bold text-3xl">
|
||||||
{formatNumber(counts.mou_count)}
|
{formatNumber(counts.mou_count)}
|
||||||
</span>
|
</span>
|
||||||
</CardTitle>
|
</CardTitle>
|
||||||
|
|
@ -455,7 +432,6 @@ export function InfoPanel({ selectedCompany }: InfoPanelProps) {
|
||||||
<CardContent className="flex-1 px-6 border-b-2 border-[#3F415A]">
|
<CardContent className="flex-1 px-6 border-b-2 border-[#3F415A]">
|
||||||
<div className="w-full">
|
<div className="w-full">
|
||||||
<CustomBarChart
|
<CustomBarChart
|
||||||
hasPercent={false}
|
|
||||||
data={barData.map((item) => ({
|
data={barData.map((item) => ({
|
||||||
label: item.label,
|
label: item.label,
|
||||||
value: item.value,
|
value: item.value,
|
||||||
|
|
@ -478,82 +454,70 @@ export function InfoPanel({ selectedCompany }: InfoPanelProps) {
|
||||||
</div>
|
</div>
|
||||||
<div className="h-42">
|
<div className="h-42">
|
||||||
{processData.length > 0 ? (
|
{processData.length > 0 ? (
|
||||||
<ResponsiveContainer width="100%" height="100%">
|
<ResponsiveContainer width="100%" height="100%">
|
||||||
<AreaChart
|
<AreaChart
|
||||||
accessibilityLayer
|
accessibilityLayer
|
||||||
data={processData}
|
data={processData}
|
||||||
margin={{ top: 25, right: 30, left: 0, bottom: 0 }}
|
margin={{ top: 25, right: 30, left: 0, bottom: 0 }}
|
||||||
>
|
>
|
||||||
<defs>
|
<defs>
|
||||||
<linearGradient
|
<linearGradient id="fillDesktop" x1="0" y1="0" x2="0" y2="1">
|
||||||
id="fillDesktop"
|
<stop offset="0%" stopColor="#3AEA83" stopOpacity={1} />
|
||||||
x1="0"
|
<stop offset="100%" stopColor="#3AEA83" stopOpacity={0} />
|
||||||
y1="0"
|
</linearGradient>
|
||||||
x2="0"
|
</defs>
|
||||||
y2="1"
|
|
||||||
>
|
|
||||||
<stop offset="0%" stopColor="#3AEA83" stopOpacity={1} />
|
|
||||||
<stop offset="100%" stopColor="#3AEA83" stopOpacity={0} />
|
|
||||||
</linearGradient>
|
|
||||||
</defs>
|
|
||||||
|
|
||||||
<CartesianGrid
|
<CartesianGrid
|
||||||
vertical={false}
|
vertical={false}
|
||||||
stroke="rgba(255,255,255,0.1)"
|
stroke="rgba(255,255,255,0.1)"
|
||||||
/>
|
/>
|
||||||
<XAxis
|
<XAxis
|
||||||
dataKey="year"
|
dataKey="year"
|
||||||
stroke="#9ca3af"
|
stroke="#9ca3af"
|
||||||
fontSize={12}
|
fontSize={12}
|
||||||
tickLine={false}
|
tickLine={false}
|
||||||
tickMargin={8}
|
tickMargin={8}
|
||||||
axisLine={false}
|
axisLine={false}
|
||||||
tickFormatter={formatPersianYear}
|
tickFormatter={formatPersianYear}
|
||||||
/>
|
/>
|
||||||
<YAxis
|
<YAxis
|
||||||
stroke="#9ca3af"
|
stroke="#9ca3af"
|
||||||
fontSize={12}
|
fontSize={12}
|
||||||
tickMargin={12}
|
tickMargin={12}
|
||||||
tickLine={false}
|
tickLine={false}
|
||||||
axisLine={false}
|
axisLine={false}
|
||||||
tickFormatter={(value) => formatNumber(value)}
|
tickFormatter={(value) => formatNumber(value)}
|
||||||
/>
|
/>
|
||||||
<Tooltip cursor={false} content={<></>} />
|
<Tooltip cursor={false} content={<></>} />
|
||||||
|
|
||||||
|
{/* ✅ Use gradient for fill */}
|
||||||
|
<Area
|
||||||
|
type="monotone"
|
||||||
|
dataKey="value"
|
||||||
|
stroke="#3AEA83"
|
||||||
|
fill="url(#fillDesktop)"
|
||||||
|
strokeWidth={2}
|
||||||
|
activeDot={({ cx, cy, payload }) => (
|
||||||
|
<g>
|
||||||
|
{/* Small circle */}
|
||||||
|
<circle cx={cx} cy={cy} r={5} fill="#3AEA83" stroke="#fff" strokeWidth={2} />
|
||||||
|
{/* Year label above point */}
|
||||||
|
<text
|
||||||
|
x={cx}
|
||||||
|
y={cy - 10}
|
||||||
|
textAnchor="middle"
|
||||||
|
fontSize={12}
|
||||||
|
fontWeight="bold"
|
||||||
|
fill="#3AEA83"
|
||||||
|
>
|
||||||
|
{formatPersianYear(payload.year)}
|
||||||
|
</text>
|
||||||
|
</g>
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
</AreaChart>
|
||||||
|
</ResponsiveContainer>
|
||||||
|
|
||||||
{/* ✅ Use gradient for fill */}
|
|
||||||
<Area
|
|
||||||
type="monotone"
|
|
||||||
dataKey="value"
|
|
||||||
stroke="#3AEA83"
|
|
||||||
fill="url(#fillDesktop)"
|
|
||||||
strokeWidth={2}
|
|
||||||
activeDot={({ cx, cy, payload }) => (
|
|
||||||
<g>
|
|
||||||
{/* Small circle */}
|
|
||||||
<circle
|
|
||||||
cx={cx}
|
|
||||||
cy={cy}
|
|
||||||
r={5}
|
|
||||||
fill="#3AEA83"
|
|
||||||
stroke="#fff"
|
|
||||||
strokeWidth={2}
|
|
||||||
/>
|
|
||||||
{/* Year label above point */}
|
|
||||||
<text
|
|
||||||
x={cx}
|
|
||||||
y={cy - 10}
|
|
||||||
textAnchor="middle"
|
|
||||||
fontSize={12}
|
|
||||||
fontWeight="bold"
|
|
||||||
fill="#3AEA83"
|
|
||||||
>
|
|
||||||
{formatPersianYear(payload.year)}
|
|
||||||
</text>
|
|
||||||
</g>
|
|
||||||
)}
|
|
||||||
/>
|
|
||||||
</AreaChart>
|
|
||||||
</ResponsiveContainer>
|
|
||||||
) : (
|
) : (
|
||||||
<div className="flex items-center justify-center h-full text-gray-400 font-persian">
|
<div className="flex items-center justify-center h-full text-gray-400 font-persian">
|
||||||
دادهای برای نمایش وجود ندارد
|
دادهای برای نمایش وجود ندارد
|
||||||
|
|
@ -561,6 +525,7 @@ export function InfoPanel({ selectedCompany }: InfoPanelProps) {
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
</CardContent>
|
</CardContent>
|
||||||
|
|
||||||
</Card>
|
</Card>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
|
|
|
||||||
|
|
@ -1,19 +1,11 @@
|
||||||
|
import React, { useEffect, useRef, useState, useCallback } from "react";
|
||||||
import * as d3 from "d3";
|
import * as d3 from "d3";
|
||||||
import { useCallback, useEffect, useRef, useState } from "react";
|
|
||||||
import { useStoredDate } from "~/hooks/useStoredDate";
|
|
||||||
import { EventBus } from "~/lib/utils";
|
|
||||||
import type { CalendarDate } from "~/types/util.type";
|
|
||||||
import { useAuth } from "../../contexts/auth-context";
|
|
||||||
import apiService from "../../lib/api";
|
import apiService from "../../lib/api";
|
||||||
|
import { useAuth } from "../../contexts/auth-context";
|
||||||
|
|
||||||
|
// Get API base URL at module level to avoid process.env access in browser
|
||||||
const API_BASE_URL =
|
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";
|
|
||||||
//آپادانا
|
|
||||||
import.meta.env.VITE_API_URL || "https://APADANA-IATM-back.pelekan.org/api";
|
|
||||||
//نوری
|
|
||||||
// import.meta.env.VITE_API_URL || "https://NOPC-IATM-back.pelekan.org/api";
|
|
||||||
|
|
||||||
|
|
||||||
export interface Node {
|
export interface Node {
|
||||||
id: string;
|
id: string;
|
||||||
|
|
@ -52,9 +44,9 @@ export interface CompanyDetails {
|
||||||
|
|
||||||
export interface NetworkGraphProps {
|
export interface NetworkGraphProps {
|
||||||
onNodeClick?: (node: CompanyDetails) => void;
|
onNodeClick?: (node: CompanyDetails) => void;
|
||||||
onLoadingChange?: (loading: boolean) => 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 {
|
||||||
|
|
@ -64,14 +56,12 @@ 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";
|
||||||
}
|
}
|
||||||
|
|
||||||
export function NetworkGraph({
|
export function NetworkGraph({ onNodeClick }: NetworkGraphProps) {
|
||||||
onNodeClick,
|
|
||||||
onLoadingChange,
|
|
||||||
}: NetworkGraphProps) {
|
|
||||||
const svgRef = useRef<SVGSVGElement | null>(null);
|
const svgRef = useRef<SVGSVGElement | null>(null);
|
||||||
const [nodes, setNodes] = useState<Node[]>([]);
|
const [nodes, setNodes] = useState<Node[]>([]);
|
||||||
const [links, setLinks] = useState<Link[]>([]);
|
const [links, setLinks] = useState<Link[]>([]);
|
||||||
|
|
@ -80,21 +70,7 @@ export function NetworkGraph({
|
||||||
const [error, setError] = useState<string | null>(null);
|
const [error, setError] = useState<string | null>(null);
|
||||||
const { token } = useAuth();
|
const { token } = useAuth();
|
||||||
|
|
||||||
// const [date, setDate] = useState<CalendarDate>();
|
// Ensure component only renders on client side
|
||||||
|
|
||||||
const [date, setDate] = useStoredDate();
|
|
||||||
useEffect(() => {
|
|
||||||
const handler = (date: CalendarDate) => {
|
|
||||||
if (date) setDate(date);
|
|
||||||
};
|
|
||||||
|
|
||||||
EventBus.on("dateSelected", handler);
|
|
||||||
|
|
||||||
return () => {
|
|
||||||
EventBus.off("dateSelected", handler);
|
|
||||||
};
|
|
||||||
}, []);
|
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (isBrowser()) {
|
if (isBrowser()) {
|
||||||
const timer = setTimeout(() => setIsMounted(true), 100);
|
const timer = setTimeout(() => setIsMounted(true), 100);
|
||||||
|
|
@ -102,27 +78,7 @@ export function NetworkGraph({
|
||||||
}
|
}
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
const getImageUrl = useCallback(
|
// Fetch data from API
|
||||||
(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,
|
|
||||||
// start_date: date?.start || null,
|
|
||||||
// end_date: date?.end || null,
|
|
||||||
},
|
|
||||||
});
|
|
||||||
},
|
|
||||||
[date]
|
|
||||||
);
|
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (!isMounted) return;
|
if (!isMounted) return;
|
||||||
|
|
||||||
|
|
@ -133,45 +89,28 @@ export function NetworkGraph({
|
||||||
setIsLoading(true);
|
setIsLoading(true);
|
||||||
try {
|
try {
|
||||||
const res = await apiService.call<any[]>({
|
const res = await apiService.call<any[]>({
|
||||||
graph_production_function: {
|
graph_production_function: {},
|
||||||
start_date: date.start || null,
|
|
||||||
end_date: date.end || null,
|
|
||||||
},
|
|
||||||
});
|
});
|
||||||
if (aborted) return;
|
if (aborted) return;
|
||||||
|
|
||||||
const data = parseApiResponse(JSON.parse(res.data)?.graph_production);
|
const data = parseApiResponse(JSON.parse(res.data)?.graph_production);
|
||||||
console.log(
|
console.log(
|
||||||
"All available fields in first item:",
|
"All available fields in first item:",
|
||||||
Object.keys(data[0] || {})
|
Object.keys(data[0] || {}),
|
||||||
);
|
);
|
||||||
|
|
||||||
// نود مرکزی
|
// Create center node
|
||||||
const centerNode: Node = {
|
const centerNode: Node = {
|
||||||
id: "center",
|
id: "center",
|
||||||
// label: "پتروشیمی بندر امام",
|
label: "پتروشیمی بندر امام", //مرکز زیست بوم
|
||||||
// label: "پتروشیمی نوری",
|
|
||||||
label: "پتروشیمی آپادانا",
|
|
||||||
category: "center",
|
category: "center",
|
||||||
stageid: 0,
|
stageid: 0,
|
||||||
isCenter: true,
|
isCenter: true,
|
||||||
};
|
};
|
||||||
|
|
||||||
// دستهبندیها
|
// Create ecosystem nodes
|
||||||
const categories = Array.from(
|
const ecosystemNodes: Node[] = data.map((item: any) => ({
|
||||||
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,
|
||||||
|
|
@ -179,16 +118,13 @@ export function NetworkGraph({
|
||||||
rawData: item,
|
rawData: item,
|
||||||
}));
|
}));
|
||||||
|
|
||||||
// لینکها: مرکز → دستهبندیها → نودهای نهایی
|
// Create links (all nodes connected to center)
|
||||||
const graphLinks: Link[] = [
|
const graphLinks: Link[] = ecosystemNodes.map((node) => ({
|
||||||
...categoryNodes.map((cat) => ({ source: "center", target: cat.id })),
|
source: "center",
|
||||||
...finalNodes.map((node) => {
|
target: node.id,
|
||||||
const catIndex = categories.indexOf(node.category);
|
}));
|
||||||
return { source: `cat-${catIndex}`, target: node.id };
|
|
||||||
}),
|
|
||||||
];
|
|
||||||
|
|
||||||
setNodes([centerNode, ...categoryNodes, ...finalNodes]);
|
setNodes([centerNode, ...ecosystemNodes]);
|
||||||
setLinks(graphLinks);
|
setLinks(graphLinks);
|
||||||
} catch (err: any) {
|
} catch (err: any) {
|
||||||
if (err.name !== "AbortError") {
|
if (err.name !== "AbortError") {
|
||||||
|
|
@ -206,19 +142,43 @@ export function NetworkGraph({
|
||||||
aborted = true;
|
aborted = true;
|
||||||
controller.abort();
|
controller.abort();
|
||||||
};
|
};
|
||||||
}, [isMounted, token, getImageUrl, date]);
|
}, [isMounted, token]);
|
||||||
|
|
||||||
|
// 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")
|
||||||
|
|
@ -236,27 +196,33 @@ export function NetworkGraph({
|
||||||
feMerge.append("feMergeNode").attr("in", "coloredBlur");
|
feMerge.append("feMergeNode").attr("in", "coloredBlur");
|
||||||
feMerge.append("feMergeNode").attr("in", "SourceGraphic");
|
feMerge.append("feMergeNode").attr("in", "SourceGraphic");
|
||||||
|
|
||||||
const container = svg.append("g");
|
// Create zoom behavior
|
||||||
|
|
||||||
const zoom = d3
|
const zoom = d3
|
||||||
.zoom<SVGSVGElement, unknown>()
|
.zoom<SVGSVGElement, unknown>()
|
||||||
.scaleExtent([0.3, 2.5])
|
.scaleExtent([0.8, 2.5]) // Limit zoom out to 1x, zoom in to 2.5x
|
||||||
.on("zoom", (event) => container.attr("transform", event.transform));
|
.on("zoom", (event) => {
|
||||||
|
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",
|
||||||
"دانش بنیان": "#F59E0B",
|
"دانش بنیان": "#F59E0B",
|
||||||
استارتاپ: "#EF4444",
|
استارتاپ: "#EF4444",
|
||||||
"تامین کننده": "#8B5CF6",
|
شرکت: "#8B5CF6",
|
||||||
صندوق: "#06B6D4",
|
صندوق: "#06B6D4",
|
||||||
شتابدهنده: "#9333EA",
|
شتابدهنده: "#9333EA",
|
||||||
"مرکز نوآوری": "#F472B6",
|
"مرکز نوآوری": "#F472B6",
|
||||||
center: "#34D399",
|
center: "#34D399",
|
||||||
};
|
};
|
||||||
|
|
||||||
|
// Create force simulation
|
||||||
const simulation = d3
|
const simulation = d3
|
||||||
.forceSimulation<Node>(nodes)
|
.forceSimulation<Node>(nodes)
|
||||||
.force(
|
.force(
|
||||||
|
|
@ -265,21 +231,16 @@ export function NetworkGraph({
|
||||||
.forceLink<Node, Link>(links)
|
.forceLink<Node, Link>(links)
|
||||||
.id((d) => d.id)
|
.id((d) => d.id)
|
||||||
.distance(150)
|
.distance(150)
|
||||||
.strength(0.2)
|
.strength(0.1),
|
||||||
)
|
)
|
||||||
.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(
|
|
||||||
"radial",
|
|
||||||
d3.forceRadial((d) => (d.isCenter ? 0 : 300), width / 2, height / 2)
|
|
||||||
)
|
|
||||||
.force(
|
.force(
|
||||||
"collision",
|
"collision",
|
||||||
d3.forceCollide().radius((d) => (d.isCenter ? 50 : 35))
|
d3.forceCollide().radius((d) => (d.isCenter ? 40 : 30)),
|
||||||
);
|
);
|
||||||
|
|
||||||
// Initial zoom to show entire graph
|
const initialScale = 0.85;
|
||||||
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,
|
||||||
|
|
@ -288,69 +249,37 @@ export function NetworkGraph({
|
||||||
zoom.transform,
|
zoom.transform,
|
||||||
d3.zoomIdentity
|
d3.zoomIdentity
|
||||||
.translate(initialTranslate[0], initialTranslate[1])
|
.translate(initialTranslate[0], initialTranslate[1])
|
||||||
.scale(initialScale)
|
.scale(initialScale),
|
||||||
);
|
);
|
||||||
|
|
||||||
// Fix center node
|
// Fix center node position
|
||||||
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) {
|
||||||
const centerX = width / 2;
|
centerNode.fx = width / 2;
|
||||||
const centerY = height / 2;
|
centerNode.fy = 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 نداشته باشند**
|
// Create links
|
||||||
// فقط forceLink آنها را به دستهها متصل نگه میدارد
|
|
||||||
|
|
||||||
// const finalNodes = nodes.filter(n => !n.isCenter && n.stageid !== -1);
|
|
||||||
|
|
||||||
// categoryNodes.forEach((catNode) => {
|
|
||||||
// const childNodes = finalNodes.filter(n => n.category === catNode.category);
|
|
||||||
// const childCount = childNodes.length;
|
|
||||||
// const radius = 100; // فاصله از دسته
|
|
||||||
// const angleStep = (2 * Math.PI) / childCount;
|
|
||||||
|
|
||||||
// childNodes.forEach((node, i) => {
|
|
||||||
// const angle = i * angleStep;
|
|
||||||
// node.fx = catNode.fx! + radius * Math.cos(angle);
|
|
||||||
// node.fy = catNode.fy! + radius * Math.sin(angle);
|
|
||||||
// });
|
|
||||||
// });
|
|
||||||
|
|
||||||
// Curved links
|
|
||||||
const link = container
|
const link = container
|
||||||
.selectAll(".link")
|
.selectAll(".link")
|
||||||
.data(links)
|
.data(links)
|
||||||
.enter()
|
.enter()
|
||||||
.append("path")
|
.append("line")
|
||||||
.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)
|
||||||
.enter()
|
.enter()
|
||||||
.append("g")
|
.append("g")
|
||||||
.attr("class", "node")
|
.attr("class", "node")
|
||||||
.style("cursor", (d) => (d.stageid === -1 ? "default" : "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) => {
|
||||||
|
|
@ -372,100 +301,56 @@ export function NetworkGraph({
|
||||||
|
|
||||||
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) {
|
||||||
// const rect = group
|
// Center node as rectangle
|
||||||
// .append("rect")
|
const rect = group
|
||||||
// .attr("width", 200)
|
.append("rect")
|
||||||
// .attr("height", 80)
|
.attr("width", 150)
|
||||||
// .attr("x", -100) // نصف عرض جدید منفی
|
.attr("height", 60)
|
||||||
// .attr("y", -40) // نصف ارتفاع جدید منفی
|
.attr("x", -75)
|
||||||
// .attr("rx", 8)
|
.attr("y", -30)
|
||||||
// .attr("ry", 8)
|
.attr("rx", 8)
|
||||||
// .attr("fill", categoryToColor[d.category] || "#94A3B8")
|
.attr("ry", 8)
|
||||||
// .attr("stroke", "#FFFFFF")
|
.attr("fill", categoryToColor[d.category] || "#94A3B8")
|
||||||
// .attr("stroke-width", 3)
|
.attr("stroke", "#FFFFFF")
|
||||||
// .style("pointer-events", "none");
|
.attr("stroke-width", 3)
|
||||||
|
.style("pointer-events", "none");
|
||||||
|
|
||||||
// if (d.imageUrl || d.isCenter) {
|
// Add center image if available
|
||||||
// const pattern = defs
|
if (d.imageUrl || d.isCenter) {
|
||||||
// .append("pattern")
|
const pattern = defs
|
||||||
// .attr("id", `image-${d.id}`)
|
.append("pattern")
|
||||||
// .attr("x", 0)
|
.attr("id", `image-${d.id}`)
|
||||||
// .attr("y", 0)
|
.attr("x", 0)
|
||||||
// .attr("width", 1)
|
.attr("y", 0)
|
||||||
// .attr("height", 1);
|
.attr("width", 1)
|
||||||
|
.attr("height", 1);
|
||||||
|
|
||||||
// pattern
|
pattern
|
||||||
// .append("image")
|
.append("image")
|
||||||
// .attr("x", 0)
|
.attr("x", 0)
|
||||||
// .attr("y", 0)
|
.attr("y", 0)
|
||||||
// .attr("width", 200) // ← هماندازه با مستطیل
|
.attr("width", 150)
|
||||||
// .attr("height", 80)
|
.attr("height", 60)
|
||||||
// .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 {
|
||||||
// راه حل سادهتر - ابعاد ثابت با حفظ نسبت
|
// Regular nodes as circles
|
||||||
if (d.isCenter) {
|
|
||||||
|
|
||||||
//آپادانا
|
|
||||||
const fixedWidth = 198;
|
|
||||||
const fixedHeight = 200; // یا میتوانید براساس نسبت تصویر محاسبه کنید
|
|
||||||
|
|
||||||
//بندر امام
|
|
||||||
// const fixedWidth = 100;
|
|
||||||
// const fixedHeight = 80; // یا میتوانید براساس نسبت تصویر محاسبه کنید
|
|
||||||
|
|
||||||
//نوری
|
|
||||||
// const fixedWidth = 100;
|
|
||||||
// const fixedHeight = 80; // یا میتوانید براساس نسبت تصویر محاسبه کنید
|
|
||||||
|
|
||||||
const rect = group
|
|
||||||
.append("rect")
|
|
||||||
.attr("width", fixedWidth)
|
|
||||||
.attr("height", fixedHeight)
|
|
||||||
.attr("x", -fixedWidth / 2)
|
|
||||||
.attr("y", -fixedHeight / 2)
|
|
||||||
.attr("rx", 8)
|
|
||||||
.attr("ry", 8)
|
|
||||||
.attr("fill", categoryToColor[d.category] || "#94A3B8")
|
|
||||||
.attr("stroke", "#FFFFFF")
|
|
||||||
.attr("stroke-width", 3)
|
|
||||||
.style("pointer-events", "none");
|
|
||||||
|
|
||||||
const pattern = defs
|
|
||||||
.append("pattern")
|
|
||||||
.attr("id", `image-${d.id}`)
|
|
||||||
.attr("x", 0)
|
|
||||||
.attr("y", 0)
|
|
||||||
.attr("width", 1)
|
|
||||||
.attr("height", 1);
|
|
||||||
|
|
||||||
pattern
|
|
||||||
.append("image")
|
|
||||||
.attr("x", 0)
|
|
||||||
.attr("y", 0)
|
|
||||||
.attr("width", fixedWidth)
|
|
||||||
.attr("height", fixedHeight)
|
|
||||||
.attr("href", d.isCenter ? "/main-circle.png" : d.imageUrl)
|
|
||||||
.attr("preserveAspectRatio", "xMidYMid meet"); // حفظ نسبت تصویر
|
|
||||||
|
|
||||||
rect.attr("fill", `url(#image-${d.id})`);
|
|
||||||
|
|
||||||
}
|
|
||||||
else {
|
|
||||||
const circle = group
|
const circle = group
|
||||||
.append("circle")
|
.append("circle")
|
||||||
.attr("r", 25)
|
.attr("r", 25)
|
||||||
.attr("fill", categoryToColor[d.category] || "#fff")
|
.attr("fill", categoryToColor[d.category] || "8#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")
|
||||||
|
|
@ -482,8 +367,10 @@ const fixedHeight = 200; // یا میتوانید براساس نسبت تص
|
||||||
.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}`)
|
||||||
|
|
@ -497,33 +384,20 @@ const fixedHeight = 200; // یا میتوانید براساس نسبت تص
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// Add labels below nodes
|
||||||
const labels = nodeGroup
|
const labels = nodeGroup
|
||||||
.append("text")
|
.append("text")
|
||||||
.text((d) => d.label)
|
.text((d) => d.label)
|
||||||
.attr("text-anchor", "middle")
|
.attr("text-anchor", "middle")
|
||||||
.attr("dy", (d) => {
|
.attr("dy", (d) => (d.isCenter ? 50 : 45))
|
||||||
if (d.isCenter) {
|
.attr("font-size", (d) => (d.isCenter ? "14px" : "12px"))
|
||||||
|
.attr("font-weight", "bold")
|
||||||
//آپادانا
|
.attr("fill", "#F9FAFB")
|
||||||
const centerNodeHeight = 200; // ارتفاع نود مرکزی
|
.attr("stroke", "rgba(17, 24, 39, 0.95)")
|
||||||
|
.attr("stroke-width", 4)
|
||||||
//بندر امام
|
.attr("paint-order", "stroke");
|
||||||
// const centerNodeHeight = 80; // ارتفاع نود مرکزی
|
|
||||||
|
|
||||||
//نوری
|
|
||||||
// const centerNodeHeight = 80; // ارتفاع نود مرکزی
|
|
||||||
|
|
||||||
return centerNodeHeight / 2 + 20; // نصف ارتفاع + فاصله 20px
|
|
||||||
}
|
|
||||||
return 45; // برای نودهای دیگر
|
|
||||||
})
|
|
||||||
.attr("font-size", (d) => (d.isCenter ? "14px" : "12px"))
|
|
||||||
.attr("font-weight", "bold")
|
|
||||||
.attr("fill", "#F9FAFB")
|
|
||||||
.attr("stroke", "rgba(17, 24, 39, 0.95)")
|
|
||||||
.attr("stroke-width", 4)
|
|
||||||
.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;
|
||||||
|
|
@ -545,88 +419,79 @@ const fixedHeight = 200; // یا میتوانید براساس نسبت تص
|
||||||
.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 || d.stageid === -1) return;
|
if (d.isCenter) return;
|
||||||
|
|
||||||
if (onNodeClick && d.stageid) {
|
if (onNodeClick && d.stageid) {
|
||||||
// Open dialog immediately with basic info
|
|
||||||
const basicDetails: CompanyDetails = {
|
|
||||||
id: d.id,
|
|
||||||
label: d.label,
|
|
||||||
category: d.category,
|
|
||||||
stageid: d.stageid,
|
|
||||||
fields: [],
|
|
||||||
};
|
|
||||||
onNodeClick(basicDetails);
|
|
||||||
|
|
||||||
// Start loading
|
|
||||||
onLoadingChange?.(true);
|
|
||||||
|
|
||||||
try {
|
try {
|
||||||
if (date.start && date.end) {
|
// Fetch detailed company data
|
||||||
const res = await callAPI(d.stageid);
|
const res = await callAPI(d.stageid);
|
||||||
const responseData = JSON.parse(res.data);
|
|
||||||
const fieldValues =
|
|
||||||
JSON.parse(responseData?.getvalue)?.[0]?.FieldValues || [];
|
|
||||||
|
|
||||||
const filteredFields = fieldValues.filter(
|
const responseData = JSON.parse(res.data);
|
||||||
(field: any) =>
|
const fieldValues =
|
||||||
!["image", "img", "full_name", "about_collaboration"].includes(
|
JSON.parse(responseData?.getvalue)?.[0]?.FieldValues || [];
|
||||||
field.F.toLowerCase()
|
// Filter out image fields and find description
|
||||||
)
|
const filteredFields = fieldValues.filter(
|
||||||
);
|
(field: any) =>
|
||||||
|
!["image", "img", "full_name", "about_collaboration"].includes(
|
||||||
|
field.F.toLowerCase(),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
|
||||||
const descriptionField = fieldValues.find(
|
const descriptionField = fieldValues.find(
|
||||||
(field: any) =>
|
(field: any) =>
|
||||||
field.F.toLowerCase().includes("description") ||
|
field.F.toLowerCase().includes("description") ||
|
||||||
field.F.toLowerCase().includes("about_collaboration") ||
|
field.F.toLowerCase().includes("about_collaboration") ||
|
||||||
field.F.toLowerCase().includes("about")
|
field.F.toLowerCase().includes("about"),
|
||||||
);
|
);
|
||||||
|
|
||||||
const companyDetails: CompanyDetails = {
|
const companyDetails: CompanyDetails = {
|
||||||
id: d.id,
|
id: d.id,
|
||||||
label: d.label,
|
label: d.label,
|
||||||
category: d.category,
|
category: d.category,
|
||||||
stageid: d.stageid,
|
stageid: d.stageid,
|
||||||
fields: filteredFields,
|
fields: filteredFields,
|
||||||
description: descriptionField?.V || undefined,
|
description: descriptionField?.V || undefined,
|
||||||
};
|
};
|
||||||
|
|
||||||
onNodeClick(companyDetails);
|
onNodeClick(companyDetails);
|
||||||
}
|
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error("Failed to fetch company details:", error);
|
console.error("Failed to fetch company details:", error);
|
||||||
// Keep the basic details already shown
|
// Fallback to basic info
|
||||||
} finally {
|
const basicDetails: CompanyDetails = {
|
||||||
// Stop loading
|
id: d.id,
|
||||||
onLoadingChange?.(false);
|
label: d.label,
|
||||||
|
category: d.category,
|
||||||
|
stageid: d.stageid,
|
||||||
|
fields: [],
|
||||||
|
};
|
||||||
|
onNodeClick(basicDetails);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// Update positions on simulation tick
|
||||||
simulation.on("tick", () => {
|
simulation.on("tick", () => {
|
||||||
link.attr("d", (d: any) => {
|
link
|
||||||
const sx = (d.source as Node).x!;
|
.attr("x1", (d) => (d.source as Node).x!)
|
||||||
const sy = (d.source as Node).y!;
|
.attr("y1", (d) => (d.source as Node).y!)
|
||||||
const tx = (d.target as Node).x!;
|
.attr("x2", (d) => (d.target as Node).x!)
|
||||||
const ty = (d.target as Node).y!;
|
.attr("y2", (d) => (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, date]);
|
}, [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">
|
||||||
|
|
@ -640,6 +505,7 @@ const fixedHeight = 200; // یا میتوانید براساس نسبت تص
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 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">
|
||||||
|
|
@ -653,11 +519,14 @@ const fixedHeight = 200; // یا میتوانید براساس نسبت تص
|
||||||
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;
|
||||||
|
|
@ -678,25 +547,40 @@ const fixedHeight = 200; // یا میتوانید براساس نسبت تص
|
||||||
<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={{
|
||||||
transform: `rotate(${(i * 360) / 8}deg) translateX(32px)`,
|
left: "50%",
|
||||||
transformOrigin: "left center",
|
top: "40px",
|
||||||
|
transform: "translateX(-50%)",
|
||||||
|
animationDelay: `${i * 200 + 100}ms`,
|
||||||
|
}}
|
||||||
|
></div>
|
||||||
|
<div
|
||||||
|
className="absolute w-0.5 bg-gray-600 animate-pulse opacity-30"
|
||||||
|
style={{
|
||||||
|
left: "50%",
|
||||||
|
top: "50%",
|
||||||
|
height: `${radius - 16}px`,
|
||||||
|
transformOrigin: "top",
|
||||||
|
transform: `translateX(-50%) rotate(${angle + Math.PI}rad)`,
|
||||||
|
animationDelay: `${i * 100}ms`,
|
||||||
}}
|
}}
|
||||||
></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">
|
<div className="w-full h-full relative bg-transparent overflow-hidden">
|
||||||
<svg
|
<svg ref={svgRef} className="w-full h-full" style={{ minHeight: 500 }} />
|
||||||
ref={svgRef}
|
|
||||||
className="w-full h-full bg-transparent"
|
|
||||||
style={{ cursor: "grab" }}
|
|
||||||
/>
|
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,67 +0,0 @@
|
||||||
import { ChevronLeft, ChevronRight } from "lucide-react";
|
|
||||||
import React from "react";
|
|
||||||
|
|
||||||
interface MonthItem {
|
|
||||||
id: string;
|
|
||||||
label: string;
|
|
||||||
start: string;
|
|
||||||
end: string;
|
|
||||||
}
|
|
||||||
|
|
||||||
// interface CurrentDay {
|
|
||||||
// start: string;
|
|
||||||
// end: string;
|
|
||||||
// month: string;
|
|
||||||
// }
|
|
||||||
|
|
||||||
interface CalendarProps {
|
|
||||||
title: string;
|
|
||||||
nextYearHandler: () => void;
|
|
||||||
prevYearHandler: () => void;
|
|
||||||
currentYear?: number;
|
|
||||||
monthList: Array<MonthItem>;
|
|
||||||
selectedDate?: string;
|
|
||||||
selectDateHandler: (item: MonthItem) => void;
|
|
||||||
}
|
|
||||||
|
|
||||||
export const Calendar: React.FC<CalendarProps> = ({
|
|
||||||
title,
|
|
||||||
nextYearHandler,
|
|
||||||
prevYearHandler,
|
|
||||||
currentYear,
|
|
||||||
monthList,
|
|
||||||
selectedDate,
|
|
||||||
selectDateHandler,
|
|
||||||
}) => {
|
|
||||||
return (
|
|
||||||
<div className="filter-box bg-pr-gray w-full px-1">
|
|
||||||
<header className="flex flex-row border-b border-[#5F6284] pb-1.5 justify-center">
|
|
||||||
<span className="font-light">{title}</span>
|
|
||||||
<div className="flex flex-row items-center gap-3">
|
|
||||||
<ChevronRight
|
|
||||||
className="inline-block w-6 h-6 cursor-pointer"
|
|
||||||
onClick={nextYearHandler}
|
|
||||||
/>
|
|
||||||
<span className="font-light">{currentYear}</span>
|
|
||||||
<ChevronLeft
|
|
||||||
className="inline-block w-6 h-6 cursor-pointer"
|
|
||||||
onClick={prevYearHandler}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
</header>
|
|
||||||
<div className="content flex flex-col gap-2 text-center pt-1 cursor-pointer">
|
|
||||||
{monthList.map((item, index) => (
|
|
||||||
<span
|
|
||||||
key={`${item.id}-${index}`}
|
|
||||||
className={`text-lg hover:bg-[#33364D] p-1 rounded-xl transition-all duration-300 ${
|
|
||||||
selectedDate === item.label ? `bg-[#33364D]` : ""
|
|
||||||
}`}
|
|
||||||
onClick={() => selectDateHandler(item)}
|
|
||||||
>
|
|
||||||
{item.label}
|
|
||||||
</span>
|
|
||||||
))}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
@ -7,7 +7,7 @@ interface BaseCardProps {
|
||||||
headerClassName?: string;
|
headerClassName?: string;
|
||||||
contentClassName?: string;
|
contentClassName?: string;
|
||||||
children: React.ReactNode;
|
children: React.ReactNode;
|
||||||
icon?: React.ComponentType<{ className?: string }>;
|
icon ?: React.ComponentType<{ className?: string }>;
|
||||||
withHeader?: boolean;
|
withHeader?: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -18,44 +18,32 @@ export function BaseCard({
|
||||||
contentClassName,
|
contentClassName,
|
||||||
children,
|
children,
|
||||||
withHeader = false,
|
withHeader = false,
|
||||||
icon: Icon,
|
icon : Icon,
|
||||||
}: BaseCardProps) {
|
}: BaseCardProps) {
|
||||||
return (
|
return (
|
||||||
<Card
|
<Card
|
||||||
className={cn(
|
className={cn(
|
||||||
"bg-[linear-gradient(to_bottom_left,#464861,50%,#111628)] backdrop-blur-sm py-2 pb-0 grid items-center",
|
"bg-[linear-gradient(to_bottom_left,#464861,50%,#111628)] backdrop-blur-sm py-4 grid items-center",
|
||||||
className
|
className
|
||||||
)}
|
)}
|
||||||
>
|
>
|
||||||
{Icon && title ? (
|
{Icon && title ? (
|
||||||
<CardHeader
|
<CardHeader className={cn("border-b-2 border-gray-500/20 py-2 px-0 pb-4", headerClassName)}>
|
||||||
className={cn(
|
<CardTitle className="text-white text-sm text-right font-persian px-4 my-auto items-center flex w-full justify-between">{title} {<Icon />} </CardTitle>
|
||||||
"border-b-2 border-gray-500/20 py-2 px-0 pb-4",
|
|
||||||
headerClassName
|
|
||||||
)}
|
|
||||||
>
|
|
||||||
<CardTitle className="text-white text-sm text-right font-persian px-4 my-auto items-center flex w-full justify-between">
|
|
||||||
{title} {<Icon />}{" "}
|
|
||||||
</CardTitle>
|
|
||||||
</CardHeader>
|
</CardHeader>
|
||||||
) : withHeader && title ? (
|
) :
|
||||||
<CardHeader
|
withHeader && title ? (
|
||||||
className={cn("pb-2 border-b-2 border-gray-500/20", headerClassName)}
|
<CardHeader className={cn("pb-2 border-b-2 border-gray-500/20", headerClassName)}>
|
||||||
>
|
<CardTitle className="text-white text-sm text-right font-persian px-4">{title}</CardTitle>
|
||||||
<CardTitle className="text-white text-sm text-right font-persian px-4">
|
|
||||||
{title}
|
|
||||||
</CardTitle>
|
|
||||||
</CardHeader>
|
</CardHeader>
|
||||||
) : title ? (
|
) : title ? (
|
||||||
<div className="border-b-2 border-gray-500/20 pb-2">
|
<div className="border-b-2 border-gray-500/20 pb-2">
|
||||||
<h3 className="text-sm font-bold text-white text-right font-persian px-4">
|
<h3 className="text-sm font-bold text-white text-right font-persian px-4">{title}</h3>
|
||||||
{title}
|
|
||||||
</h3>
|
|
||||||
</div>
|
</div>
|
||||||
) : null}
|
) : null}
|
||||||
<CardContent className={cn("py-2 px-4 ", contentClassName)}>
|
<CardContent className={cn("py-2 px-4", contentClassName)}>
|
||||||
{children}
|
{children}
|
||||||
</CardContent>
|
</CardContent>
|
||||||
</Card>
|
</Card>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
@ -9,7 +9,7 @@ const Card = React.forwardRef<
|
||||||
<div
|
<div
|
||||||
ref={ref}
|
ref={ref}
|
||||||
className={cn(
|
className={cn(
|
||||||
"rounded-lg border bg-card text-card-foreground shadow-sm",
|
"rounded-lg border bg-card text-card-foreground shadow-sm ",
|
||||||
className
|
className
|
||||||
)}
|
)}
|
||||||
{...props}
|
{...props}
|
||||||
|
|
|
||||||
|
|
@ -1,4 +1,4 @@
|
||||||
import { calculateNiceRange, formatNumber } from "~/lib/utils";
|
import { formatNumber, calculateNiceRange } from "~/lib/utils";
|
||||||
|
|
||||||
export interface BarChartData {
|
export interface BarChartData {
|
||||||
label: string;
|
label: string;
|
||||||
|
|
@ -18,7 +18,6 @@ interface CustomBarChartProps {
|
||||||
showAxisLabels?: boolean;
|
showAxisLabels?: boolean;
|
||||||
className?: string;
|
className?: string;
|
||||||
loading?: boolean;
|
loading?: boolean;
|
||||||
hasPercent?: boolean;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export function CustomBarChart({
|
export function CustomBarChart({
|
||||||
|
|
@ -29,7 +28,6 @@ export function CustomBarChart({
|
||||||
showAxisLabels = true,
|
showAxisLabels = true,
|
||||||
className = "",
|
className = "",
|
||||||
loading = false,
|
loading = false,
|
||||||
hasPercent = true,
|
|
||||||
}: CustomBarChartProps) {
|
}: CustomBarChartProps) {
|
||||||
// استفاده از nice numbers برای محاسبه دامنه مناسب
|
// استفاده از nice numbers برای محاسبه دامنه مناسب
|
||||||
const values = data.map((item) => item.maxValue || item.value);
|
const values = data.map((item) => item.maxValue || item.value);
|
||||||
|
|
@ -39,7 +37,7 @@ export function CustomBarChart({
|
||||||
// Loading skeleton
|
// Loading skeleton
|
||||||
if (loading) {
|
if (loading) {
|
||||||
return (
|
return (
|
||||||
<div className={`space-y-6 p-4 pt-0 ${className}`} style={{ height }}>
|
<div className={`space-y-6 p-4 ${className}`} style={{ height }}>
|
||||||
{title && (
|
{title && (
|
||||||
<div className="h-7 bg-gray-600 rounded animate-pulse mb-4 w-1/2"></div>
|
<div className="h-7 bg-gray-600 rounded animate-pulse mb-4 w-1/2"></div>
|
||||||
)}
|
)}
|
||||||
|
|
@ -71,7 +69,7 @@ export function CustomBarChart({
|
||||||
<div className={`space-y-6 ${className}`} style={{ height }}>
|
<div className={`space-y-6 ${className}`} style={{ height }}>
|
||||||
{title && (
|
{title && (
|
||||||
<div className="border-b-[#3F415A] border-b-2">
|
<div className="border-b-[#3F415A] border-b-2">
|
||||||
<h3 className="text-sm font-semibold text-white font-persian text-right px-4 pb-3">
|
<h3 className="text-sm font-semibold text-white font-persian text-right p-4">
|
||||||
{title}
|
{title}
|
||||||
</h3>
|
</h3>
|
||||||
</div>
|
</div>
|
||||||
|
|
@ -86,7 +84,7 @@ export function CustomBarChart({
|
||||||
return (
|
return (
|
||||||
<div key={index} className="flex items-center gap-3">
|
<div key={index} className="flex items-center gap-3">
|
||||||
<span
|
<span
|
||||||
className={`font-persian text-sm font-normal min-w-[120px] text-left ${
|
className={`font-persian text-sm font-normal min-w-[120px] text-right ${
|
||||||
item.labelColor || "text-white"
|
item.labelColor || "text-white"
|
||||||
}`}
|
}`}
|
||||||
>
|
>
|
||||||
|
|
@ -109,8 +107,7 @@ export function CustomBarChart({
|
||||||
<span className={`text-base font-normal text-left text-white`}>
|
<span className={`text-base font-normal text-left text-white`}>
|
||||||
{item.valuePrefix || ""}
|
{item.valuePrefix || ""}
|
||||||
|
|
||||||
{formatNumber(parseFloat(displayValue))}
|
{formatNumber(parseFloat(displayValue))}%
|
||||||
{hasPercent ? "%" : ""}
|
|
||||||
{item.valueSuffix || ""}
|
{item.valueSuffix || ""}
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
|
|
|
||||||
|
|
@ -1,18 +1,18 @@
|
||||||
"use client";
|
"use client"
|
||||||
|
|
||||||
import * as DialogPrimitive from "@radix-ui/react-dialog";
|
import * as React from "react"
|
||||||
import { X } from "lucide-react";
|
import * as DialogPrimitive from "@radix-ui/react-dialog"
|
||||||
import * as React from "react";
|
import { X } from "lucide-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 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 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",
|
||||||
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 p-4 space-y-1.5 text-center sm:text-left",
|
"flex flex-col 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,
|
||||||
DialogClose,
|
|
||||||
DialogContent,
|
|
||||||
DialogDescription,
|
|
||||||
DialogFooter,
|
|
||||||
DialogHeader,
|
|
||||||
DialogOverlay,
|
|
||||||
DialogPortal,
|
DialogPortal,
|
||||||
DialogTitle,
|
DialogOverlay,
|
||||||
|
DialogClose,
|
||||||
DialogTrigger,
|
DialogTrigger,
|
||||||
};
|
DialogContent,
|
||||||
|
DialogHeader,
|
||||||
|
DialogFooter,
|
||||||
|
DialogTitle,
|
||||||
|
DialogDescription,
|
||||||
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,27 +1,27 @@
|
||||||
"use client";
|
"use client"
|
||||||
|
|
||||||
import * as DropdownMenuPrimitive from "@radix-ui/react-dropdown-menu";
|
import * as React from "react"
|
||||||
import { Check, ChevronDown, Circle } from "lucide-react";
|
import * as DropdownMenuPrimitive from "@radix-ui/react-dropdown-menu"
|
||||||
import * as React from "react";
|
import { Check, ChevronRight, Circle } from "lucide-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,10 +34,11 @@ 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>,
|
||||||
|
|
@ -51,9 +52,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>,
|
||||||
|
|
@ -64,34 +65,32 @@ const DropdownMenuContent = React.forwardRef<
|
||||||
ref={ref}
|
ref={ref}
|
||||||
sideOffset={sideOffset}
|
sideOffset={sideOffset}
|
||||||
className={cn(
|
className={cn(
|
||||||
"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",
|
"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",
|
||||||
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, selected, ...props }, ref) => (
|
>(({ className, inset, ...props }, ref) => (
|
||||||
<DropdownMenuPrimitive.Item
|
<DropdownMenuPrimitive.Item
|
||||||
ref={ref}
|
ref={ref}
|
||||||
className={cn(
|
className={cn(
|
||||||
"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",
|
"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",
|
||||||
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>,
|
||||||
|
|
@ -113,9 +112,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>,
|
||||||
|
|
@ -136,13 +135,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
|
||||||
|
|
@ -154,8 +153,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>,
|
||||||
|
|
@ -166,8 +165,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,
|
||||||
|
|
@ -178,43 +177,24 @@ 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,
|
||||||
DropdownMenuButton,
|
DropdownMenuTrigger,
|
||||||
DropdownMenuCheckboxItem,
|
|
||||||
DropdownMenuContent,
|
DropdownMenuContent,
|
||||||
DropdownMenuGroup,
|
|
||||||
DropdownMenuItem,
|
DropdownMenuItem,
|
||||||
DropdownMenuLabel,
|
DropdownMenuCheckboxItem,
|
||||||
DropdownMenuPortal,
|
|
||||||
DropdownMenuRadioGroup,
|
|
||||||
DropdownMenuRadioItem,
|
DropdownMenuRadioItem,
|
||||||
|
DropdownMenuLabel,
|
||||||
DropdownMenuSeparator,
|
DropdownMenuSeparator,
|
||||||
DropdownMenuShortcut,
|
DropdownMenuShortcut,
|
||||||
|
DropdownMenuGroup,
|
||||||
|
DropdownMenuPortal,
|
||||||
DropdownMenuSub,
|
DropdownMenuSub,
|
||||||
DropdownMenuSubContent,
|
DropdownMenuSubContent,
|
||||||
DropdownMenuSubTrigger,
|
DropdownMenuSubTrigger,
|
||||||
DropdownMenuTrigger,
|
DropdownMenuRadioGroup,
|
||||||
};
|
}
|
||||||
|
|
|
||||||
|
|
@ -13,7 +13,6 @@ interface FunnelChartProps {
|
||||||
title?: string;
|
title?: string;
|
||||||
className?: string;
|
className?: string;
|
||||||
}
|
}
|
||||||
const greenColors = ["#3C9F71","#3BC47A","#3BC47A","#3BD77E","#3AEA83"]
|
|
||||||
|
|
||||||
export function FunnelChart({ data, title, className = "" }: FunnelChartProps) {
|
export function FunnelChart({ data, title, className = "" }: FunnelChartProps) {
|
||||||
const maxValue = Math.max(...data.map(d => d.value));
|
const maxValue = Math.max(...data.map(d => d.value));
|
||||||
|
|
@ -21,7 +20,7 @@ export function FunnelChart({ data, title, className = "" }: FunnelChartProps) {
|
||||||
if (!maxValue || maxValue <= 0) return 0;
|
if (!maxValue || maxValue <= 0) return 0;
|
||||||
return Math.round((value / maxValue) * 100);
|
return Math.round((value / maxValue) * 100);
|
||||||
};
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className={`w-full ${className}`}>
|
<div className={`w-full ${className}`}>
|
||||||
{title && (
|
{title && (
|
||||||
|
|
@ -29,7 +28,7 @@ export function FunnelChart({ data, title, className = "" }: FunnelChartProps) {
|
||||||
{title}
|
{title}
|
||||||
</h3>
|
</h3>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
<div className="flex px-4 flex-col items-center gap-2 space-y-2">
|
<div className="flex px-4 flex-col items-center gap-2 space-y-2">
|
||||||
{/* Start Process Line */}
|
{/* Start Process Line */}
|
||||||
<div className="flex items-center w-full gap-10 mt-6 px-4">
|
<div className="flex items-center w-full gap-10 mt-6 px-4">
|
||||||
|
|
@ -48,18 +47,18 @@ export function FunnelChart({ data, title, className = "" }: FunnelChartProps) {
|
||||||
{data.map((item, index) => {
|
{data.map((item, index) => {
|
||||||
const widthPercentage = toPercent(item.value);
|
const widthPercentage = toPercent(item.value);
|
||||||
const barWidth = Math.max(20, widthPercentage); // Minimum 20% width
|
const barWidth = Math.max(20, widthPercentage); // Minimum 20% width
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div key={index} className="grid grid-cols-[6rem_1fr] gap-2 w-full">
|
<div key={index} className="grid grid-cols-[6rem_1fr] gap-2 w-full">
|
||||||
<div className="text-sm font-light text-white font-persian cols-start-1 justify-self-start min-w-[max-content] text-center">
|
<div className="text-sm font-light text-white cols-start-1 justify-self-start font-thin min-w-[max-content] text-center">
|
||||||
{item.label}
|
{item.label}
|
||||||
</div>
|
</div>
|
||||||
<div className="flex items-center gap-10 w-full cols-start-2 justify-center">
|
<div className="flex items-center gap-10 w-full cols-start-2 flex items-center justify-center w-full">
|
||||||
<div className="flex items-center w-full">
|
<div className="flex items-center w-full">
|
||||||
<div style={{ width: `${(100 - barWidth) / 2}%` }} />
|
<div style={{ width: `${(100 - barWidth) / 2}%` }} />
|
||||||
<div
|
<div
|
||||||
className="bg-[#3BC47A] h-8 rounded-2xl flex items-center justify-center text-lg relative"
|
className="bg-[#3BC47A] h-8 rounded-2xl flex items-center justify-center text-lg relative"
|
||||||
style={{ width: `${barWidth}%` ,backgroundColor : `${greenColors[index]}`}}
|
style={{ width: `${barWidth}%` }}
|
||||||
>
|
>
|
||||||
<span className="text-pr-gray text-base font-semibold">
|
<span className="text-pr-gray text-base font-semibold">
|
||||||
{item.value.toLocaleString('fa-IR')}
|
{item.value.toLocaleString('fa-IR')}
|
||||||
|
|
|
||||||
|
|
@ -17,32 +17,32 @@ export function MetricCard({
|
||||||
percentLabel = "درصد به کل",
|
percentLabel = "درصد به کل",
|
||||||
}: MetricCardProps) {
|
}: MetricCardProps) {
|
||||||
return (
|
return (
|
||||||
<BaseCard title={title} className="h-full">
|
<BaseCard title={title}>
|
||||||
<div className="flex items-center justify-center flex-col">
|
<div className="flex items-center justify-center flex-col">
|
||||||
<div className="flex items-center gap-4 h-full">
|
<div className="flex items-center gap-4">
|
||||||
<div className="text-center">
|
|
||||||
<p className="text-3xl font-bold text-green-400">
|
|
||||||
{formatNumber(value)}
|
|
||||||
</p>
|
|
||||||
<div className="text-xs text-gray-400 font-persian">
|
|
||||||
{valueLabel}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
{percentValue !== undefined && (
|
|
||||||
<>
|
|
||||||
<span className="text-5xl font-thin text-gray-600">/</span>
|
|
||||||
<div className="text-center">
|
<div className="text-center">
|
||||||
<p className="text-3xl font-bold text-green-400">
|
<p className="text-3xl font-bold text-green-400">
|
||||||
{formatNumber(percentValue)}%
|
{formatNumber(value)}
|
||||||
</p>
|
</p>
|
||||||
<div className="text-xs text-gray-400 font-persian">
|
<div className="text-xs text-gray-400 font-persian">
|
||||||
{percentLabel}
|
{valueLabel}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</>
|
{percentValue !== undefined && (
|
||||||
)}
|
<>
|
||||||
</div>
|
<span className="text-5xl font-thin text-gray-600">/</span>
|
||||||
</div>
|
<div className="text-center">
|
||||||
</BaseCard>
|
<p className="text-3xl font-bold text-green-400">
|
||||||
|
{formatNumber(percentValue)}%
|
||||||
|
</p>
|
||||||
|
<div className="text-xs text-gray-400 font-persian">
|
||||||
|
{percentLabel}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</BaseCard>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
@ -6,46 +6,24 @@ import { cn, formatNumber } from "~/lib/utils"
|
||||||
const Progress = React.forwardRef<
|
const Progress = React.forwardRef<
|
||||||
React.ElementRef<typeof ProgressPrimitive.Root>,
|
React.ElementRef<typeof ProgressPrimitive.Root>,
|
||||||
React.ComponentPropsWithoutRef<typeof ProgressPrimitive.Root>
|
React.ComponentPropsWithoutRef<typeof ProgressPrimitive.Root>
|
||||||
>(({ className, value, ...props }, ref) => {
|
>(({ className, value, ...props }, ref) => (
|
||||||
// Dynamic scaling logic based on value ranges
|
<ProgressPrimitive.Root
|
||||||
const getScaledValue = (inputValue: number) => {
|
ref={ref}
|
||||||
const numValue = Number(inputValue);
|
className={cn(
|
||||||
if (numValue <= 1) {
|
"relative h-4 w-full overflow-hidden rounded-full bg-pr-gray",
|
||||||
return numValue * 100;
|
className
|
||||||
}
|
)}
|
||||||
else if (numValue <= 10) {
|
{...props}
|
||||||
return (numValue / 10) * 100;
|
>
|
||||||
} else if (numValue <= 50) {
|
<span className="left-0 text-sm absolute z-10 px-2 text-[#5F6284]">۰%</span>
|
||||||
return (numValue / 50) * 100;
|
<span className="w-full right-0 text-sm absolute z-10 px-2 text-[#5F6284]"
|
||||||
}
|
>{formatNumber(Math.ceil(value || 0 * 10) / 10)}%</span>
|
||||||
else {
|
<ProgressPrimitive.Indicator
|
||||||
return numValue
|
className="h-full w-full flex-1 bg-primary transition-all"
|
||||||
}
|
style={{ transform: `translateX(-${15 - (value || 0)}%)` }}
|
||||||
};
|
/>
|
||||||
|
</ProgressPrimitive.Root>
|
||||||
const scaledValue = getScaledValue(Number(value) || 0);
|
))
|
||||||
const displayValue = Number(value) || 0;
|
|
||||||
|
|
||||||
return (
|
|
||||||
<ProgressPrimitive.Root
|
|
||||||
ref={ref}
|
|
||||||
className={cn(
|
|
||||||
"relative h-4 w-full overflow-hidden rounded-full bg-pr-gray",
|
|
||||||
className
|
|
||||||
)}
|
|
||||||
{...props}
|
|
||||||
>
|
|
||||||
<span className="left-0 text-sm absolute z-10 px-2 text-[#5F6284]">۰%</span>
|
|
||||||
<span className="w-full right-0 text-sm absolute z-10 px-2 text-[#5F6284]">
|
|
||||||
{formatNumber(displayValue.toFixed(2))}%
|
|
||||||
</span>
|
|
||||||
<ProgressPrimitive.Indicator
|
|
||||||
className="h-full w-full flex-1 bg-pr-green transition-all z-20"
|
|
||||||
style={{ transform: `translateX(-${100-scaledValue}%)` }}
|
|
||||||
/>
|
|
||||||
</ProgressPrimitive.Root>
|
|
||||||
)
|
|
||||||
})
|
|
||||||
Progress.displayName = ProgressPrimitive.Root.displayName
|
Progress.displayName = ProgressPrimitive.Root.displayName
|
||||||
|
|
||||||
export { Progress }
|
export { Progress }
|
||||||
|
|
|
||||||
|
|
@ -4,7 +4,7 @@ import { cn } from "~/lib/utils"
|
||||||
|
|
||||||
interface TableProps extends React.HTMLAttributes<HTMLTableElement> {
|
interface TableProps extends React.HTMLAttributes<HTMLTableElement> {
|
||||||
containerClassName?: string
|
containerClassName?: string
|
||||||
containerRef?: React.RefObject<HTMLDivElement | null>
|
containerRef?: React.RefObject<HTMLDivElement>
|
||||||
}
|
}
|
||||||
|
|
||||||
const Table = React.forwardRef<HTMLTableElement, TableProps>(
|
const Table = React.forwardRef<HTMLTableElement, TableProps>(
|
||||||
|
|
|
||||||
|
|
@ -81,7 +81,7 @@ export function TabsTrigger({
|
||||||
className={cn(
|
className={cn(
|
||||||
"inline-flex items-center justify-center whitespace-nowrap rounded-sm px-3 py-1.5 text-sm font-medium ring-offset-background transition-all focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:pointer-events-none disabled:opacity-50",
|
"inline-flex items-center justify-center whitespace-nowrap rounded-sm px-3 py-1.5 text-sm font-medium ring-offset-background transition-all focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:pointer-events-none disabled:opacity-50",
|
||||||
isActive
|
isActive
|
||||||
? "bg-pr-gray text-foreground shadow-sm"
|
? "bg-gray-700 text-foreground shadow-sm"
|
||||||
: "hover:bg-muted/50",
|
: "hover:bg-muted/50",
|
||||||
className,
|
className,
|
||||||
)}
|
)}
|
||||||
|
|
|
||||||
|
|
@ -52,7 +52,7 @@ function TooltipContent({
|
||||||
{...props}
|
{...props}
|
||||||
>
|
>
|
||||||
{children}
|
{children}
|
||||||
<TooltipPrimitive.Arrow className={cn("bg-primary fill-primary z-50 size-2.5 translate-y-[calc(-50%_-_2px)] rotate-45 rounded-[2px]",className)} />
|
<TooltipPrimitive.Arrow className="bg-primary fill-primary z-50 size-2.5 translate-y-[calc(-50%_-_2px)] rotate-45 rounded-[2px]" />
|
||||||
</TooltipPrimitive.Content>
|
</TooltipPrimitive.Content>
|
||||||
</TooltipPrimitive.Portal>
|
</TooltipPrimitive.Portal>
|
||||||
)
|
)
|
||||||
|
|
|
||||||
|
|
@ -1,27 +0,0 @@
|
||||||
import jalaali from "jalaali-js";
|
|
||||||
import { useEffect, useState } from "react";
|
|
||||||
import type { CalendarDate } from "~/types/util.type";
|
|
||||||
|
|
||||||
const { jy } = jalaali.toJalaali(new Date());
|
|
||||||
|
|
||||||
export function useStoredDate(): [
|
|
||||||
CalendarDate,
|
|
||||||
React.Dispatch<React.SetStateAction<CalendarDate>>,
|
|
||||||
] {
|
|
||||||
const [date, setDate] = useState<CalendarDate>({});
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
const storedDate = localStorage.getItem("dateSelected");
|
|
||||||
|
|
||||||
if (storedDate) {
|
|
||||||
setDate(JSON.parse(storedDate));
|
|
||||||
} else {
|
|
||||||
setDate({
|
|
||||||
start: `${jy}/01/01`,
|
|
||||||
end: `${jy}/12/30`,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}, [jy]);
|
|
||||||
|
|
||||||
return [date, setDate];
|
|
||||||
}
|
|
||||||
|
|
@ -162,24 +162,10 @@ class ApiService {
|
||||||
|
|
||||||
// Innovation process function call wrapper
|
// Innovation process function call wrapper
|
||||||
public async call<T = any>(payload: any) {
|
public async call<T = any>(payload: any) {
|
||||||
//بندر امام
|
|
||||||
const url = "https://inogen-back.pelekan.org/api/call";
|
const url = "https://inogen-back.pelekan.org/api/call";
|
||||||
//آپادانا
|
|
||||||
const url = "https://APADANA-IATM-back.pelekan.org/api/call";
|
|
||||||
//نوری
|
|
||||||
const url = "https://NOPC-IATM-back.pelekan.org/api/call";
|
|
||||||
return this.postAbsolute<T>(url, payload);
|
return this.postAbsolute<T>(url, payload);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
const API_BASE_URL =
|
|
||||||
//بندر امام
|
|
||||||
// import.meta.env.VITE_API_URL || "https://inogen-bpms-back.pelekan.org/api";
|
|
||||||
//آپادانا
|
|
||||||
import.meta.env.VITE_API_URL || "https://APADANA-IATM-back.pelekan.org/api";
|
|
||||||
//نوری
|
|
||||||
// import.meta.env.VITE_API_URL || "https://NOPC-IATM-back.pelekan.org/api";
|
|
||||||
|
|
||||||
// GET request
|
// GET request
|
||||||
public async get<T = any>(endpoint: string): Promise<ApiResponse<T>> {
|
public async get<T = any>(endpoint: string): Promise<ApiResponse<T>> {
|
||||||
return this.request<T>(endpoint, {
|
return this.request<T>(endpoint, {
|
||||||
|
|
|
||||||
|
|
@ -1,7 +1,6 @@
|
||||||
import { clsx, type ClassValue } from "clsx";
|
import { clsx, type ClassValue } from "clsx";
|
||||||
import EventEmitter from "events";
|
|
||||||
import moment from "moment-jalaali";
|
|
||||||
import { twMerge } from "tailwind-merge";
|
import { twMerge } from "tailwind-merge";
|
||||||
|
import moment from "moment-jalaali";
|
||||||
|
|
||||||
export function cn(...inputs: ClassValue[]) {
|
export function cn(...inputs: ClassValue[]) {
|
||||||
return twMerge(clsx(inputs));
|
return twMerge(clsx(inputs));
|
||||||
|
|
@ -23,6 +22,8 @@ export const formatCurrency = (amount: string | number) => {
|
||||||
return new Intl.NumberFormat("fa-IR").format(numericAmount) + " ریال";
|
return new Intl.NumberFormat("fa-IR").format(numericAmount) + " ریال";
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* محاسبه دامنه nice numbers برای محور Y نمودارها
|
* محاسبه دامنه nice numbers برای محور Y نمودارها
|
||||||
* @param values آرایه از مقادیر دادهها
|
* @param values آرایه از مقادیر دادهها
|
||||||
|
|
@ -45,7 +46,7 @@ export function calculateNiceRange(
|
||||||
|
|
||||||
// پیدا کردن حداکثر مقدار در دادهها
|
// پیدا کردن حداکثر مقدار در دادهها
|
||||||
const dataMax = Math.max(...values);
|
const dataMax = Math.max(...values);
|
||||||
|
|
||||||
// اگر همه مقادیر صفر یا منفی هستند
|
// اگر همه مقادیر صفر یا منفی هستند
|
||||||
if (dataMax <= 0) {
|
if (dataMax <= 0) {
|
||||||
return { niceMax: 100, tickInterval: 20, ticks: [0, 20, 40, 60, 80, 100] };
|
return { niceMax: 100, tickInterval: 20, ticks: [0, 20, 40, 60, 80, 100] };
|
||||||
|
|
@ -56,19 +57,19 @@ export function calculateNiceRange(
|
||||||
|
|
||||||
// محاسبه nice upper limit
|
// محاسبه nice upper limit
|
||||||
const niceMax = calculateNiceNumber(maxWithMargin, true);
|
const niceMax = calculateNiceNumber(maxWithMargin, true);
|
||||||
|
|
||||||
// محاسبه فاصله مناسب tick ها بر اساس niceMax
|
// محاسبه فاصله مناسب tick ها بر اساس niceMax
|
||||||
const range = niceMax - minValue;
|
const range = niceMax - minValue;
|
||||||
const targetTicks = 5; // هدف: 5 tick
|
const targetTicks = 5; // هدف: 5 tick
|
||||||
const roughTickInterval = range / (targetTicks - 1);
|
const roughTickInterval = range / (targetTicks - 1);
|
||||||
const niceTickInterval = calculateNiceNumber(roughTickInterval, false);
|
const niceTickInterval = calculateNiceNumber(roughTickInterval, false);
|
||||||
|
|
||||||
// ایجاد آرایه tick ها
|
// ایجاد آرایه tick ها
|
||||||
const ticks: number[] = [];
|
const ticks: number[] = [];
|
||||||
for (let i = minValue; i <= niceMax; i += niceTickInterval) {
|
for (let i = minValue; i <= niceMax; i += niceTickInterval) {
|
||||||
ticks.push(Math.round(i));
|
ticks.push(Math.round(i));
|
||||||
}
|
}
|
||||||
|
|
||||||
// اطمینان از اینکه niceMax در آرایه tick ها باشد
|
// اطمینان از اینکه niceMax در آرایه tick ها باشد
|
||||||
if (ticks[ticks.length - 1] !== niceMax) {
|
if (ticks[ticks.length - 1] !== niceMax) {
|
||||||
ticks.push(niceMax);
|
ticks.push(niceMax);
|
||||||
|
|
@ -89,13 +90,13 @@ export function calculateNiceRange(
|
||||||
*/
|
*/
|
||||||
function calculateNiceNumber(value: number, round: boolean): number {
|
function calculateNiceNumber(value: number, round: boolean): number {
|
||||||
if (value <= 0) return 0;
|
if (value <= 0) return 0;
|
||||||
|
|
||||||
// پیدا کردن قدرت 10
|
// پیدا کردن قدرت 10
|
||||||
const exponent = Math.floor(Math.log10(value));
|
const exponent = Math.floor(Math.log10(value));
|
||||||
const fraction = value / Math.pow(10, exponent);
|
const fraction = value / Math.pow(10, exponent);
|
||||||
|
|
||||||
let niceFraction: number;
|
let niceFraction: number;
|
||||||
|
|
||||||
if (round) {
|
if (round) {
|
||||||
// برای حداکثر: به سمت بالا گرد میکنیم با دقت بیشتر
|
// برای حداکثر: به سمت بالا گرد میکنیم با دقت بیشتر
|
||||||
if (fraction <= 1.0) niceFraction = 1;
|
if (fraction <= 1.0) niceFraction = 1;
|
||||||
|
|
@ -111,12 +112,12 @@ function calculateNiceNumber(value: number, round: boolean): number {
|
||||||
else if (fraction <= 5.0) niceFraction = 5;
|
else if (fraction <= 5.0) niceFraction = 5;
|
||||||
else niceFraction = 10;
|
else niceFraction = 10;
|
||||||
}
|
}
|
||||||
|
|
||||||
return niceFraction * Math.pow(10, exponent);
|
return niceFraction * Math.pow(10, exponent);
|
||||||
}
|
}
|
||||||
|
|
||||||
export const handleDataValue = (val: any): any => {
|
export const handleDataValue = (val: any): any => {
|
||||||
moment.loadPersian({ usePersianDigits: true });
|
moment.loadPersian({ usePersianDigits: true });
|
||||||
if (val == null) return val;
|
if (val == null) return val;
|
||||||
if (
|
if (
|
||||||
typeof val === "string" &&
|
typeof val === "string" &&
|
||||||
|
|
@ -131,6 +132,4 @@ export const handleDataValue = (val: any): any => {
|
||||||
return val.toString().replace(/\d/g, (d) => "۰۱۲۳۴۵۶۷۸۹"[+d]);
|
return val.toString().replace(/\d/g, (d) => "۰۱۲۳۴۵۶۷۸۹"[+d]);
|
||||||
}
|
}
|
||||||
return val;
|
return val;
|
||||||
};
|
}
|
||||||
|
|
||||||
export const EventBus = new EventEmitter();
|
|
||||||
|
|
|
||||||
|
|
@ -1,33 +1,28 @@
|
||||||
import moment from "moment-jalaali";
|
import type { Route } from "./+types/ecosystem";
|
||||||
import React from "react";
|
import React from "react";
|
||||||
import { ProtectedRoute } from "~/components/auth/protected-route";
|
import { ProtectedRoute } from "~/components/auth/protected-route";
|
||||||
import { DashboardLayout } from "~/components/dashboard/layout";
|
import { DashboardLayout } from "~/components/dashboard/layout";
|
||||||
import { InfoPanel } from "~/components/ecosystem/info-panel";
|
import { Card, CardContent, CardHeader, CardTitle } from "~/components/ui/card";
|
||||||
import { NetworkGraph } from "~/components/ecosystem/network-graph";
|
|
||||||
import { Card, CardContent } from "~/components/ui/card";
|
|
||||||
import {
|
import {
|
||||||
Dialog,
|
Dialog,
|
||||||
DialogContent,
|
DialogContent,
|
||||||
|
DialogDescription,
|
||||||
DialogHeader,
|
DialogHeader,
|
||||||
DialogTitle,
|
DialogTitle,
|
||||||
} from "~/components/ui/dialog";
|
} from "~/components/ui/dialog";
|
||||||
|
import { NetworkGraph } from "~/components/ecosystem/network-graph";
|
||||||
|
import { InfoPanel } from "~/components/ecosystem/info-panel";
|
||||||
import { useAuth } from "~/contexts/auth-context";
|
import { useAuth } from "~/contexts/auth-context";
|
||||||
import type { Route } from "./+types/ecosystem";
|
import moment from "moment-jalaali";
|
||||||
|
|
||||||
// Get API base URL at module level to avoid process.env access in browser
|
// 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";
|
|
||||||
//آپادانا
|
|
||||||
import.meta.env.VITE_API_URL || "https://APADANA-IATM-back.pelekan.org/api";
|
|
||||||
//نوری
|
|
||||||
// import.meta.env.VITE_API_URL || "https://NOPC-IATM-back.pelekan.org/api";
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
// Import the CompanyDetails type
|
// Import the CompanyDetails type
|
||||||
import { Hexagon } from "lucide-react";
|
|
||||||
import type { CompanyDetails } from "~/components/ecosystem/network-graph";
|
import type { CompanyDetails } from "~/components/ecosystem/network-graph";
|
||||||
|
import { formatNumber } from "~/lib/utils";
|
||||||
|
import { Hexagon } from "lucide-react";
|
||||||
|
|
||||||
export function meta({}: Route.MetaArgs) {
|
export function meta({}: Route.MetaArgs) {
|
||||||
return [
|
return [
|
||||||
|
|
@ -61,20 +56,10 @@ function handleValue(val: any): any {
|
||||||
export default function EcosystemPage() {
|
export default function EcosystemPage() {
|
||||||
const [selectedCompany, setSelectedCompany] =
|
const [selectedCompany, setSelectedCompany] =
|
||||||
React.useState<CompanyDetails | null>(null);
|
React.useState<CompanyDetails | null>(null);
|
||||||
const [isDialogLoading, setIsDialogLoading] = React.useState(false);
|
|
||||||
const { token } = useAuth();
|
const { token } = useAuth();
|
||||||
|
|
||||||
const closeDialog = () => {
|
const closeDialog = () => {
|
||||||
setSelectedCompany(null);
|
setSelectedCompany(null);
|
||||||
setIsDialogLoading(false);
|
|
||||||
};
|
|
||||||
|
|
||||||
const handleNodeClick = (company: CompanyDetails) => {
|
|
||||||
setSelectedCompany(company);
|
|
||||||
};
|
|
||||||
|
|
||||||
const handleLoadingChange = (loading: boolean) => {
|
|
||||||
setIsDialogLoading(loading);
|
|
||||||
};
|
};
|
||||||
|
|
||||||
// Construct image URL
|
// Construct image URL
|
||||||
|
|
@ -85,7 +70,7 @@ export default function EcosystemPage() {
|
||||||
return (
|
return (
|
||||||
<ProtectedRoute requireAuth={true}>
|
<ProtectedRoute requireAuth={true}>
|
||||||
<DashboardLayout title="زیست بوم فناوری">
|
<DashboardLayout title="زیست بوم فناوری">
|
||||||
<div>
|
<div className="p-4 lg:p-6">
|
||||||
<div className="grid grid-cols-1 items-start lg:grid-cols-12 gap-4">
|
<div className="grid grid-cols-1 items-start lg:grid-cols-12 gap-4">
|
||||||
<div className="lg:col-span-4">
|
<div className="lg:col-span-4">
|
||||||
<InfoPanel selectedCompany={selectedCompany} />
|
<InfoPanel selectedCompany={selectedCompany} />
|
||||||
|
|
@ -94,10 +79,7 @@ export default function EcosystemPage() {
|
||||||
<div className="lg:col-span-8 h-full">
|
<div className="lg:col-span-8 h-full">
|
||||||
<Card className="h-full overflow-hidden bg-transparent border-[#3F415A]">
|
<Card className="h-full overflow-hidden bg-transparent border-[#3F415A]">
|
||||||
<CardContent className="p-0 h-full bg-transparent">
|
<CardContent className="p-0 h-full bg-transparent">
|
||||||
<NetworkGraph
|
<NetworkGraph onNodeClick={setSelectedCompany} />
|
||||||
onNodeClick={handleNodeClick}
|
|
||||||
onLoadingChange={handleLoadingChange}
|
|
||||||
/>
|
|
||||||
</CardContent>
|
</CardContent>
|
||||||
</Card>
|
</Card>
|
||||||
</div>
|
</div>
|
||||||
|
|
@ -109,7 +91,7 @@ export default function EcosystemPage() {
|
||||||
open={!!selectedCompany}
|
open={!!selectedCompany}
|
||||||
onOpenChange={(open) => !open && closeDialog()}
|
onOpenChange={(open) => !open && closeDialog()}
|
||||||
>
|
>
|
||||||
<DialogContent className="font-persian max-w-6xl min-h-max bg-[linear-gradient(to_bottom_left,#464861,20%,#111628)]">
|
<DialogContent className="font-persian max-w-6xl max-h-[75vh] overflow-y-auto bg-[linear-gradient(to_bottom_left,#464861,20%,#111628)]">
|
||||||
<DialogHeader>
|
<DialogHeader>
|
||||||
<DialogTitle className="text-right border-b-2 border-gray-600 pt-2 pb-4 mr-4 text-sm font-semibold">
|
<DialogTitle className="text-right border-b-2 border-gray-600 pt-2 pb-4 mr-4 text-sm font-semibold">
|
||||||
معرفی
|
معرفی
|
||||||
|
|
@ -117,138 +99,98 @@ export default function EcosystemPage() {
|
||||||
</DialogTitle>
|
</DialogTitle>
|
||||||
</DialogHeader>
|
</DialogHeader>
|
||||||
|
|
||||||
{isDialogLoading ? (
|
<div className="grid grid-cols-1 md:grid-cols-2 gap-6">
|
||||||
<div className="grid grid-cols-1 md:grid-cols-2 p-4 gap-6">
|
{/* Right Column - Description */}
|
||||||
{/* Right Column - Loading Skeleton */}
|
<div className="space-y-4 p-6 border-l-2 border-gray-600">
|
||||||
<div className="space-y-4 p-6 border-l-2 border-gray-600">
|
{/* Company Image */}
|
||||||
{/* Company Image & Title Skeleton */}
|
<div className="flex justify-between px-10 items-center text-3xl font-bold mb-4">
|
||||||
<div className="flex justify-between px-10 items-center mb-4">
|
{selectedCompany?.label || ""}
|
||||||
<div className="h-8 bg-gray-600 rounded animate-pulse w-48"></div>
|
{selectedCompany?.stageid && token?.accessToken ? (
|
||||||
<div className="w-12 h-12 bg-gray-600 rounded-2xl animate-pulse"></div>
|
<img
|
||||||
</div>
|
src={getImageUrl(selectedCompany.stageid)}
|
||||||
{/* Description Skeleton */}
|
alt={selectedCompany?.label || ""}
|
||||||
<div className="p-4 rounded-lg space-y-2">
|
className="w-12 h-12 object-cover rounded-2xl"
|
||||||
<div className="h-4 bg-gray-600 rounded animate-pulse w-full"></div>
|
onError={(e) => {
|
||||||
<div className="h-4 bg-gray-600 rounded animate-pulse w-5/6"></div>
|
// Hide image and show fallback on error
|
||||||
<div className="h-4 bg-gray-600 rounded animate-pulse w-4/6"></div>
|
e.currentTarget.style.display = "none";
|
||||||
<div className="h-4 bg-gray-600 rounded animate-pulse w-3/6"></div>
|
if (e.currentTarget.nextSibling) {
|
||||||
|
(
|
||||||
|
e.currentTarget.nextSibling as HTMLElement
|
||||||
|
).style.display = "flex";
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
) : null}
|
||||||
|
<div
|
||||||
|
className="w-24 h-24 rounded-full bg-gray-600 border-4 border-green-400 flex items-center justify-center"
|
||||||
|
style={{
|
||||||
|
display:
|
||||||
|
selectedCompany?.stageid && token?.accessToken
|
||||||
|
? "none"
|
||||||
|
: "flex",
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<svg
|
||||||
|
className="w-10 h-10 text-gray-400"
|
||||||
|
fill="none"
|
||||||
|
stroke="currentColor"
|
||||||
|
viewBox="0 0 24 24"
|
||||||
|
>
|
||||||
|
<path
|
||||||
|
strokeLinecap="round"
|
||||||
|
strokeLinejoin="round"
|
||||||
|
strokeWidth={2}
|
||||||
|
d="M19 21V5a2 2 0 00-2-2H7a2 2 0 00-2 2v16m14 0h2m-2 0h-5m-9 0H3m2 0h5M9 7h1m-1 4h1m4-4h1m-1 4h1m-5 10v-5a1 1 0 011-1h2a1 1 0 011 1v5m-4 0h4"
|
||||||
|
/>
|
||||||
|
</svg>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
{/* Left Column - Loading Skeleton */}
|
{selectedCompany?.description ? (
|
||||||
<div className="space-y-2">
|
<div className="p-4 rounded-lg">
|
||||||
<div className="h-6 bg-gray-600 rounded animate-pulse w-32"></div>
|
<p className="font-persian text-sm font-normal leading-relaxed">
|
||||||
|
{selectedCompany.description}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
) : (
|
||||||
|
<div className="text-gray-500 font-persian text-sm">
|
||||||
|
توضیحات در دسترس نیست
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
{/* Left Column - Company Fields */}
|
||||||
|
<div className="space-y-2">
|
||||||
|
<h3 className="font-persian gap-1 flex text-sm font-semibold">
|
||||||
|
اطلاعات
|
||||||
|
<span>{selectedCompany?.category}</span>
|
||||||
|
</h3>
|
||||||
|
{selectedCompany?.fields &&
|
||||||
|
selectedCompany.fields.length > 0 ? (
|
||||||
<div className="space-y-3 px-2">
|
<div className="space-y-3 px-2">
|
||||||
{Array.from({ length: 6 }).map((_, index) => (
|
{selectedCompany.fields.map((field, index) => (
|
||||||
<div
|
<div
|
||||||
key={index}
|
key={index}
|
||||||
className="flex justify-between items-center rounded-lg"
|
className="flex justify-between items-center rounded-lg"
|
||||||
>
|
>
|
||||||
<div className="flex items-center gap-1">
|
<span className="font-persian flex items-center gap-1 text-sm font-light">
|
||||||
<div className="h-4 w-4 bg-gray-600 rounded animate-pulse"></div>
|
<Hexagon className="text-pr-green h-4 w-4" />
|
||||||
<div className="h-4 bg-gray-600 rounded animate-pulse w-24"></div>
|
{field.N}:
|
||||||
</div>
|
</span>
|
||||||
<div className="h-4 bg-gray-600 rounded animate-pulse w-20"></div>
|
<span className="text-right min-w-1/3">
|
||||||
|
<span className="font-persian text-sm font-normal text-right">
|
||||||
|
{handleValue(field.V)}
|
||||||
|
{field.U && <span className="mr-1">({field.U})</span>}
|
||||||
|
</span>
|
||||||
|
</span>
|
||||||
</div>
|
</div>
|
||||||
))}
|
))}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
) : (
|
||||||
</div>
|
<div className="text-gray-500 font-persian text-sm">
|
||||||
) : (
|
اطلاعات تکمیلی در دسترس نیست
|
||||||
<div className="grid p-4 pb-6 grid-cols-1 md:grid-cols-2 gap-6">
|
|
||||||
{/* Right Column - Description */}
|
|
||||||
<div className="space-y-4 p-6 border-l-2 border-gray-600">
|
|
||||||
{/* Company Image */}
|
|
||||||
<div className="flex justify-between px-10 items-center text-3xl font-bold mb-4">
|
|
||||||
{selectedCompany?.label || ""}
|
|
||||||
{selectedCompany?.stageid && token?.accessToken ? (
|
|
||||||
<img
|
|
||||||
src={getImageUrl(selectedCompany.stageid)}
|
|
||||||
alt={selectedCompany?.label || ""}
|
|
||||||
className="w-12 h-12 object-cover rounded-2xl"
|
|
||||||
onError={(e) => {
|
|
||||||
// Hide image and show fallback on error
|
|
||||||
e.currentTarget.style.display = "none";
|
|
||||||
if (e.currentTarget.nextSibling) {
|
|
||||||
(
|
|
||||||
e.currentTarget.nextSibling as HTMLElement
|
|
||||||
).style.display = "flex";
|
|
||||||
}
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
) : null}
|
|
||||||
<div
|
|
||||||
className="w-24 h-24 rounded-full bg-gray-600 border-4 border-pr-green flex items-center justify-center"
|
|
||||||
style={{
|
|
||||||
display:
|
|
||||||
selectedCompany?.stageid && token?.accessToken
|
|
||||||
? "none"
|
|
||||||
: "flex",
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
<svg
|
|
||||||
className="w-10 h-10 text-gray-400"
|
|
||||||
fill="none"
|
|
||||||
stroke="currentColor"
|
|
||||||
viewBox="0 0 24 24"
|
|
||||||
>
|
|
||||||
<path
|
|
||||||
strokeLinecap="round"
|
|
||||||
strokeLinejoin="round"
|
|
||||||
strokeWidth={2}
|
|
||||||
d="M19 21V5a2 2 0 00-2-2H7a2 2 0 00-2 2v16m14 0h2m-2 0h-5m-9 0H3m2 0h5M9 7h1m-1 4h1m4-4h1m-1 4h1m-5 10v-5a1 1 0 011-1h2a1 1 0 011 1v5m-4 0h4"
|
|
||||||
/>
|
|
||||||
</svg>
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
{selectedCompany?.description ? (
|
)}
|
||||||
<div className="p-4 rounded-lg">
|
|
||||||
<p className="font-persian text-sm font-normal leading-relaxed">
|
|
||||||
{selectedCompany.description}
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
) : (
|
|
||||||
<div className="text-gray-500 font-persian text-sm">
|
|
||||||
توضیحات در دسترس نیست
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
{/* Left Column - Company Fields */}
|
|
||||||
<div className="space-y-2">
|
|
||||||
<h3 className="font-persian gap-1 flex text-sm font-semibold">
|
|
||||||
اطلاعات
|
|
||||||
<span>{selectedCompany?.category}</span>
|
|
||||||
</h3>
|
|
||||||
{selectedCompany?.fields &&
|
|
||||||
selectedCompany.fields.length > 0 ? (
|
|
||||||
<div className="space-y-3 px-2">
|
|
||||||
{selectedCompany.fields.map((field, index) => (
|
|
||||||
<div
|
|
||||||
key={index}
|
|
||||||
className="flex justify-between items-center rounded-lg"
|
|
||||||
>
|
|
||||||
<span className="font-persian flex items-center gap-1 text-sm font-light">
|
|
||||||
<Hexagon className="text-pr-green h-4 w-4" />
|
|
||||||
{field.N}:
|
|
||||||
</span>
|
|
||||||
<span className="text-right min-w-1/3">
|
|
||||||
<span className="font-persian text-sm font-normal text-right">
|
|
||||||
{handleValue(field.V)}
|
|
||||||
{field.U && (
|
|
||||||
<span className="mr-1">({field.U})</span>
|
|
||||||
)}
|
|
||||||
</span>
|
|
||||||
</span>
|
|
||||||
</div>
|
|
||||||
))}
|
|
||||||
</div>
|
|
||||||
) : (
|
|
||||||
<div className="text-gray-500 font-persian text-sm">
|
|
||||||
اطلاعات تکمیلی در دسترس نیست
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
)}
|
</div>
|
||||||
</DialogContent>
|
</DialogContent>
|
||||||
</Dialog>
|
</Dialog>
|
||||||
</DashboardLayout>
|
</DashboardLayout>
|
||||||
|
|
|
||||||
|
|
@ -1,6 +0,0 @@
|
||||||
export interface CalendarDate {
|
|
||||||
start?: string;
|
|
||||||
end?: string;
|
|
||||||
sinceMonth?: string;
|
|
||||||
untilMonth?: string;
|
|
||||||
}
|
|
||||||
5128
package-lock.json
generated
5128
package-lock.json
generated
File diff suppressed because it is too large
Load Diff
|
|
@ -26,7 +26,6 @@
|
||||||
"class-variance-authority": "^0.7.1",
|
"class-variance-authority": "^0.7.1",
|
||||||
"clsx": "^2.1.1",
|
"clsx": "^2.1.1",
|
||||||
"d3": "^7.9.0",
|
"d3": "^7.9.0",
|
||||||
"file-saver": "^2.0.5",
|
|
||||||
"graphology": "^0.26.0",
|
"graphology": "^0.26.0",
|
||||||
"isbot": "^5.1.27",
|
"isbot": "^5.1.27",
|
||||||
"lucide-react": "^0.525.0",
|
"lucide-react": "^0.525.0",
|
||||||
|
|
@ -36,13 +35,11 @@
|
||||||
"react-hot-toast": "^2.5.2",
|
"react-hot-toast": "^2.5.2",
|
||||||
"react-router": "^7.7.0",
|
"react-router": "^7.7.0",
|
||||||
"recharts": "^2.15.4",
|
"recharts": "^2.15.4",
|
||||||
"tailwind-merge": "^3.3.1",
|
"tailwind-merge": "^3.3.1"
|
||||||
"xlsx-js-style": "^1.2.0"
|
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@react-router/dev": "^7.7.0",
|
"@react-router/dev": "^7.7.0",
|
||||||
"@tailwindcss/vite": "^4.1.4",
|
"@tailwindcss/vite": "^4.1.4",
|
||||||
"@types/file-saver": "^2.0.7",
|
|
||||||
"@types/node": "^20",
|
"@types/node": "^20",
|
||||||
"@types/react": "^19.1.2",
|
"@types/react": "^19.1.2",
|
||||||
"@types/react-dom": "^19.1.2",
|
"@types/react-dom": "^19.1.2",
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue
Block a user