mirror of
https://github.com/Tria-plc/edr-platform.git
synced 2026-08-29 23:28:11 +00:00
@@ -1,6 +1,12 @@
|
|||||||
@import "tailwindcss";
|
@import "tailwindcss";
|
||||||
@import "@edr/ui-common/theme.css" layer(theme);
|
@import "@edr/ui-common/theme.css" layer(theme);
|
||||||
|
|
||||||
|
/* The app toggles dark mode by setting the `dark` class on <html> (see
|
||||||
|
main.tsx / FreightDashboardLayout). Without this, Tailwind v4 compiles
|
||||||
|
`dark:` utilities to `@media (prefers-color-scheme: dark)` and they follow
|
||||||
|
the OS setting instead of the in-app toggle. */
|
||||||
|
@custom-variant dark (&:where(.dark, .dark *));
|
||||||
|
|
||||||
/* Bridge the central Mantine theme into Tailwind. freightMantineTheme
|
/* Bridge the central Mantine theme into Tailwind. freightMantineTheme
|
||||||
(createTheme) is the single source of truth; these just alias its generated
|
(createTheme) is the single source of truth; these just alias its generated
|
||||||
CSS variables so `bg-edr-*`, `text-edr-*`, `border-edr-*` utilities resolve
|
CSS variables so `bg-edr-*`, `text-edr-*`, `border-edr-*` utilities resolve
|
||||||
@@ -17,6 +23,34 @@
|
|||||||
--color-edr-soft: var(--mantine-color-edr-soft-6);
|
--color-edr-soft: var(--mantine-color-edr-soft-6);
|
||||||
--color-edr-ink: var(--mantine-color-edr-ink-6);
|
--color-edr-ink: var(--mantine-color-edr-ink-6);
|
||||||
--color-edr-accent: var(--mantine-color-edr-accent-6);
|
--color-edr-accent: var(--mantine-color-edr-accent-6);
|
||||||
|
|
||||||
|
/* Numeric `primary-*` scale used throughout the vendored IAM UI
|
||||||
|
(src/user-management, src/shared, …). Aliased to the edr-green Mantine
|
||||||
|
tuple (freight-brand.ts) so bg-primary-50 … text-primary-900 resolve to
|
||||||
|
brand shades; without these the utilities are simply not generated. */
|
||||||
|
--color-primary-50: var(--mantine-color-edr-green-0);
|
||||||
|
--color-primary-100: var(--mantine-color-edr-green-1);
|
||||||
|
--color-primary-200: var(--mantine-color-edr-green-2);
|
||||||
|
--color-primary-300: var(--mantine-color-edr-green-3);
|
||||||
|
--color-primary-400: var(--mantine-color-edr-green-4);
|
||||||
|
--color-primary-500: var(--mantine-color-edr-green-5);
|
||||||
|
--color-primary-600: var(--mantine-color-edr-green-6);
|
||||||
|
--color-primary-700: var(--mantine-color-edr-green-7);
|
||||||
|
--color-primary-800: var(--mantine-color-edr-green-8);
|
||||||
|
--color-primary-900: var(--mantine-color-edr-green-9);
|
||||||
|
--color-primary-950: #022c22;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Main brand color. NOTE: at runtime TenantConfig.applyTenantTheme() sets
|
||||||
|
--primary (and --ring/--accent/…) as INLINE styles on <html> from the
|
||||||
|
per-hostname tenant config, which beats any stylesheet — change the color
|
||||||
|
there (localhost → #0EA371). This block is the pre-mount fallback and fixes
|
||||||
|
--primary-foreground (the layered dark default is dark-on-dark). Same value
|
||||||
|
in both modes so the brand doesn't shift when toggling. */
|
||||||
|
:root,
|
||||||
|
.dark {
|
||||||
|
--primary: #0EA371;
|
||||||
|
--primary-foreground: #ffffff;
|
||||||
}
|
}
|
||||||
|
|
||||||
:root {
|
:root {
|
||||||
|
|||||||
@@ -24,7 +24,7 @@ const links = [
|
|||||||
{
|
{
|
||||||
title: "User management",
|
title: "User management",
|
||||||
description: "Employees, roles, and permissions",
|
description: "Employees, roles, and permissions",
|
||||||
href: "/dashboard/user-management",
|
href: "/user-management",
|
||||||
icon: Users,
|
icon: Users,
|
||||||
},
|
},
|
||||||
];
|
];
|
||||||
|
|||||||
@@ -119,7 +119,7 @@ const tenantConfigs: Record<string, TenantConfig> = {
|
|||||||
appName: "Smart Office",
|
appName: "Smart Office",
|
||||||
organizationName: "Addis Ababa City Administration",
|
organizationName: "Addis Ababa City Administration",
|
||||||
logo: "",
|
logo: "",
|
||||||
primaryColor: "#115005",
|
primaryColor: "#0EA371",
|
||||||
moduleConfig: {
|
moduleConfig: {
|
||||||
dms: true,
|
dms: true,
|
||||||
performance: true,
|
performance: true,
|
||||||
@@ -543,7 +543,14 @@ export const applyTenantTheme = (config: TenantConfig) => {
|
|||||||
root.style.setProperty("--primary", primary);
|
root.style.setProperty("--primary", primary);
|
||||||
root.style.setProperty("--ring", primary);
|
root.style.setProperty("--ring", primary);
|
||||||
root.style.setProperty("--sidebar-primary", primary);
|
root.style.setProperty("--sidebar-primary", primary);
|
||||||
root.style.setProperty("--accent", primary);
|
// Accent is a subtle hover/highlight background (dropdown items, menus) that
|
||||||
|
// must stay readable under --accent-foreground text in BOTH color schemes.
|
||||||
|
// Full-strength brand color here produced dark-pill-with-dark-text hovers,
|
||||||
|
// so use a translucent tint of the brand color instead.
|
||||||
|
root.style.setProperty(
|
||||||
|
"--accent",
|
||||||
|
`color-mix(in oklab, ${primary} 15%, transparent)`,
|
||||||
|
);
|
||||||
root.style.setProperty("--chart-1", primary);
|
root.style.setProperty("--chart-1", primary);
|
||||||
|
|
||||||
if (config.organizationName) {
|
if (config.organizationName) {
|
||||||
|
|||||||
@@ -140,7 +140,7 @@ const DashboardPage = () => {
|
|||||||
<Link to="/organizations">
|
<Link to="/organizations">
|
||||||
<Button
|
<Button
|
||||||
variant="link"
|
variant="link"
|
||||||
className="text-purple-600 dark:text-purple-400 text-sm px-0">
|
className="text-primary dark:text-primary-400 text-sm px-0">
|
||||||
{t("organization.viewMore")}
|
{t("organization.viewMore")}
|
||||||
</Button>
|
</Button>
|
||||||
</Link>
|
</Link>
|
||||||
|
|||||||
@@ -448,14 +448,14 @@ export const FormAddableList = ({
|
|||||||
{items.map((unit: string, index: number) => (
|
{items.map((unit: string, index: number) => (
|
||||||
<div
|
<div
|
||||||
key={`${unit}-${index}`}
|
key={`${unit}-${index}`}
|
||||||
className="flex items-center gap-1 px-2 py-1 bg-blue-50 text-blue-700 border border-blue-100 rounded-md"
|
className="flex items-center gap-1 px-2 py-1 bg-primary-50 text-primary-700 border border-primary-100 rounded-md"
|
||||||
>
|
>
|
||||||
<span className="text-sm truncate max-w-[200px]">
|
<span className="text-sm truncate max-w-[200px]">
|
||||||
{unit}
|
{unit}
|
||||||
</span>
|
</span>
|
||||||
<X
|
<X
|
||||||
size={14}
|
size={14}
|
||||||
className="cursor-pointer text-blue-700 hover:text-red-600 ml-1"
|
className="cursor-pointer text-primary-700 hover:text-red-600 ml-1"
|
||||||
onClick={() => handleDelete(index)}
|
onClick={() => handleDelete(index)}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
@@ -503,7 +503,7 @@ export const ClearableSelect = ({
|
|||||||
</span>
|
</span>
|
||||||
<X
|
<X
|
||||||
size={17}
|
size={17}
|
||||||
className="cursor-pointer text-blue-700 hover:text-red-600 ml-1"
|
className="cursor-pointer text-primary-700 hover:text-red-600 ml-1"
|
||||||
onClick={() => field.onChange("")}
|
onClick={() => field.onChange("")}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
@@ -784,7 +784,7 @@ export const SelectedItemsChips = ({
|
|||||||
{items.map((item) => (
|
{items.map((item) => (
|
||||||
<div
|
<div
|
||||||
key={item.id}
|
key={item.id}
|
||||||
className="flex items-center bg-blue-100 text-blue-800 px-3 py-1 rounded-full text-sm"
|
className="flex items-center bg-primary-100 text-primary-800 px-3 py-1 rounded-full text-sm"
|
||||||
>
|
>
|
||||||
<span className="truncate max-w-[180px]">
|
<span className="truncate max-w-[180px]">
|
||||||
{localizedName(item.name)}
|
{localizedName(item.name)}
|
||||||
|
|||||||
@@ -43,7 +43,7 @@ export const SliderUI: FC<SliderUIProps> = ({
|
|||||||
disabled={disabled}
|
disabled={disabled}
|
||||||
className="relative flex w-full touch-none select-none items-center h-5">
|
className="relative flex w-full touch-none select-none items-center h-5">
|
||||||
<SliderPrimitive.Track className="relative bg-gray-200 flex-1 h-1 rounded-full">
|
<SliderPrimitive.Track className="relative bg-gray-200 flex-1 h-1 rounded-full">
|
||||||
<SliderPrimitive.Range className="absolute bg-blue-500 h-full rounded-full" />
|
<SliderPrimitive.Range className="absolute bg-primary-500 h-full rounded-full" />
|
||||||
</SliderPrimitive.Track>
|
</SliderPrimitive.Track>
|
||||||
<SliderPrimitive.Thumb className="block w-5 h-5 bg-white border border-gray-300 rounded-full shadow" />
|
<SliderPrimitive.Thumb className="block w-5 h-5 bg-white border border-gray-300 rounded-full shadow" />
|
||||||
</SliderPrimitive.Root>
|
</SliderPrimitive.Root>
|
||||||
|
|||||||
@@ -11,9 +11,9 @@ const badgeVariants = cva(
|
|||||||
variant: {
|
variant: {
|
||||||
//draft and forward
|
//draft and forward
|
||||||
default:
|
default:
|
||||||
"bg-blue-100 text-blue-800 text-sm font-medium me-2 px-3 py-0.5 rounded-full dark:bg-blue-900 dark:text-blue-300",
|
"bg-primary-100 text-primary-800 text-sm font-medium me-2 px-3 py-0.5 rounded-full dark:bg-primary-900 dark:text-primary-300",
|
||||||
secondary:
|
secondary:
|
||||||
"bg-indigo-100 text-indigo-800 text-sm font-medium me-2 px-2.5 py-0.5 rounded-full dark:bg-indigo-900 dark:text-indigo-300",
|
"bg-primary-100 text-primary-800 text-sm font-medium me-2 px-2.5 py-0.5 rounded-full dark:bg-primary-900 dark:text-primary-300",
|
||||||
destructive:
|
destructive:
|
||||||
"bg-gray-100 text-gray-800 text-sm font-medium me-2 px-2.5 py-0.5 rounded-sm dark:bg-gray-700 dark:text-gray-300",
|
"bg-gray-100 text-gray-800 text-sm font-medium me-2 px-2.5 py-0.5 rounded-sm dark:bg-gray-700 dark:text-gray-300",
|
||||||
outline:
|
outline:
|
||||||
|
|||||||
@@ -37,7 +37,7 @@ const buttonVariants = cva(
|
|||||||
actionYellow:
|
actionYellow:
|
||||||
"bg-amber-50/90 dark:bg-amber-950/20 border border-amber-200 dark:border-amber-900/40 text-amber-700 dark:text-amber-300 hover:bg-amber-100 dark:hover:bg-amber-950/40 hover:text-amber-800 dark:hover:text-amber-200 hover:border-amber-300 dark:hover:border-amber-800/60 focus-visible:ring-amber-500 rounded-full h-9 px-4 flex items-center gap-2 font-medium shadow-xs hover:shadow-md transition-all duration-300",
|
"bg-amber-50/90 dark:bg-amber-950/20 border border-amber-200 dark:border-amber-900/40 text-amber-700 dark:text-amber-300 hover:bg-amber-100 dark:hover:bg-amber-950/40 hover:text-amber-800 dark:hover:text-amber-200 hover:border-amber-300 dark:hover:border-amber-800/60 focus-visible:ring-amber-500 rounded-full h-9 px-4 flex items-center gap-2 font-medium shadow-xs hover:shadow-md transition-all duration-300",
|
||||||
actionBlue:
|
actionBlue:
|
||||||
"bg-blue-50/90 dark:bg-blue-950/20 border border-blue-200 dark:border-blue-900/40 text-blue-700 dark:text-blue-300 hover:bg-blue-100 dark:hover:bg-blue-950/40 hover:text-blue-800 dark:hover:text-blue-200 hover:border-blue-300 dark:hover:border-blue-800/60 focus-visible:ring-blue-500 rounded-full h-9 px-4 flex items-center gap-2 font-medium shadow-xs hover:shadow-md transition-all duration-300",
|
"bg-primary-50/90 dark:bg-primary-950/20 border border-primary-200 dark:border-primary-900/40 text-primary-700 dark:text-primary-300 hover:bg-primary-100 dark:hover:bg-primary-950/40 hover:text-primary-800 dark:hover:text-primary-200 hover:border-primary-300 dark:hover:border-primary-800/60 focus-visible:ring-primary-500 rounded-full h-9 px-4 flex items-center gap-2 font-medium shadow-xs hover:shadow-md transition-all duration-300",
|
||||||
actionPurple:
|
actionPurple:
|
||||||
"bg-purple-50/90 dark:bg-purple-950/20 border border-purple-200 dark:border-purple-900/40 text-purple-700 dark:text-purple-300 hover:bg-purple-100 dark:hover:bg-purple-950/40 hover:text-purple-800 dark:hover:text-purple-200 hover:border-purple-300 dark:hover:border-purple-800/60 focus-visible:ring-purple-500 rounded-full h-9 px-4 flex items-center gap-2 font-medium shadow-xs hover:shadow-md transition-all duration-300",
|
"bg-purple-50/90 dark:bg-purple-950/20 border border-purple-200 dark:border-purple-900/40 text-purple-700 dark:text-purple-300 hover:bg-purple-100 dark:hover:bg-purple-950/40 hover:text-purple-800 dark:hover:text-purple-200 hover:border-purple-300 dark:hover:border-purple-800/60 focus-visible:ring-purple-500 rounded-full h-9 px-4 flex items-center gap-2 font-medium shadow-xs hover:shadow-md transition-all duration-300",
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -466,7 +466,7 @@ function TreeItem({
|
|||||||
{selectedCount !== null && selectedCount > 0 && (
|
{selectedCount !== null && selectedCount > 0 && (
|
||||||
<Badge
|
<Badge
|
||||||
variant="secondary"
|
variant="secondary"
|
||||||
className="mr-2 bg-blue-100 hover:bg-blue-100 flex-shrink-0">
|
className="mr-2 bg-primary-100 hover:bg-primary-100 flex-shrink-0">
|
||||||
{selectedCount} selected
|
{selectedCount} selected
|
||||||
</Badge>
|
</Badge>
|
||||||
)}
|
)}
|
||||||
@@ -1053,7 +1053,7 @@ export default function TreeView({
|
|||||||
onMouseMove={handleMouseMove}>
|
onMouseMove={handleMouseMove}>
|
||||||
{isDragging && (
|
{isDragging && (
|
||||||
<div
|
<div
|
||||||
className="absolute inset-0 bg-blue-500/0 pointer-events-none"
|
className="absolute inset-0 bg-primary-500/0 pointer-events-none"
|
||||||
style={{
|
style={{
|
||||||
top: Math.min(
|
top: Math.min(
|
||||||
dragStart || 0,
|
dragStart || 0,
|
||||||
|
|||||||
@@ -321,11 +321,11 @@ const SetPasswordPage = () => {
|
|||||||
type="button"
|
type="button"
|
||||||
onClick={handleResendCode}
|
onClick={handleResendCode}
|
||||||
disabled={isResendingCode || !email || !phoneNumber || isSubmitting}
|
disabled={isResendingCode || !email || !phoneNumber || isSubmitting}
|
||||||
className="text-sm text-indigo-600 hover:text-indigo-800 font-medium flex items-center justify-center space-x-1 mx-auto"
|
className="text-sm text-primary-600 hover:text-primary-800 font-medium flex items-center justify-center space-x-1 mx-auto"
|
||||||
>
|
>
|
||||||
{isResendingCode ? (
|
{isResendingCode ? (
|
||||||
<>
|
<>
|
||||||
<RefreshCw className="animate-spin h-4 w-4 text-indigo-600 mr-2" />
|
<RefreshCw className="animate-spin h-4 w-4 text-primary-600 mr-2" />
|
||||||
<span>Sending...</span>
|
<span>Sending...</span>
|
||||||
</>
|
</>
|
||||||
) : (
|
) : (
|
||||||
|
|||||||
@@ -38,8 +38,8 @@ export function ActivityStats({
|
|||||||
label: t("auditLog.stats.totalActivities"),
|
label: t("auditLog.stats.totalActivities"),
|
||||||
value: totalActivities.toLocaleString(),
|
value: totalActivities.toLocaleString(),
|
||||||
icon: Activity,
|
icon: Activity,
|
||||||
color: "text-blue-600 dark:text-blue-400",
|
color: "text-primary-600 dark:text-primary-400",
|
||||||
bgColor: "bg-blue-50 dark:bg-blue-950",
|
bgColor: "bg-primary-50 dark:bg-primary-950",
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
label: t("auditLog.stats.successRate"),
|
label: t("auditLog.stats.successRate"),
|
||||||
|
|||||||
@@ -50,11 +50,11 @@ export function ExportModal({
|
|||||||
const textStrong = 'text-gray-900 dark:text-gray-100'
|
const textStrong = 'text-gray-900 dark:text-gray-100'
|
||||||
const textMuted = 'text-gray-600 dark:text-gray-300'
|
const textMuted = 'text-gray-600 dark:text-gray-300'
|
||||||
const textSubtle = 'text-gray-500 dark:text-gray-400'
|
const textSubtle = 'text-gray-500 dark:text-gray-400'
|
||||||
const primaryBorder = 'border-primary dark:border-sky-600'
|
const primaryBorder = 'border-primary dark:border-primary-600'
|
||||||
const hoverBg = 'hover:bg-accent dark:hover:bg-gray-800'
|
const hoverBg = 'hover:bg-accent dark:hover:bg-gray-800'
|
||||||
const hoverText = 'hover:text-accent-foreground dark:hover:text-gray-100'
|
const hoverText = 'hover:text-accent-foreground dark:hover:text-gray-100'
|
||||||
const primaryBtn =
|
const primaryBtn =
|
||||||
'bg-primary dark:bg-sky-600 hover:bg-primary/90 dark:hover:bg-sky-500 text-white'
|
'bg-primary dark:bg-primary-600 hover:bg-primary/90 dark:hover:bg-primary-500 text-white'
|
||||||
|
|
||||||
const handleExport = () => {
|
const handleExport = () => {
|
||||||
onExport(options)
|
onExport(options)
|
||||||
|
|||||||
@@ -152,7 +152,7 @@ export const AuditLogDetailPanel = ({
|
|||||||
{change.to !== undefined && (
|
{change.to !== undefined && (
|
||||||
<p className="text-gray-600 mt-1">
|
<p className="text-gray-600 mt-1">
|
||||||
<span className="text-xs font-semibold">To:</span>{" "}
|
<span className="text-xs font-semibold">To:</span>{" "}
|
||||||
<code className="bg-blue-50 px-2 py-1 rounded text-xs text-blue-900">
|
<code className="bg-primary-50 px-2 py-1 rounded text-xs text-primary-900">
|
||||||
{JSON.stringify(change.to)}
|
{JSON.stringify(change.to)}
|
||||||
</code>
|
</code>
|
||||||
</p>
|
</p>
|
||||||
|
|||||||
@@ -51,8 +51,8 @@ export const ForgotPassword = () => {
|
|||||||
};
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="min-h-screen bg-gradient-to-br from-cyan-50 via-white to-primary-50 relative overflow-hidden">
|
<div className="min-h-screen bg-gradient-to-br from-primary-50 via-white to-primary-50 relative overflow-hidden">
|
||||||
<div className="absolute top-0 right-0 w-96 h-96 bg-cyan-100/30 rounded-full blur-3xl" />
|
<div className="absolute top-0 right-0 w-96 h-96 bg-primary-100/30 rounded-full blur-3xl" />
|
||||||
<div className="absolute bottom-0 left-0 w-96 h-96 bg-primary-100/30 rounded-full blur-3xl" />
|
<div className="absolute bottom-0 left-0 w-96 h-96 bg-primary-100/30 rounded-full blur-3xl" />
|
||||||
|
|
||||||
<button
|
<button
|
||||||
@@ -77,7 +77,7 @@ export const ForgotPassword = () => {
|
|||||||
<h1 className="text-2xl font-bold text-white mb-2">
|
<h1 className="text-2xl font-bold text-white mb-2">
|
||||||
{t("forgotpassword.title")}
|
{t("forgotpassword.title")}
|
||||||
</h1>
|
</h1>
|
||||||
<p className="text-cyan-50 text-sm">
|
<p className="text-primary-50 text-sm">
|
||||||
{t("forgotpassword.subtitle")}
|
{t("forgotpassword.subtitle")}
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
@@ -111,7 +111,7 @@ export const ForgotPassword = () => {
|
|||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="flex items-start gap-3 p-4 bg-cyan-50 border border-cyan-100 rounded-lg">
|
<div className="flex items-start gap-3 p-4 bg-primary-50 border border-primary-100 rounded-lg">
|
||||||
<Mail className="w-5 h-5 text-primary flex-shrink-0 mt-0.5" />
|
<Mail className="w-5 h-5 text-primary flex-shrink-0 mt-0.5" />
|
||||||
<div className="flex-1">
|
<div className="flex-1">
|
||||||
<p className="text-sm text-gray-700 font-medium mb-1">
|
<p className="text-sm text-gray-700 font-medium mb-1">
|
||||||
|
|||||||
@@ -1,11 +1,15 @@
|
|||||||
import { useEffect, useState } from "react";
|
import { useEffect, useState } from "react";
|
||||||
|
|
||||||
|
// Same storage key/values as main.tsx and FreightDashboardLayout so the
|
||||||
|
// vendored IAM UI toggle and the host dashboard toggle stay in sync.
|
||||||
|
const THEME_STORAGE_KEY = "edr-theme";
|
||||||
|
|
||||||
export const useDarkMode = () => {
|
export const useDarkMode = () => {
|
||||||
const [isDarkMode, setIsDarkMode] = useState(() => {
|
const [isDarkMode, setIsDarkMode] = useState(() => {
|
||||||
// Initialize from localStorage or system preference
|
// Initialize from localStorage or system preference
|
||||||
const savedTheme = localStorage.getItem("darkMode");
|
const savedTheme = localStorage.getItem(THEME_STORAGE_KEY);
|
||||||
if (savedTheme) {
|
if (savedTheme === "dark" || savedTheme === "light") {
|
||||||
return savedTheme === "true";
|
return savedTheme === "dark";
|
||||||
}
|
}
|
||||||
return window.matchMedia("(prefers-color-scheme: dark)").matches;
|
return window.matchMedia("(prefers-color-scheme: dark)").matches;
|
||||||
});
|
});
|
||||||
@@ -14,10 +18,10 @@ export const useDarkMode = () => {
|
|||||||
// Apply dark mode class on mount and when isDarkMode changes
|
// Apply dark mode class on mount and when isDarkMode changes
|
||||||
if (isDarkMode) {
|
if (isDarkMode) {
|
||||||
document.documentElement.classList.add("dark");
|
document.documentElement.classList.add("dark");
|
||||||
localStorage.setItem("darkMode", "true");
|
localStorage.setItem(THEME_STORAGE_KEY, "dark");
|
||||||
} else {
|
} else {
|
||||||
document.documentElement.classList.remove("dark");
|
document.documentElement.classList.remove("dark");
|
||||||
localStorage.setItem("darkMode", "false");
|
localStorage.setItem(THEME_STORAGE_KEY, "light");
|
||||||
}
|
}
|
||||||
}, [isDarkMode]);
|
}, [isDarkMode]);
|
||||||
|
|
||||||
|
|||||||
@@ -239,7 +239,7 @@ export default function AuditLogPageShared({
|
|||||||
case "warning":
|
case "warning":
|
||||||
return "bg-yellow-100 text-yellow-800 dark:bg-yellow-950 dark:text-yellow-300";
|
return "bg-yellow-100 text-yellow-800 dark:bg-yellow-950 dark:text-yellow-300";
|
||||||
case "pending":
|
case "pending":
|
||||||
return "bg-blue-100 text-blue-800 dark:bg-blue-950 dark:text-blue-300";
|
return "bg-primary-100 text-primary-800 dark:bg-primary-950 dark:text-primary-300";
|
||||||
default:
|
default:
|
||||||
return "bg-gray-100 text-gray-800 dark:bg-gray-800 dark:text-gray-300";
|
return "bg-gray-100 text-gray-800 dark:bg-gray-800 dark:text-gray-300";
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -17,11 +17,11 @@ export const ActivityTimeline = ({ activities }: ActivityTimelineProps) => {
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="relative pl-6">
|
<div className="relative pl-6">
|
||||||
<div className="absolute left-2 top-2 bottom-2 w-0.5 bg-purple-500" />
|
<div className="absolute left-2 top-2 bottom-2 w-0.5 bg-primary" />
|
||||||
<ul className="space-y-6">
|
<ul className="space-y-6">
|
||||||
{activities.map((activity) => (
|
{activities.map((activity) => (
|
||||||
<li key={activity.id} className="relative pl-4">
|
<li key={activity.id} className="relative pl-4">
|
||||||
<div className="absolute left-0 top-1 w-3 h-3 bg-purple-500 rounded-full" />
|
<div className="absolute left-0 top-1 w-3 h-3 bg-primary rounded-full" />
|
||||||
<div className="text-xs text-muted-foreground">
|
<div className="text-xs text-muted-foreground">
|
||||||
{formatDistanceToNow(new Date(activity.timestamp), {
|
{formatDistanceToNow(new Date(activity.timestamp), {
|
||||||
addSuffix: true,
|
addSuffix: true,
|
||||||
@@ -32,7 +32,7 @@ export const ActivityTimeline = ({ activities }: ActivityTimelineProps) => {
|
|||||||
</div>
|
</div>
|
||||||
<div className="text-xs">
|
<div className="text-xs">
|
||||||
By{" "}
|
By{" "}
|
||||||
<span className="font-medium text-purple-600">
|
<span className="font-medium text-primary">
|
||||||
{activity.user}
|
{activity.user}
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -7,7 +7,7 @@ export const HeaderBar = () => {
|
|||||||
<h1 className="text-2xl font-bold text-gray-800 dark:text-gray-100">
|
<h1 className="text-2xl font-bold text-gray-800 dark:text-gray-100">
|
||||||
{tenantConfig.appName} Dashboard
|
{tenantConfig.appName} Dashboard
|
||||||
</h1>
|
</h1>
|
||||||
<button className="bg-purple-600 hover:bg-purple-700 text-white px-4 py-2 rounded-md text-sm">
|
<button className="bg-primary hover:bg-primary-700 text-white px-4 py-2 rounded-md text-sm">
|
||||||
+ Add Organization
|
+ Add Organization
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -38,7 +38,7 @@ export const OrgTable = ({ organizations }: OrgTableProps) => {
|
|||||||
{organizations.map((org, index) => (
|
{organizations.map((org, index) => (
|
||||||
<TableRow
|
<TableRow
|
||||||
key={org.id}
|
key={org.id}
|
||||||
className={index % 2 ? "bg-purple-50/20 dark:bg-gray-700/40" : "bg-white dark:bg-gray-800"}
|
className={index % 2 ? "bg-primary-50/20 dark:bg-gray-700/40" : "bg-white dark:bg-gray-800"}
|
||||||
>
|
>
|
||||||
<TableCell className="px-6 py-3 font-medium">
|
<TableCell className="px-6 py-3 font-medium">
|
||||||
{org.name?.en || "N/A"}
|
{org.name?.en || "N/A"}
|
||||||
|
|||||||
@@ -34,7 +34,7 @@ export const StatCard: React.FC<StatCardProps> = ({
|
|||||||
onClick={onClick}
|
onClick={onClick}
|
||||||
className={cn(
|
className={cn(
|
||||||
"p-4 rounded-xl shadow-sm transition-colors",
|
"p-4 rounded-xl shadow-sm transition-colors",
|
||||||
variant === "primary" ? "bg-purple-600 text-white" : "bg-white dark:bg-gray-800 dark:border-gray-700",
|
variant === "primary" ? "bg-primary text-white" : "bg-white dark:bg-gray-800 dark:border-gray-700",
|
||||||
onClick && "cursor-pointer hover:bg-gray-100 dark:hover:bg-gray-700"
|
onClick && "cursor-pointer hover:bg-gray-100 dark:hover:bg-gray-700"
|
||||||
)}
|
)}
|
||||||
>
|
>
|
||||||
|
|||||||
@@ -43,7 +43,7 @@ export const ExternalUsersColumnDefn = (
|
|||||||
className={`px-2 py-1 rounded-full text-xs font-medium ${
|
className={`px-2 py-1 rounded-full text-xs font-medium ${
|
||||||
userType === "external_organization"
|
userType === "external_organization"
|
||||||
? "bg-purple-100 text-purple-800 dark:bg-purple-900/40 dark:text-purple-300"
|
? "bg-purple-100 text-purple-800 dark:bg-purple-900/40 dark:text-purple-300"
|
||||||
: "bg-blue-100 text-blue-800 dark:bg-blue-900/40 dark:text-blue-300"
|
: "bg-primary-100 text-primary-800 dark:bg-primary-900/40 dark:text-primary-300"
|
||||||
}`}
|
}`}
|
||||||
>
|
>
|
||||||
{displayText}
|
{displayText}
|
||||||
@@ -65,7 +65,7 @@ export const ExternalUsersColumnDefn = (
|
|||||||
return "bg-red-100 text-red-600 hover:bg-red-100 dark:bg-red-900/40 dark:text-red-300 dark:hover:bg-red-900/50";
|
return "bg-red-100 text-red-600 hover:bg-red-100 dark:bg-red-900/40 dark:text-red-300 dark:hover:bg-red-900/50";
|
||||||
case "pending":
|
case "pending":
|
||||||
default:
|
default:
|
||||||
return "bg-blue-100 text-blue-600 hover:bg-blue-100 dark:bg-blue-900/40 dark:text-blue-300 dark:hover:bg-blue-900/50";
|
return "bg-primary-100 text-primary-600 hover:bg-primary-100 dark:bg-primary-900/40 dark:text-primary-300 dark:hover:bg-primary-900/50";
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -117,7 +117,7 @@ export const ExternalUsersColumnDefn = (
|
|||||||
</button>
|
</button>
|
||||||
{/* <button
|
{/* <button
|
||||||
onClick={handleEdit}
|
onClick={handleEdit}
|
||||||
className="px-2 py-1 bg-blue-100 rounded hover:bg-blue-200 text-sm font-medium"
|
className="px-2 py-1 bg-primary-100 rounded hover:bg-primary-200 text-sm font-medium"
|
||||||
>
|
>
|
||||||
{t("Edit")}
|
{t("Edit")}
|
||||||
</button> */}
|
</button> */}
|
||||||
|
|||||||
@@ -186,7 +186,7 @@ const ViewDocument: React.FC<ViewDocumentProps> = ({ userId }) => {
|
|||||||
</h3>
|
</h3>
|
||||||
<div className="grid grid-cols-1 md:grid-cols-3 gap-4">
|
<div className="grid grid-cols-1 md:grid-cols-3 gap-4">
|
||||||
<div className="flex items-center gap-3 p-3 bg-white dark:bg-gray-800 rounded-lg border border-gray-200 dark:border-gray-700">
|
<div className="flex items-center gap-3 p-3 bg-white dark:bg-gray-800 rounded-lg border border-gray-200 dark:border-gray-700">
|
||||||
<Building className="w-5 h-5 text-blue-600" />
|
<Building className="w-5 h-5 text-primary-600" />
|
||||||
<div className="flex-1 min-w-0">
|
<div className="flex-1 min-w-0">
|
||||||
<p className="text-sm font-medium text-gray-500 dark:text-gray-400">
|
<p className="text-sm font-medium text-gray-500 dark:text-gray-400">
|
||||||
{t("viewDocument.organizationName")}
|
{t("viewDocument.organizationName")}
|
||||||
@@ -340,7 +340,7 @@ const ViewDocument: React.FC<ViewDocumentProps> = ({ userId }) => {
|
|||||||
<span
|
<span
|
||||||
className={`inline-block w-2 h-2 rounded-full mt-1.5 mr-2 ${
|
className={`inline-block w-2 h-2 rounded-full mt-1.5 mr-2 ${
|
||||||
req.isOptional
|
req.isOptional
|
||||||
? "bg-blue-400"
|
? "bg-primary-400"
|
||||||
: "bg-primary-400"
|
: "bg-primary-400"
|
||||||
}`}
|
}`}
|
||||||
/>
|
/>
|
||||||
|
|||||||
@@ -272,8 +272,8 @@ export function AssignAdminDialog({
|
|||||||
className={cn(
|
className={cn(
|
||||||
"w-full flex items-center justify-between px-3 py-2 rounded-md text-left text-sm transition-colors",
|
"w-full flex items-center justify-between px-3 py-2 rounded-md text-left text-sm transition-colors",
|
||||||
!selectedUnit
|
!selectedUnit
|
||||||
? "bg-blue-200 text-blue-800 dark:bg-blue-900/40 dark:text-blue-300"
|
? "bg-primary-200 text-primary-800 dark:bg-primary-900/40 dark:text-primary-300"
|
||||||
: "hover:bg-blue-100 dark:hover:bg-blue-900/30 text-gray-700 dark:text-gray-300"
|
: "hover:bg-primary-100 dark:hover:bg-primary-900/30 text-gray-700 dark:text-gray-300"
|
||||||
)}
|
)}
|
||||||
>
|
>
|
||||||
<span>
|
<span>
|
||||||
@@ -292,8 +292,8 @@ export function AssignAdminDialog({
|
|||||||
className={cn(
|
className={cn(
|
||||||
"w-full flex items-center justify-between px-3 py-2 rounded-md text-left text-sm transition-colors",
|
"w-full flex items-center justify-between px-3 py-2 rounded-md text-left text-sm transition-colors",
|
||||||
selectedUnit === unit.id
|
selectedUnit === unit.id
|
||||||
? "bg-blue-200 text-blue-800 dark:bg-blue-900/40 dark:text-blue-300"
|
? "bg-primary-200 text-primary-800 dark:bg-primary-900/40 dark:text-primary-300"
|
||||||
: "hover:bg-blue-100 dark:hover:bg-blue-900/30 text-gray-700 dark:text-gray-300"
|
: "hover:bg-primary-100 dark:hover:bg-primary-900/30 text-gray-700 dark:text-gray-300"
|
||||||
)}
|
)}
|
||||||
>
|
>
|
||||||
<span>{localizedName(unit.name)}</span>
|
<span>{localizedName(unit.name)}</span>
|
||||||
|
|||||||
@@ -118,8 +118,8 @@ export const OrganizationCard: React.FC<OrganizationCardProps> = ({ id }) => {
|
|||||||
break;
|
break;
|
||||||
case "woreda":
|
case "woreda":
|
||||||
Icon = MapPin;
|
Icon = MapPin;
|
||||||
bgColor = "bg-blue-100 dark:bg-blue-800";
|
bgColor = "bg-primary-100 dark:bg-primary-800";
|
||||||
textColor = "text-blue-800 dark:text-blue-100";
|
textColor = "text-primary-800 dark:text-primary-100";
|
||||||
break;
|
break;
|
||||||
case "subcity":
|
case "subcity":
|
||||||
Icon = Home;
|
Icon = Home;
|
||||||
|
|||||||
@@ -91,7 +91,7 @@ export const OrganizationsColumnDefn: ColumnDef<OrganizationDto>[] = [
|
|||||||
<Badge
|
<Badge
|
||||||
className={`${
|
className={`${
|
||||||
rowData.isGovernmentOrganization
|
rowData.isGovernmentOrganization
|
||||||
? "bg-blue-600 text-white hover:bg-blue-400"
|
? "bg-primary-600 text-white hover:bg-primary-400"
|
||||||
: "bg-red-600 text-white hover:bg-red-400"
|
: "bg-red-600 text-white hover:bg-red-400"
|
||||||
} rounded-full px-6 py-1 font-medium`}
|
} rounded-full px-6 py-1 font-medium`}
|
||||||
>
|
>
|
||||||
|
|||||||
@@ -104,7 +104,7 @@ export const getSitesColumnDefn = ({
|
|||||||
{t("sites.actions.view")}
|
{t("sites.actions.view")}
|
||||||
</DropdownMenuItem>
|
</DropdownMenuItem>
|
||||||
<DropdownMenuItem onClick={() => onEdit(site)}>
|
<DropdownMenuItem onClick={() => onEdit(site)}>
|
||||||
<Edit className="mr-2 h-4 w-4 text-blue-500" />
|
<Edit className="mr-2 h-4 w-4 text-primary-500" />
|
||||||
{t("sites.actions.edit")}
|
{t("sites.actions.edit")}
|
||||||
</DropdownMenuItem>
|
</DropdownMenuItem>
|
||||||
<DropdownMenuItem onClick={() => onBranding(site)}>
|
<DropdownMenuItem onClick={() => onBranding(site)}>
|
||||||
|
|||||||
@@ -48,7 +48,7 @@ const TemplateCard = () => {
|
|||||||
templateList.find((t) => t.id === selectedTemplateId) ?? null;
|
templateList.find((t) => t.id === selectedTemplateId) ?? null;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Card className="bg-gradient-to-br from-blue-50 to-indigo-50">
|
<Card className="bg-gradient-to-br from-primary-50 to-primary-50">
|
||||||
<CardHeader>
|
<CardHeader>
|
||||||
<div className="flex items-center justify-between">
|
<div className="flex items-center justify-between">
|
||||||
<div>
|
<div>
|
||||||
|
|||||||
@@ -55,12 +55,12 @@ export const TemplateEditor: React.FC<TemplateEditorProps> = ({
|
|||||||
${hasPlaceholders ? `
|
${hasPlaceholders ? `
|
||||||
.placeholder {
|
.placeholder {
|
||||||
${isDarkMode ? `
|
${isDarkMode ? `
|
||||||
background-color: #1e3a5f;
|
background-color: #065f46;
|
||||||
border: 1px dashed #60a5fa;
|
border: 1px dashed #34d399;
|
||||||
color: #93c5fd;
|
color: #a7f3d0;
|
||||||
` : `
|
` : `
|
||||||
background-color: #e0f2fe;
|
background-color: #ecfdf5;
|
||||||
border: 1px dashed #38bdf8;
|
border: 1px dashed #0EA371;
|
||||||
`}
|
`}
|
||||||
padding: 2px 4px;
|
padding: 2px 4px;
|
||||||
border-radius: 4px;
|
border-radius: 4px;
|
||||||
|
|||||||
@@ -14,19 +14,23 @@ export const AppLayout = () => {
|
|||||||
const userRoles = user?.roles?.map((role) => role.key) || [];
|
const userRoles = user?.roles?.map((role) => role.key) || [];
|
||||||
const isSuperAdmin = userRoles.includes("super_admin");
|
const isSuperAdmin = userRoles.includes("super_admin");
|
||||||
|
|
||||||
|
// html/body/#root are `overflow: hidden` (index.css) — the host chrome
|
||||||
|
// scrolls inside FreightDashboardLayout. This subtree renders its own
|
||||||
|
// full-page layout instead, so it must be its own scroll container or
|
||||||
|
// nothing scrolls. Top/AppMenuTabs are `fixed`, unaffected by the scroller.
|
||||||
if (isAuthPage) {
|
if (isAuthPage) {
|
||||||
return (
|
return (
|
||||||
<div className="min-h-screen w-full bg-gray-50">
|
<div className="h-dvh w-full overflow-y-auto bg-gray-50">
|
||||||
<Outlet />
|
<Outlet />
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="w-full flex flex-col min-h-screen bg-background text-foreground">
|
<div className="w-full h-dvh overflow-y-auto flex flex-col bg-background text-foreground">
|
||||||
<Top onToggleSidebar={toggleSidebar} showRecordManagementShortcut={!isSuperAdmin} />
|
<Top onToggleSidebar={toggleSidebar} showRecordManagementShortcut={!isSuperAdmin} />
|
||||||
<AppMenuTabs />
|
<AppMenuTabs />
|
||||||
<div className="px-2 pb-2 pt-8 sm:px-4 sm:pb-4 sm:pt-10 flex-1">
|
<div className="px-2 pb-2 pt-8 mt-8 sm:px-4 sm:pb-4 sm:pt-10 flex-1">
|
||||||
<div className="w-full overflow-x-auto">
|
<div className="w-full overflow-x-auto">
|
||||||
<Outlet />
|
<Outlet />
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -1619,11 +1619,11 @@ export const TemplateSampleForm = ({
|
|||||||
{selectedExternalCC.map((unitName, idx) => (
|
{selectedExternalCC.map((unitName, idx) => (
|
||||||
<div
|
<div
|
||||||
key={idx}
|
key={idx}
|
||||||
className="flex items-center gap-2 px-3 py-1 bg-blue-100 dark:bg-blue-900/40 text-blue-700 dark:text-blue-300 rounded-full text-sm">
|
className="flex items-center gap-2 px-3 py-1 bg-primary-100 dark:bg-primary-900/40 text-primary-700 dark:text-primary-300 rounded-full text-sm">
|
||||||
<span>{unitName}</span>
|
<span>{unitName}</span>
|
||||||
<button
|
<button
|
||||||
onClick={() => handleRemoveExternalCC(unitName)}
|
onClick={() => handleRemoveExternalCC(unitName)}
|
||||||
className="hover:text-blue-900 dark:hover:text-blue-100">
|
className="hover:text-primary-900 dark:hover:text-primary-100">
|
||||||
<X className="h-4 w-4" />
|
<X className="h-4 w-4" />
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
@@ -1692,11 +1692,11 @@ export const TemplateSampleForm = ({
|
|||||||
return (
|
return (
|
||||||
<div
|
<div
|
||||||
key={recipientId}
|
key={recipientId}
|
||||||
className="flex items-center gap-2 px-3 py-1 bg-blue-100 dark:bg-blue-900/40 text-blue-700 dark:text-blue-300 rounded-full text-sm">
|
className="flex items-center gap-2 px-3 py-1 bg-primary-100 dark:bg-primary-900/40 text-primary-700 dark:text-primary-300 rounded-full text-sm">
|
||||||
<span>{name}</span>
|
<span>{name}</span>
|
||||||
<button
|
<button
|
||||||
onClick={() => handleRemoveRecipient(recipientId)}
|
onClick={() => handleRemoveRecipient(recipientId)}
|
||||||
className="hover:text-blue-900 dark:hover:text-blue-100">
|
className="hover:text-primary-900 dark:hover:text-primary-100">
|
||||||
<X className="h-4 w-4" />
|
<X className="h-4 w-4" />
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -79,7 +79,7 @@ export const TemplateSamplePreviewPage = ({
|
|||||||
</button>
|
</button>
|
||||||
<button
|
<button
|
||||||
onClick={handleDownloadPDF}
|
onClick={handleDownloadPDF}
|
||||||
className="px-4 py-2 bg-blue-600 text-white rounded-lg hover:bg-blue-700 transition-colors font-medium flex items-center gap-2"
|
className="px-4 py-2 bg-primary-600 text-white rounded-lg hover:bg-primary-700 transition-colors font-medium flex items-center gap-2"
|
||||||
>
|
>
|
||||||
<Download className="h-4 w-4" />
|
<Download className="h-4 w-4" />
|
||||||
{t("common.download", "Download")}
|
{t("common.download", "Download")}
|
||||||
|
|||||||
@@ -40,7 +40,7 @@ export const LETTER_PLACEMENT_CANVAS_ASSET_KEYS: LetterAbsoluteAssetKey[] = [
|
|||||||
];
|
];
|
||||||
|
|
||||||
const ASSET_COLORS: Record<LetterAbsoluteAssetKey, string> = {
|
const ASSET_COLORS: Record<LetterAbsoluteAssetKey, string> = {
|
||||||
signatures: "border-blue-500 bg-blue-500/20",
|
signatures: "border-primary-500 bg-primary-500/20",
|
||||||
seal: "border-emerald-500 bg-emerald-500/20",
|
seal: "border-emerald-500 bg-emerald-500/20",
|
||||||
stamp: "border-amber-500 bg-amber-500/20",
|
stamp: "border-amber-500 bg-amber-500/20",
|
||||||
senderSignature: "border-violet-500 bg-violet-500/20",
|
senderSignature: "border-violet-500 bg-violet-500/20",
|
||||||
|
|||||||
@@ -100,7 +100,7 @@ const ReviewAllPendingUsers = () => {
|
|||||||
<div className="p-6 w-full">
|
<div className="p-6 w-full">
|
||||||
<div className="flex justify-between items-center mb-6">
|
<div className="flex justify-between items-center mb-6">
|
||||||
<h1 className="text-2xl font-bold text-gray-900 flex items-center gap-2">
|
<h1 className="text-2xl font-bold text-gray-900 flex items-center gap-2">
|
||||||
<Users className="h-6 w-6 text-blue-600" />
|
<Users className="h-6 w-6 text-primary-600" />
|
||||||
{t("pendingUsers.title")}
|
{t("pendingUsers.title")}
|
||||||
</h1>
|
</h1>
|
||||||
</div>
|
</div>
|
||||||
@@ -133,7 +133,7 @@ const ReviewAllPendingUsers = () => {
|
|||||||
{/* Header with count */}
|
{/* Header with count */}
|
||||||
<div className="flex flex-col sm:flex-row sm:items-center sm:justify-between gap-4 mb-6">
|
<div className="flex flex-col sm:flex-row sm:items-center sm:justify-between gap-4 mb-6">
|
||||||
<h1 className="text-2xl font-bold text-gray-900 flex items-center gap-2">
|
<h1 className="text-2xl font-bold text-gray-900 flex items-center gap-2">
|
||||||
<Users className="h-6 w-6 text-blue-600" />
|
<Users className="h-6 w-6 text-primary-600" />
|
||||||
{t("pendingUsers.title")}
|
{t("pendingUsers.title")}
|
||||||
</h1>
|
</h1>
|
||||||
<span className="inline-flex items-center px-3 py-1 rounded-full text-sm font-medium bg-yellow-100 text-yellow-800">
|
<span className="inline-flex items-center px-3 py-1 rounded-full text-sm font-medium bg-yellow-100 text-yellow-800">
|
||||||
|
|||||||
@@ -50,7 +50,7 @@ const LetterTemplatesCard = ({ unitId }: Props) => {
|
|||||||
null;
|
null;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Card className="bg-gradient-to-br from-blue-50 to-indigo-50 dark:from-gray-800 dark:to-gray-900 dark:border-gray-700">
|
<Card className="bg-gradient-to-br from-primary-50 to-primary-50 dark:from-gray-800 dark:to-gray-900 dark:border-gray-700">
|
||||||
<CardHeader>
|
<CardHeader>
|
||||||
<div className="flex items-center justify-between">
|
<div className="flex items-center justify-between">
|
||||||
<div>
|
<div>
|
||||||
|
|||||||
@@ -358,7 +358,7 @@ const LetterTemplatesTable = ({ unitId }: Props) => {
|
|||||||
key={template.id}
|
key={template.id}
|
||||||
className={
|
className={
|
||||||
template.isGlobal
|
template.isGlobal
|
||||||
? "bg-blue-50/50 hover:bg-blue-50 dark:bg-blue-950/30 dark:hover:bg-blue-900/40"
|
? "bg-primary-50/50 hover:bg-primary-50 dark:bg-primary-950/30 dark:hover:bg-primary-900/40"
|
||||||
: ""
|
: ""
|
||||||
}
|
}
|
||||||
>
|
>
|
||||||
@@ -367,7 +367,7 @@ const LetterTemplatesTable = ({ unitId }: Props) => {
|
|||||||
<span className="flex items-center gap-2">
|
<span className="flex items-center gap-2">
|
||||||
{localizedName(template.name)}
|
{localizedName(template.name)}
|
||||||
{template.isGlobal && (
|
{template.isGlobal && (
|
||||||
<span className="rounded bg-blue-100 px-2 py-0.5 text-[10px] font-semibold text-blue-700 dark:bg-blue-900/50 dark:text-blue-200">
|
<span className="rounded bg-primary-100 px-2 py-0.5 text-[10px] font-semibold text-primary-700 dark:bg-primary-900/50 dark:text-primary-200">
|
||||||
Global
|
Global
|
||||||
</span>
|
</span>
|
||||||
)}
|
)}
|
||||||
@@ -409,7 +409,7 @@ const LetterTemplatesTable = ({ unitId }: Props) => {
|
|||||||
"contentManagement.adoptTemplate",
|
"contentManagement.adoptTemplate",
|
||||||
"Adopt Template",
|
"Adopt Template",
|
||||||
)}
|
)}
|
||||||
className="h-8 border-blue-200 px-3 text-blue-700 hover:bg-blue-100 hover:text-blue-800 dark:border-blue-700/60 dark:text-blue-300 dark:hover:bg-blue-900/40 dark:hover:text-blue-200"
|
className="h-8 border-primary-200 px-3 text-primary-700 hover:bg-primary-100 hover:text-primary-800 dark:border-primary-700/60 dark:text-primary-300 dark:hover:bg-primary-900/40 dark:hover:text-primary-200"
|
||||||
>
|
>
|
||||||
{t("common.adopt", "Adopt")}
|
{t("common.adopt", "Adopt")}
|
||||||
</Button>
|
</Button>
|
||||||
|
|||||||
@@ -160,7 +160,7 @@ export const PrefixSuffixTable = ({
|
|||||||
return (
|
return (
|
||||||
<button
|
<button
|
||||||
onClick={() => openCountPopup(item)}
|
onClick={() => openCountPopup(item)}
|
||||||
className="text-blue-600 hover:text-blue-800 dark:text-blue-400 dark:hover:text-blue-300 underline cursor-pointer">
|
className="text-primary-600 hover:text-primary-800 dark:text-primary-400 dark:hover:text-primary-300 underline cursor-pointer">
|
||||||
{count}
|
{count}
|
||||||
</button>
|
</button>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -425,7 +425,7 @@ export function TagBasedReferenceNumbers({
|
|||||||
<TableCell>
|
<TableCell>
|
||||||
<button
|
<button
|
||||||
onClick={() => openCountPopup(prefix)}
|
onClick={() => openCountPopup(prefix)}
|
||||||
className="text-blue-600 hover:text-blue-800 dark:text-blue-400 dark:hover:text-blue-300 underline cursor-pointer text-sm"
|
className="text-primary-600 hover:text-primary-800 dark:text-primary-400 dark:hover:text-primary-300 underline cursor-pointer text-sm"
|
||||||
>
|
>
|
||||||
{count}
|
{count}
|
||||||
</button>
|
</button>
|
||||||
|
|||||||
@@ -230,7 +230,7 @@ export default function PositionManagement() {
|
|||||||
value={selectedUnitId}
|
value={selectedUnitId}
|
||||||
onValueChange={(value) => setSelectedUnitId(value)}
|
onValueChange={(value) => setSelectedUnitId(value)}
|
||||||
>
|
>
|
||||||
<SelectTrigger className="mt-1 block w-full border-gray-300 rounded-md shadow-sm focus:ring-indigo-500 focus:border-indigo-500 sm:text-sm [&>span]:truncate">
|
<SelectTrigger className="mt-1 block w-full border-gray-300 rounded-md shadow-sm focus:ring-primary-500 focus:border-primary-500 sm:text-sm [&>span]:truncate">
|
||||||
<SelectValue placeholder="Select a Unit" />
|
<SelectValue placeholder="Select a Unit" />
|
||||||
</SelectTrigger>
|
</SelectTrigger>
|
||||||
<SelectContent>
|
<SelectContent>
|
||||||
|
|||||||
@@ -253,10 +253,10 @@ const PositionTypeMigrationModal: React.FC<PositionTypeMigrationModalProps> = ({
|
|||||||
|
|
||||||
{/* Display selected from/to */}
|
{/* Display selected from/to */}
|
||||||
{fromId && toId && (
|
{fromId && toId && (
|
||||||
<p className="mb-4 p-3 bg-blue-50 dark:bg-blue-950/30 rounded border border-blue-200 dark:border-blue-800">
|
<p className="mb-4 p-3 bg-primary-50 dark:bg-primary-950/30 rounded border border-primary-200 dark:border-primary-800">
|
||||||
<span className="text-sm text-gray-600 dark:text-gray-300">{t("migration.migrationPath")}:</span>
|
<span className="text-sm text-gray-600 dark:text-gray-300">{t("migration.migrationPath")}:</span>
|
||||||
<br />
|
<br />
|
||||||
<strong className="text-blue-700 dark:text-blue-400">{fromName}</strong>
|
<strong className="text-primary-700 dark:text-primary-400">{fromName}</strong>
|
||||||
<span className="mx-2">→</span>
|
<span className="mx-2">→</span>
|
||||||
<strong className="text-primary-700 dark:text-primary-400">{toName}</strong>
|
<strong className="text-primary-700 dark:text-primary-400">{toName}</strong>
|
||||||
</p>
|
</p>
|
||||||
@@ -380,7 +380,7 @@ const PositionTypeMigrationModal: React.FC<PositionTypeMigrationModalProps> = ({
|
|||||||
<AlertDialog>
|
<AlertDialog>
|
||||||
<AlertDialogTrigger asChild>
|
<AlertDialogTrigger asChild>
|
||||||
<button
|
<button
|
||||||
className="w-full mt-3 bg-blue-600 text-white px-4 py-2 rounded hover:bg-blue-700 transition disabled:opacity-50 disabled:cursor-not-allowed"
|
className="w-full mt-3 bg-primary-600 text-white px-4 py-2 rounded hover:bg-primary-700 transition disabled:opacity-50 disabled:cursor-not-allowed"
|
||||||
disabled={
|
disabled={
|
||||||
migratePositionsByPositions.isPending ||
|
migratePositionsByPositions.isPending ||
|
||||||
selectedPositions.length === 0 ||
|
selectedPositions.length === 0 ||
|
||||||
@@ -404,7 +404,7 @@ const PositionTypeMigrationModal: React.FC<PositionTypeMigrationModalProps> = ({
|
|||||||
<AlertDialogCancel className="bg-white dark:bg-gray-700 dark:text-gray-200 dark:border-gray-600 dark:hover:bg-gray-600">{t("common.Cancel")}</AlertDialogCancel>
|
<AlertDialogCancel className="bg-white dark:bg-gray-700 dark:text-gray-200 dark:border-gray-600 dark:hover:bg-gray-600">{t("common.Cancel")}</AlertDialogCancel>
|
||||||
<AlertDialogAction
|
<AlertDialogAction
|
||||||
onClick={handleMigrateSelected}
|
onClick={handleMigrateSelected}
|
||||||
className="bg-blue-600 hover:bg-blue-700 text-white"
|
className="bg-primary-600 hover:bg-primary-700 text-white"
|
||||||
>
|
>
|
||||||
{t("common.Confirm")}
|
{t("common.Confirm")}
|
||||||
</AlertDialogAction>
|
</AlertDialogAction>
|
||||||
|
|||||||
@@ -436,7 +436,7 @@ export const TeamMembers = ({
|
|||||||
return (
|
return (
|
||||||
<div className="flex justify-center py-10">
|
<div className="flex justify-center py-10">
|
||||||
<div className="flex flex-col items-center">
|
<div className="flex flex-col items-center">
|
||||||
<div className="w-8 h-8 border-4 border-t-blue-500 border-blue-200 rounded-full animate-spin mb-2"></div>
|
<div className="w-8 h-8 border-4 border-t-primary-500 border-primary-200 rounded-full animate-spin mb-2"></div>
|
||||||
<p className="text-sm text-gray-500 dark:text-gray-400">Loading team members...</p>
|
<p className="text-sm text-gray-500 dark:text-gray-400">Loading team members...</p>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -497,7 +497,7 @@ export const TeamMembers = ({
|
|||||||
{selectedEmployee && (
|
{selectedEmployee && (
|
||||||
<div className="flex flex-col items-center space-y-4 py-4">
|
<div className="flex flex-col items-center space-y-4 py-4">
|
||||||
<Avatar className="w-20 h-20 border-2 border-gray-200 dark:border-gray-600">
|
<Avatar className="w-20 h-20 border-2 border-gray-200 dark:border-gray-600">
|
||||||
<AvatarFallback className="text-xl bg-blue-100 dark:bg-blue-900 text-blue-700 dark:text-blue-300">
|
<AvatarFallback className="text-xl bg-primary-100 dark:bg-primary-900 text-primary-700 dark:text-primary-300">
|
||||||
{getInitials(
|
{getInitials(
|
||||||
localizedName(selectedEmployee.user.name),
|
localizedName(selectedEmployee.user.name),
|
||||||
).toUpperCase()}
|
).toUpperCase()}
|
||||||
@@ -532,7 +532,7 @@ export const TeamMembers = ({
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="flex items-center gap-3 p-4 rounded-lg bg-gray-50 dark:bg-gray-700 border border-gray-200 dark:border-gray-600">
|
<div className="flex items-center gap-3 p-4 rounded-lg bg-gray-50 dark:bg-gray-700 border border-gray-200 dark:border-gray-600">
|
||||||
<div className="p-2 rounded-lg bg-blue-100 dark:bg-blue-900 text-blue-700 dark:text-blue-400">
|
<div className="p-2 rounded-lg bg-primary-100 dark:bg-primary-900 text-primary-700 dark:text-primary-400">
|
||||||
<Mail className="w-5 h-5" />
|
<Mail className="w-5 h-5" />
|
||||||
</div>
|
</div>
|
||||||
<div>
|
<div>
|
||||||
@@ -545,7 +545,7 @@ export const TeamMembers = ({
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div className="flex items-center gap-3 p-4 rounded-lg bg-gray-50 dark:bg-gray-700 border border-gray-200 dark:border-gray-600">
|
<div className="flex items-center gap-3 p-4 rounded-lg bg-gray-50 dark:bg-gray-700 border border-gray-200 dark:border-gray-600">
|
||||||
<div className="p-2 rounded-lg bg-blue-100 dark:bg-blue-900 text-blue-700 dark:text-blue-400">
|
<div className="p-2 rounded-lg bg-primary-100 dark:bg-primary-900 text-primary-700 dark:text-primary-400">
|
||||||
<PhoneCall className="w-5 h-5" />
|
<PhoneCall className="w-5 h-5" />
|
||||||
</div>
|
</div>
|
||||||
<div>
|
<div>
|
||||||
@@ -574,7 +574,7 @@ export const TeamMembers = ({
|
|||||||
{selectedEmployee && (
|
{selectedEmployee && (
|
||||||
<div className="flex flex-col items-center space-y-4 py-4">
|
<div className="flex flex-col items-center space-y-4 py-4">
|
||||||
<Avatar className="w-20 h-20 border-2 border-gray-200 dark:border-gray-600">
|
<Avatar className="w-20 h-20 border-2 border-gray-200 dark:border-gray-600">
|
||||||
<AvatarFallback className="text-xl bg-blue-100 dark:bg-blue-900 text-blue-700 dark:text-blue-300">
|
<AvatarFallback className="text-xl bg-primary-100 dark:bg-primary-900 text-primary-700 dark:text-primary-300">
|
||||||
{getInitials(
|
{getInitials(
|
||||||
localizedName(selectedEmployee.user.name),
|
localizedName(selectedEmployee.user.name),
|
||||||
).toUpperCase()}
|
).toUpperCase()}
|
||||||
|
|||||||
@@ -493,7 +493,7 @@ const UserManagementTree = () => {
|
|||||||
size="sm"
|
size="sm"
|
||||||
onClick={() => setShowEmployeeSearch(true)}
|
onClick={() => setShowEmployeeSearch(true)}
|
||||||
disabled={!organizationId}
|
disabled={!organizationId}
|
||||||
className="bg-blue-50 text-blue-700 border-blue-200 hover:bg-blue-100 whitespace-nowrap dark:bg-blue-900/30 dark:text-blue-400 dark:border-blue-800 dark:hover:bg-blue-900/50"
|
className="bg-primary-50 text-primary-700 border-primary-200 hover:bg-primary-100 whitespace-nowrap dark:bg-primary-900/30 dark:text-primary-400 dark:border-primary-800 dark:hover:bg-primary-900/50"
|
||||||
>
|
>
|
||||||
<UserCircle className="h-4 w-4 mr-2" />
|
<UserCircle className="h-4 w-4 mr-2" />
|
||||||
{t("search.searchAllEmployees")}
|
{t("search.searchAllEmployees")}
|
||||||
@@ -526,7 +526,7 @@ const UserManagementTree = () => {
|
|||||||
size="sm"
|
size="sm"
|
||||||
onClick={() => setShowResendModal(true)}
|
onClick={() => setShowResendModal(true)}
|
||||||
disabled={isSendingInvitations}
|
disabled={isSendingInvitations}
|
||||||
className="border border-blue-200 bg-blue-50 text-blue-700 hover:border-blue-300 hover:bg-blue-100 dark:border-blue-900 dark:bg-blue-950 dark:text-blue-200 dark:hover:border-blue-700 dark:hover:bg-blue-900 dark:hover:text-white"
|
className="border border-primary-200 bg-primary-50 text-primary-700 hover:border-primary-300 hover:bg-primary-100 dark:border-primary-900 dark:bg-primary-950 dark:text-primary-200 dark:hover:border-primary-700 dark:hover:bg-primary-900 dark:hover:text-white"
|
||||||
>
|
>
|
||||||
<Mail className="h-4 w-4 mr-2" />
|
<Mail className="h-4 w-4 mr-2" />
|
||||||
{t("userManagement.resendInvitation", "Resend Invitation")}
|
{t("userManagement.resendInvitation", "Resend Invitation")}
|
||||||
|
|||||||
@@ -89,7 +89,7 @@ export function ViewUsers({ unitId, onClose }: ViewUsersProps) {
|
|||||||
case "admin":
|
case "admin":
|
||||||
return "bg-red-100 text-red-800 dark:bg-red-900/40 dark:text-red-300";
|
return "bg-red-100 text-red-800 dark:bg-red-900/40 dark:text-red-300";
|
||||||
case "manager":
|
case "manager":
|
||||||
return "bg-blue-100 text-blue-800 dark:bg-blue-900/40 dark:text-blue-300";
|
return "bg-primary-100 text-primary-800 dark:bg-primary-900/40 dark:text-primary-300";
|
||||||
default:
|
default:
|
||||||
return "bg-primary-100 text-primary-800 dark:bg-primary-900/40 dark:text-primary-300";
|
return "bg-primary-100 text-primary-800 dark:bg-primary-900/40 dark:text-primary-300";
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -149,7 +149,7 @@ export const OrganizationEmployeeSearch: React.FC<
|
|||||||
return (
|
return (
|
||||||
<div className="flex justify-center py-12">
|
<div className="flex justify-center py-12">
|
||||||
<div className="flex flex-col items-center">
|
<div className="flex flex-col items-center">
|
||||||
<div className="w-8 h-8 border-4 border-t-blue-500 border-blue-200 rounded-full animate-spin mb-2"></div>
|
<div className="w-8 h-8 border-4 border-t-primary-500 border-primary-200 rounded-full animate-spin mb-2"></div>
|
||||||
<p className="text-sm text-gray-500 dark:text-gray-400">
|
<p className="text-sm text-gray-500 dark:text-gray-400">
|
||||||
{t("search.searchingEmployees")}
|
{t("search.searchingEmployees")}
|
||||||
</p>
|
</p>
|
||||||
|
|||||||
@@ -66,7 +66,7 @@ export const UnitSelectionModal: React.FC<UnitSelectionModalProps> = ({
|
|||||||
{isLoading ? (
|
{isLoading ? (
|
||||||
<div className="text-center py-12">
|
<div className="text-center py-12">
|
||||||
<div className="flex flex-col items-center">
|
<div className="flex flex-col items-center">
|
||||||
<div className="animate-spin rounded-full h-8 w-8 border-b-2 border-blue-600 mx-auto"></div>
|
<div className="animate-spin rounded-full h-8 w-8 border-b-2 border-primary-600 mx-auto"></div>
|
||||||
<p className="mt-2 dark:text-gray-400">{t("loadingUnits")}</p>
|
<p className="mt-2 dark:text-gray-400">{t("loadingUnits")}</p>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -79,7 +79,7 @@ export const UnitSelectionModal: React.FC<UnitSelectionModalProps> = ({
|
|||||||
<button
|
<button
|
||||||
key={unit.id}
|
key={unit.id}
|
||||||
onClick={() => onSelectUnit(unit.id)}
|
onClick={() => onSelectUnit(unit.id)}
|
||||||
className="w-full text-left p-4 rounded border border-gray-200 hover:bg-gray-100 hover:shadow-sm transition-all focus:outline-none focus:ring-2 focus:ring-blue-500 cursor-pointer dark:border-gray-600 dark:hover:bg-gray-700 dark:focus:ring-blue-400"
|
className="w-full text-left p-4 rounded border border-gray-200 hover:bg-gray-100 hover:shadow-sm transition-all focus:outline-none focus:ring-2 focus:ring-primary-500 cursor-pointer dark:border-gray-600 dark:hover:bg-gray-700 dark:focus:ring-primary-400"
|
||||||
>
|
>
|
||||||
<div className="font-medium dark:text-gray-200 truncate">
|
<div className="font-medium dark:text-gray-200 truncate">
|
||||||
{unit.name?.en || unit.name}
|
{unit.name?.en || unit.name}
|
||||||
|
|||||||
@@ -107,7 +107,7 @@ export const EmployeePositionsDialog: React.FC<
|
|||||||
<DialogContent className="sm:max-w-lg dark:bg-gray-800">
|
<DialogContent className="sm:max-w-lg dark:bg-gray-800">
|
||||||
<DialogHeader>
|
<DialogHeader>
|
||||||
<DialogTitle className="flex items-center gap-2 dark:text-white">
|
<DialogTitle className="flex items-center gap-2 dark:text-white">
|
||||||
<Briefcase className="h-5 w-5 text-blue-600 dark:text-blue-400" />
|
<Briefcase className="h-5 w-5 text-primary-600 dark:text-primary-400" />
|
||||||
{t("positions.employeePositions")}
|
{t("positions.employeePositions")}
|
||||||
</DialogTitle>
|
</DialogTitle>
|
||||||
</DialogHeader>
|
</DialogHeader>
|
||||||
@@ -133,7 +133,7 @@ export const EmployeePositionsDialog: React.FC<
|
|||||||
key={position.id}
|
key={position.id}
|
||||||
className="flex items-center justify-between p-4 rounded-lg border border-gray-200 dark:border-gray-600 bg-white dark:bg-gray-700/50 hover:border-gray-300 dark:hover:border-gray-500 transition-colors">
|
className="flex items-center justify-between p-4 rounded-lg border border-gray-200 dark:border-gray-600 bg-white dark:bg-gray-700/50 hover:border-gray-300 dark:hover:border-gray-500 transition-colors">
|
||||||
<div className="flex items-center gap-3 flex-1">
|
<div className="flex items-center gap-3 flex-1">
|
||||||
<div className="p-2 rounded-lg bg-blue-100 dark:bg-blue-900/40 text-blue-600 dark:text-blue-400">
|
<div className="p-2 rounded-lg bg-primary-100 dark:bg-primary-900/40 text-primary-600 dark:text-primary-400">
|
||||||
<Briefcase className="h-4 w-4" />
|
<Briefcase className="h-4 w-4" />
|
||||||
</div>
|
</div>
|
||||||
<div className="min-w-0 flex-1">
|
<div className="min-w-0 flex-1">
|
||||||
|
|||||||
@@ -182,7 +182,7 @@ export const DepartmentList = ({
|
|||||||
key={pos.id}
|
key={pos.id}
|
||||||
className={cn(
|
className={cn(
|
||||||
"flex items-center gap-2 text-sm py-1 pl-8 border-l-2 border-primary-100 dark:border-primary-800 ml-4 text-gray-800 dark:text-gray-200",
|
"flex items-center gap-2 text-sm py-1 pl-8 border-l-2 border-primary-100 dark:border-primary-800 ml-4 text-gray-800 dark:text-gray-200",
|
||||||
expandedState[pos.id] ? "bg-blue-50 dark:bg-blue-900/20" : "hover:bg-blue-50 dark:hover:bg-blue-900/20"
|
expandedState[pos.id] ? "bg-primary-50 dark:bg-primary-900/20" : "hover:bg-primary-50 dark:hover:bg-primary-900/20"
|
||||||
)}
|
)}
|
||||||
onClick={() => toggleExpand(pos.id)}
|
onClick={() => toggleExpand(pos.id)}
|
||||||
>
|
>
|
||||||
@@ -200,7 +200,7 @@ export const DepartmentList = ({
|
|||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
<FolderPlus
|
<FolderPlus
|
||||||
className="w-4 h-4 text-gray-500 dark:text-gray-400 hover:text-blue-600 dark:hover:text-blue-400"
|
className="w-4 h-4 text-gray-500 dark:text-gray-400 hover:text-primary-600 dark:hover:text-primary-400"
|
||||||
onClick={(e) => {
|
onClick={(e) => {
|
||||||
e.stopPropagation();
|
e.stopPropagation();
|
||||||
onAddPosition?.(pos.id, "sub");
|
onAddPosition?.(pos.id, "sub");
|
||||||
|
|||||||
@@ -914,7 +914,7 @@ const Branding = () => {
|
|||||||
label={t("branding.socialMedia.facebook")}
|
label={t("branding.socialMedia.facebook")}
|
||||||
placeholder={t("branding.socialMedia.facebookPlaceholder")}
|
placeholder={t("branding.socialMedia.facebookPlaceholder")}
|
||||||
register={register}
|
register={register}
|
||||||
icon={<FaFacebook className="h-4 w-4 text-blue-600" />}
|
icon={<FaFacebook className="h-4 w-4 text-primary-600" />}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<SocialInput
|
<SocialInput
|
||||||
@@ -922,7 +922,7 @@ const Branding = () => {
|
|||||||
label={t("branding.socialMedia.twitter")}
|
label={t("branding.socialMedia.twitter")}
|
||||||
placeholder={t("branding.socialMedia.twitterPlaceholder")}
|
placeholder={t("branding.socialMedia.twitterPlaceholder")}
|
||||||
register={register}
|
register={register}
|
||||||
icon={<FaTwitter className="h-4 w-4 text-sky-500" />}
|
icon={<FaTwitter className="h-4 w-4 text-primary-500" />}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<SocialInput
|
<SocialInput
|
||||||
@@ -930,7 +930,7 @@ const Branding = () => {
|
|||||||
label={t("branding.socialMedia.linkedin")}
|
label={t("branding.socialMedia.linkedin")}
|
||||||
placeholder={t("branding.socialMedia.linkedinPlaceholder")}
|
placeholder={t("branding.socialMedia.linkedinPlaceholder")}
|
||||||
register={register}
|
register={register}
|
||||||
icon={<FaLinkedin className="h-4 w-4 text-blue-700" />}
|
icon={<FaLinkedin className="h-4 w-4 text-primary-700" />}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<SocialInput
|
<SocialInput
|
||||||
@@ -954,7 +954,7 @@ const Branding = () => {
|
|||||||
label={t("branding.socialMedia.telegram")}
|
label={t("branding.socialMedia.telegram")}
|
||||||
placeholder={t("branding.socialMedia.telegramPlaceholder")}
|
placeholder={t("branding.socialMedia.telegramPlaceholder")}
|
||||||
register={register}
|
register={register}
|
||||||
icon={<Send className="h-4 w-4 text-sky-500" />}
|
icon={<Send className="h-4 w-4 text-primary-500" />}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<SocialInput
|
<SocialInput
|
||||||
|
|||||||
@@ -20,7 +20,7 @@ export const ViewGoal = () => {
|
|||||||
<div className="md:w-2/3 p-6 md:p-8 flex flex-col justify-center">
|
<div className="md:w-2/3 p-6 md:p-8 flex flex-col justify-center">
|
||||||
<div className="mb-6">
|
<div className="mb-6">
|
||||||
<div className="flex items-center mb-6">
|
<div className="flex items-center mb-6">
|
||||||
<div className="w-12 h-12 rounded-lg bg-gradient-to-r from-indigo-500 to-blue-600 flex items-center justify-center text-white text-2xl mr-4">
|
<div className="w-12 h-12 rounded-lg bg-gradient-to-r from-primary-500 to-primary-600 flex items-center justify-center text-white text-2xl mr-4">
|
||||||
<ImageIcon className="w-6 h-6" />
|
<ImageIcon className="w-6 h-6" />
|
||||||
</div>
|
</div>
|
||||||
<div>
|
<div>
|
||||||
@@ -75,7 +75,7 @@ export const ViewGoal = () => {
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Visual Section */}
|
{/* Visual Section */}
|
||||||
<div className="md:w-1/3 bg-gradient-to-br from-blue-50 to-indigo-100 p-6 md:p-8 flex flex-col justify-center items-center">
|
<div className="md:w-1/3 bg-gradient-to-br from-primary-50 to-primary-100 p-6 md:p-8 flex flex-col justify-center items-center">
|
||||||
<div className="text-center">
|
<div className="text-center">
|
||||||
<div className="w-32 h-32 mx-auto mb-6 bg-white rounded-2xl shadow-lg flex items-center justify-center overflow-hidden">
|
<div className="w-32 h-32 mx-auto mb-6 bg-white rounded-2xl shadow-lg flex items-center justify-center overflow-hidden">
|
||||||
{item?.presigned ? (
|
{item?.presigned ? (
|
||||||
|
|||||||
@@ -13,14 +13,14 @@ export const ViewMessage = () => {
|
|||||||
const locale = i18n.language;
|
const locale = i18n.language;
|
||||||
const localizedName = useLocalizedName();
|
const localizedName = useLocalizedName();
|
||||||
return (
|
return (
|
||||||
<div className="min-h-screen bg-gradient-to-br from-blue-50 to-indigo-100 flex items-center justify-center p-4 md:p-8">
|
<div className="min-h-screen bg-gradient-to-br from-primary-50 to-primary-100 flex items-center justify-center p-4 md:p-8">
|
||||||
<div className="max-w-6xl w-full">
|
<div className="max-w-6xl w-full">
|
||||||
{/* Header */}
|
{/* Header */}
|
||||||
<div className="text-center mb-12">
|
<div className="text-center mb-12">
|
||||||
<h1 className="text-4xl md:text-5xl font-bold text-gray-800 mb-4">
|
<h1 className="text-4xl md:text-5xl font-bold text-gray-800 mb-4">
|
||||||
{t("Leadership Message")}
|
{t("Leadership Message")}
|
||||||
</h1>
|
</h1>
|
||||||
<div className="w-24 h-1 bg-indigo-600 mx-auto"></div>
|
<div className="w-24 h-1 bg-primary-600 mx-auto"></div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Card */}
|
{/* Card */}
|
||||||
@@ -54,7 +54,7 @@ export const ViewMessage = () => {
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Checkmark */}
|
{/* Checkmark */}
|
||||||
<div className="absolute top-6 right-6 bg-white text-indigo-600 rounded-full p-2 shadow-lg">
|
<div className="absolute top-6 right-6 bg-white text-primary-600 rounded-full p-2 shadow-lg">
|
||||||
<svg
|
<svg
|
||||||
xmlns="http://www.w3.org/2000/svg"
|
xmlns="http://www.w3.org/2000/svg"
|
||||||
className="h-6 w-6"
|
className="h-6 w-6"
|
||||||
@@ -76,8 +76,8 @@ export const ViewMessage = () => {
|
|||||||
<div className="md:w-3/5 p-6 md:p-8 flex flex-col justify-center">
|
<div className="md:w-3/5 p-6 md:p-8 flex flex-col justify-center">
|
||||||
<div className="mb-6">
|
<div className="mb-6">
|
||||||
<div className="flex items-center mb-6">
|
<div className="flex items-center mb-6">
|
||||||
<div className="w-10 h-1 bg-indigo-600 mr-3"></div>
|
<div className="w-10 h-1 bg-primary-600 mr-3"></div>
|
||||||
<h3 className="text-lg font-semibold text-indigo-600 uppercase tracking-wide">
|
<h3 className="text-lg font-semibold text-primary-600 uppercase tracking-wide">
|
||||||
{t("Message Content")}
|
{t("Message Content")}
|
||||||
</h3>
|
</h3>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -121,7 +121,7 @@ export const AddNews = () => {
|
|||||||
{/* Category Selection */}
|
{/* Category Selection */}
|
||||||
<div className="bg-gray-50 dark:bg-gray-700 p-5 rounded-lg border border-gray-100 dark:border-gray-600">
|
<div className="bg-gray-50 dark:bg-gray-700 p-5 rounded-lg border border-gray-100 dark:border-gray-600">
|
||||||
<h3 className="text-lg font-semibold text-gray-800 dark:text-gray-200 mb-4 flex items-center">
|
<h3 className="text-lg font-semibold text-gray-800 dark:text-gray-200 mb-4 flex items-center">
|
||||||
<div className="h-1 w-4 bg-blue-500 mr-2 rounded-full"></div>
|
<div className="h-1 w-4 bg-primary-500 mr-2 rounded-full"></div>
|
||||||
Category
|
Category
|
||||||
</h3>
|
</h3>
|
||||||
<div className="space-y-2">
|
<div className="space-y-2">
|
||||||
@@ -153,7 +153,7 @@ export const AddNews = () => {
|
|||||||
{/* Title Section */}
|
{/* Title Section */}
|
||||||
<div className="bg-gray-50 dark:bg-gray-700 p-5 rounded-lg border border-gray-100 dark:border-gray-600">
|
<div className="bg-gray-50 dark:bg-gray-700 p-5 rounded-lg border border-gray-100 dark:border-gray-600">
|
||||||
<h3 className="text-lg font-semibold text-gray-800 dark:text-gray-200 mb-4 flex items-center">
|
<h3 className="text-lg font-semibold text-gray-800 dark:text-gray-200 mb-4 flex items-center">
|
||||||
<div className="h-1 w-4 bg-blue-500 mr-2 rounded-full"></div>
|
<div className="h-1 w-4 bg-primary-500 mr-2 rounded-full"></div>
|
||||||
Title
|
Title
|
||||||
</h3>
|
</h3>
|
||||||
<div className="grid grid-cols-1 md:grid-cols-2 gap-6">
|
<div className="grid grid-cols-1 md:grid-cols-2 gap-6">
|
||||||
|
|||||||
@@ -102,9 +102,9 @@ export const EditNews = () => {
|
|||||||
</Label>
|
</Label>
|
||||||
|
|
||||||
{preview && (
|
{preview && (
|
||||||
<div className="mb-3 flex items-center justify-between bg-blue-50 p-3 rounded-lg">
|
<div className="mb-3 flex items-center justify-between bg-primary-50 p-3 rounded-lg">
|
||||||
<div className="flex items-center">
|
<div className="flex items-center">
|
||||||
<ImageIcon className="h-5 w-5 text-blue-500 mr-2" />
|
<ImageIcon className="h-5 w-5 text-primary-500 mr-2" />
|
||||||
<span className="text-sm font-medium truncate max-w-[200px]">
|
<span className="text-sm font-medium truncate max-w-[200px]">
|
||||||
{existingName || "Selected Image"}
|
{existingName || "Selected Image"}
|
||||||
</span>
|
</span>
|
||||||
@@ -114,7 +114,7 @@ export const EditNews = () => {
|
|||||||
variant="ghost"
|
variant="ghost"
|
||||||
size="sm"
|
size="sm"
|
||||||
onClick={onView}
|
onClick={onView}
|
||||||
className="text-blue-600 hover:text-blue-800"
|
className="text-primary-600 hover:text-primary-800"
|
||||||
>
|
>
|
||||||
Preview
|
Preview
|
||||||
</Button>
|
</Button>
|
||||||
@@ -186,7 +186,7 @@ export const EditNews = () => {
|
|||||||
{/* Category Selection */}
|
{/* Category Selection */}
|
||||||
<div className="bg-gray-50 dark:bg-gray-700 p-5 rounded-lg border border-gray-100 dark:border-gray-600">
|
<div className="bg-gray-50 dark:bg-gray-700 p-5 rounded-lg border border-gray-100 dark:border-gray-600">
|
||||||
<h3 className="text-lg font-semibold text-gray-800 dark:text-gray-200 mb-4 flex items-center">
|
<h3 className="text-lg font-semibold text-gray-800 dark:text-gray-200 mb-4 flex items-center">
|
||||||
<div className="h-1 w-4 bg-blue-500 mr-2 rounded-full"></div>
|
<div className="h-1 w-4 bg-primary-500 mr-2 rounded-full"></div>
|
||||||
Category
|
Category
|
||||||
</h3>
|
</h3>
|
||||||
<div className="space-y-2">
|
<div className="space-y-2">
|
||||||
@@ -218,7 +218,7 @@ export const EditNews = () => {
|
|||||||
{/* Title Section */}
|
{/* Title Section */}
|
||||||
<div className="bg-gray-50 dark:bg-gray-700 p-5 rounded-lg border border-gray-100 dark:border-gray-600">
|
<div className="bg-gray-50 dark:bg-gray-700 p-5 rounded-lg border border-gray-100 dark:border-gray-600">
|
||||||
<h3 className="text-lg font-semibold text-gray-800 dark:text-gray-100 mb-4 flex items-center">
|
<h3 className="text-lg font-semibold text-gray-800 dark:text-gray-100 mb-4 flex items-center">
|
||||||
<div className="h-1 w-4 bg-blue-500 mr-2 rounded-full"></div>
|
<div className="h-1 w-4 bg-primary-500 mr-2 rounded-full"></div>
|
||||||
Title
|
Title
|
||||||
</h3>
|
</h3>
|
||||||
<div className="grid grid-cols-1 md:grid-cols-2 gap-6">
|
<div className="grid grid-cols-1 md:grid-cols-2 gap-6">
|
||||||
|
|||||||
@@ -35,7 +35,7 @@ export const NewsColumnDefn = ({
|
|||||||
? rowData.newsCategory?.newsCategoryTitle?.am
|
? rowData.newsCategory?.newsCategoryTitle?.am
|
||||||
: rowData.newsCategory?.newsCategoryTitle?.en;
|
: rowData.newsCategory?.newsCategoryTitle?.en;
|
||||||
return (
|
return (
|
||||||
<span className="text-sm text-blue-600 bg-blue-50 px-2 py-1 rounded-full">
|
<span className="text-sm text-primary-600 bg-primary-50 px-2 py-1 rounded-full">
|
||||||
{categoryTitle || "No Category"}
|
{categoryTitle || "No Category"}
|
||||||
</span>
|
</span>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -346,7 +346,7 @@ export const TemplateForm = () => {
|
|||||||
{/* Names Section */}
|
{/* Names Section */}
|
||||||
<div className="bg-gray-50 p-5 rounded-lg border border-gray-100 space-y-4">
|
<div className="bg-gray-50 p-5 rounded-lg border border-gray-100 space-y-4">
|
||||||
<h3 className="text-lg font-semibold text-gray-800 flex items-center">
|
<h3 className="text-lg font-semibold text-gray-800 flex items-center">
|
||||||
<div className="h-1 w-4 bg-blue-500 mr-2 rounded-full"></div>
|
<div className="h-1 w-4 bg-primary-500 mr-2 rounded-full"></div>
|
||||||
{t("template.names", "Names")}
|
{t("template.names", "Names")}
|
||||||
</h3>
|
</h3>
|
||||||
|
|
||||||
|
|||||||
@@ -231,7 +231,7 @@ export const TemplateList = () => {
|
|||||||
</TableCell>
|
</TableCell>
|
||||||
<TableCell className="text-gray-600">
|
<TableCell className="text-gray-600">
|
||||||
{template.fileInfo?.originalname ? (
|
{template.fileInfo?.originalname ? (
|
||||||
<span className="text-blue-600 hover:text-blue-700 cursor-pointer">
|
<span className="text-primary-600 hover:text-primary-700 cursor-pointer">
|
||||||
{template.fileInfo.originalname}
|
{template.fileInfo.originalname}
|
||||||
</span>
|
</span>
|
||||||
) : (
|
) : (
|
||||||
|
|||||||
@@ -97,7 +97,7 @@
|
|||||||
// {/* Title Section */}
|
// {/* Title Section */}
|
||||||
// <div className="bg-gray-50 p-5 rounded-lg border border-gray-100">
|
// <div className="bg-gray-50 p-5 rounded-lg border border-gray-100">
|
||||||
// <h3 className="text-lg font-semibold text-gray-800 mb-4 flex items-center">
|
// <h3 className="text-lg font-semibold text-gray-800 mb-4 flex items-center">
|
||||||
// <div className="h-1 w-4 bg-blue-500 mr-2 rounded-full"></div>
|
// <div className="h-1 w-4 bg-primary-500 mr-2 rounded-full"></div>
|
||||||
// Color
|
// Color
|
||||||
// </h3>
|
// </h3>
|
||||||
// <div className="grid grid-cols-1 md:grid-cols-2 gap-6">
|
// <div className="grid grid-cols-1 md:grid-cols-2 gap-6">
|
||||||
|
|||||||
@@ -128,7 +128,7 @@ export const EditTheme = () => {
|
|||||||
{/* Title Section */}
|
{/* Title Section */}
|
||||||
<div className="bg-gray-50 dark:bg-gray-700 p-5 rounded-lg border border-gray-100 dark:border-gray-600">
|
<div className="bg-gray-50 dark:bg-gray-700 p-5 rounded-lg border border-gray-100 dark:border-gray-600">
|
||||||
<h3 className="text-lg font-semibold text-gray-800 dark:text-gray-200 mb-4 flex items-center">
|
<h3 className="text-lg font-semibold text-gray-800 dark:text-gray-200 mb-4 flex items-center">
|
||||||
<div className="h-1 w-4 bg-blue-500 mr-2 rounded-full"></div>
|
<div className="h-1 w-4 bg-primary-500 mr-2 rounded-full"></div>
|
||||||
Color
|
Color
|
||||||
</h3>
|
</h3>
|
||||||
<div className="grid grid-cols-1 md:grid-cols-2 gap-6">
|
<div className="grid grid-cols-1 md:grid-cols-2 gap-6">
|
||||||
|
|||||||
@@ -11,10 +11,10 @@ export const ViewVision = () => {
|
|||||||
const locale = i18n.language;
|
const locale = i18n.language;
|
||||||
const localizedName = useLocalizedName();
|
const localizedName = useLocalizedName();
|
||||||
return (
|
return (
|
||||||
<div className="min-h-screen bg-gradient-to-br from-blue-50 to-indigo-100 flex items-center justify-center p-4 md:p-8">
|
<div className="min-h-screen bg-gradient-to-br from-primary-50 to-primary-100 flex items-center justify-center p-4 md:p-8">
|
||||||
<div className="max-w-6xl w-full">
|
<div className="max-w-6xl w-full">
|
||||||
{/* Header */}
|
{/* Header */}
|
||||||
<div className="md:w-1/3 bg-gradient-to-br from-blue-50 to-indigo-100 p-6 md:p-8 flex flex-col justify-center items-center">
|
<div className="md:w-1/3 bg-gradient-to-br from-primary-50 to-primary-100 p-6 md:p-8 flex flex-col justify-center items-center">
|
||||||
<div className="text-center">
|
<div className="text-center">
|
||||||
<div className="w-32 h-32 mx-auto mb-6 bg-white rounded-2xl shadow-lg flex items-center justify-center">
|
<div className="w-32 h-32 mx-auto mb-6 bg-white rounded-2xl shadow-lg flex items-center justify-center">
|
||||||
<span className="text-5xl">{"image from goal"}</span>
|
<span className="text-5xl">{"image from goal"}</span>
|
||||||
|
|||||||
@@ -40,7 +40,7 @@ export function FormInputBox({
|
|||||||
return (
|
return (
|
||||||
<div className="bg-gray-50 dark:bg-gray-700 p-5 rounded-lg border border-gray-100 dark:border-gray-600">
|
<div className="bg-gray-50 dark:bg-gray-700 p-5 rounded-lg border border-gray-100 dark:border-gray-600">
|
||||||
<h3 className="text-lg font-semibold text-gray-800 dark:text-gray-200 mb-4 flex items-center">
|
<h3 className="text-lg font-semibold text-gray-800 dark:text-gray-200 mb-4 flex items-center">
|
||||||
<div className="h-1 w-4 bg-blue-500 mr-2 rounded-full"></div>
|
<div className="h-1 w-4 bg-primary-500 mr-2 rounded-full"></div>
|
||||||
{label}
|
{label}
|
||||||
</h3>
|
</h3>
|
||||||
<div className="grid grid-cols-1 md:grid-cols-2 gap-6">
|
<div className="grid grid-cols-1 md:grid-cols-2 gap-6">
|
||||||
@@ -190,7 +190,7 @@ export const Section = ({
|
|||||||
}) => (
|
}) => (
|
||||||
<div className="bg-gray-50 dark:bg-gray-700 p-5 rounded-lg border border-gray-100 dark:border-gray-600">
|
<div className="bg-gray-50 dark:bg-gray-700 p-5 rounded-lg border border-gray-100 dark:border-gray-600">
|
||||||
<h3 className="text-lg font-semibold text-gray-800 dark:text-gray-200 mb-4 flex items-center">
|
<h3 className="text-lg font-semibold text-gray-800 dark:text-gray-200 mb-4 flex items-center">
|
||||||
<div className="h-1 w-4 bg-blue-500 mr-2 rounded-full"></div>
|
<div className="h-1 w-4 bg-primary-500 mr-2 rounded-full"></div>
|
||||||
{title}
|
{title}
|
||||||
</h3>
|
</h3>
|
||||||
{children}
|
{children}
|
||||||
@@ -230,9 +230,9 @@ export const ImageUpload = ({
|
|||||||
</Label>
|
</Label>
|
||||||
|
|
||||||
{preview && (
|
{preview && (
|
||||||
<div className="mb-3 flex items-center justify-between bg-blue-50 dark:bg-blue-900/30 p-3 rounded-lg">
|
<div className="mb-3 flex items-center justify-between bg-primary-50 dark:bg-primary-900/30 p-3 rounded-lg">
|
||||||
<div className="flex items-center">
|
<div className="flex items-center">
|
||||||
<ImageIcon className="h-5 w-5 text-blue-500 dark:text-blue-400 mr-2" />
|
<ImageIcon className="h-5 w-5 text-primary-500 dark:text-primary-400 mr-2" />
|
||||||
<span className="text-sm font-medium truncate max-w-[200px] dark:text-gray-200">
|
<span className="text-sm font-medium truncate max-w-[200px] dark:text-gray-200">
|
||||||
{existingName || "Selected Image"}
|
{existingName || "Selected Image"}
|
||||||
</span>
|
</span>
|
||||||
@@ -242,7 +242,7 @@ export const ImageUpload = ({
|
|||||||
variant="ghost"
|
variant="ghost"
|
||||||
size="sm"
|
size="sm"
|
||||||
onClick={onView}
|
onClick={onView}
|
||||||
className="text-blue-600 dark:text-blue-400 hover:text-blue-800 dark:hover:text-blue-300"
|
className="text-primary-600 dark:text-primary-400 hover:text-primary-800 dark:hover:text-primary-300"
|
||||||
>
|
>
|
||||||
Preview
|
Preview
|
||||||
</Button>
|
</Button>
|
||||||
|
|||||||
@@ -1,6 +1,11 @@
|
|||||||
@import "tailwindcss";
|
@import "tailwindcss";
|
||||||
@import "@edr/ui-common/theme.css" layer(theme);
|
@import "@edr/ui-common/theme.css" layer(theme);
|
||||||
|
|
||||||
|
/* Dark mode is toggled via the `dark` class on <html>. Without this, Tailwind
|
||||||
|
v4 compiles `dark:` utilities to `@media (prefers-color-scheme: dark)` and
|
||||||
|
they follow the OS setting instead of the in-app toggle. */
|
||||||
|
@custom-variant dark (&:where(.dark, .dark *));
|
||||||
|
|
||||||
/* Bridge the central Mantine theme into Tailwind. Mantine (createTheme) is the
|
/* Bridge the central Mantine theme into Tailwind. Mantine (createTheme) is the
|
||||||
single source of truth; these just alias its generated CSS variables so
|
single source of truth; these just alias its generated CSS variables so
|
||||||
`bg-edr-*`, `text-edr-*`, `border-edr-*` utilities resolve to the same tokens. */
|
`bg-edr-*`, `text-edr-*`, `border-edr-*` utilities resolve to the same tokens. */
|
||||||
|
|||||||
@@ -28,9 +28,8 @@ MINIO_ACCESS_KEY=minioadmin
|
|||||||
MINIO_SECRET_KEY=minioadmin
|
MINIO_SECRET_KEY=minioadmin
|
||||||
MINIO_BUCKET=edr-dev
|
MINIO_BUCKET=edr-dev
|
||||||
|
|
||||||
# CORS
|
# CORS — comma-separated list of allowed origins (add more, comma-separated)
|
||||||
FRONTEND_URL=http://localhost:5174
|
CORS_ORIGINS=http://localhost:5174,http://localhost:5184
|
||||||
BACK_OFFICE_URL=http://localhost:5184
|
|
||||||
|
|
||||||
# JWT (legacy passenger auth — being replaced by IAM)
|
# JWT (legacy passenger auth — being replaced by IAM)
|
||||||
# REQUIRED in production — use a random 32+ character string (e.g. openssl rand -hex 32)
|
# REQUIRED in production — use a random 32+ character string (e.g. openssl rand -hex 32)
|
||||||
|
|||||||
@@ -1,7 +1,5 @@
|
|||||||
import { Logger, Module, OnApplicationBootstrap } from "@nestjs/common";
|
import { Logger, Module, OnApplicationBootstrap } from "@nestjs/common";
|
||||||
import { ThrottlerModule } from "@nestjs/throttler";
|
import { APP_FILTER } from "@nestjs/core";
|
||||||
import { DynamicThrottlerGuard } from "./common/dynamic-throttler.guard";
|
|
||||||
import { APP_GUARD, APP_FILTER } from "@nestjs/core";
|
|
||||||
import { ConfigModule, ConfigService } from "@nestjs/config";
|
import { ConfigModule, ConfigService } from "@nestjs/config";
|
||||||
import { ScheduleModule } from "@nestjs/schedule";
|
import { ScheduleModule } from "@nestjs/schedule";
|
||||||
import { EventEmitterModule } from "@nestjs/event-emitter";
|
import { EventEmitterModule } from "@nestjs/event-emitter";
|
||||||
@@ -69,11 +67,6 @@ import { EOtpType } from "@tria-plc/iamapi-common";
|
|||||||
|
|
||||||
@Module({
|
@Module({
|
||||||
imports: [
|
imports: [
|
||||||
ThrottlerModule.forRoot([
|
|
||||||
{ name: "auth", ttl: 60, limit: 100000 },
|
|
||||||
{ name: "strict", ttl: 60, limit: 100000 },
|
|
||||||
{ name: "default", ttl: 60, limit: 100000 },
|
|
||||||
]),
|
|
||||||
ConfigModule.forRoot({
|
ConfigModule.forRoot({
|
||||||
isGlobal: true,
|
isGlobal: true,
|
||||||
load: [
|
load: [
|
||||||
@@ -149,9 +142,7 @@ import { EOtpType } from "@tria-plc/iamapi-common";
|
|||||||
ConfigurableFareModule,
|
ConfigurableFareModule,
|
||||||
],
|
],
|
||||||
providers: [
|
providers: [
|
||||||
{ provide: APP_GUARD, useClass: DynamicThrottlerGuard },
|
|
||||||
{ provide: APP_FILTER, useClass: DeleteExceptionFilter },
|
{ provide: APP_FILTER, useClass: DeleteExceptionFilter },
|
||||||
DynamicThrottlerGuard,
|
|
||||||
EdrPassengerOrgSeeder,
|
EdrPassengerOrgSeeder,
|
||||||
PassengerStaffUsersSeeder,
|
PassengerStaffUsersSeeder,
|
||||||
SegmentFareSeeder,
|
SegmentFareSeeder,
|
||||||
|
|||||||
@@ -1,57 +0,0 @@
|
|||||||
import { Injectable, ExecutionContext, Inject } from '@nestjs/common';
|
|
||||||
import { Reflector } from '@nestjs/core';
|
|
||||||
import { ThrottlerGuard, ThrottlerStorage, getOptionsToken, getStorageToken } from '@nestjs/throttler';
|
|
||||||
import { SystemConfigService, CONFIG_KEYS } from '../modules/system-config/system-config.service';
|
|
||||||
|
|
||||||
// Route-prefix → throttler tier mapping.
|
|
||||||
// Evaluated in order; first match wins.
|
|
||||||
const ROUTE_TIERS: Array<{ prefix: string; tier: 'auth' | 'strict' | 'default' }> = [
|
|
||||||
{ prefix: '/auth', tier: 'auth' },
|
|
||||||
{ prefix: '/fayda/verification',tier: 'auth' },
|
|
||||||
{ prefix: '/bookings', tier: 'strict' },
|
|
||||||
{ prefix: '/passengers', tier: 'strict' },
|
|
||||||
{ prefix: '/payments', tier: 'strict' },
|
|
||||||
{ prefix: '/wallet', tier: 'strict' },
|
|
||||||
];
|
|
||||||
|
|
||||||
@Injectable()
|
|
||||||
export class DynamicThrottlerGuard extends ThrottlerGuard {
|
|
||||||
constructor(
|
|
||||||
@Inject(getOptionsToken()) options: any,
|
|
||||||
@Inject(getStorageToken()) storageService: ThrottlerStorage,
|
|
||||||
reflector: Reflector,
|
|
||||||
private readonly systemConfig: SystemConfigService,
|
|
||||||
) {
|
|
||||||
super(options, storageService, reflector);
|
|
||||||
}
|
|
||||||
|
|
||||||
async canActivate(context: ExecutionContext): Promise<boolean> {
|
|
||||||
if (context.getType() !== 'http') {
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
const [authLimit, authTtl, strictLimit, strictTtl, defaultLimit, defaultTtl] =
|
|
||||||
await Promise.all([
|
|
||||||
this.systemConfig.getNumber(CONFIG_KEYS.THROTTLE_AUTH_LIMIT),
|
|
||||||
this.systemConfig.getNumber(CONFIG_KEYS.THROTTLE_AUTH_TTL_MS),
|
|
||||||
this.systemConfig.getNumber(CONFIG_KEYS.THROTTLE_STRICT_LIMIT),
|
|
||||||
this.systemConfig.getNumber(CONFIG_KEYS.THROTTLE_STRICT_TTL_MS),
|
|
||||||
this.systemConfig.getNumber(CONFIG_KEYS.THROTTLE_DEFAULT_LIMIT),
|
|
||||||
this.systemConfig.getNumber(CONFIG_KEYS.THROTTLE_DEFAULT_TTL_MS),
|
|
||||||
]);
|
|
||||||
|
|
||||||
const url: string = context.switchToHttp().getRequest<{ url: string }>().url ?? '';
|
|
||||||
const matched = ROUTE_TIERS.find(({ prefix }) => url.startsWith(prefix));
|
|
||||||
const tier = matched?.tier ?? 'default';
|
|
||||||
|
|
||||||
if (tier === 'auth') {
|
|
||||||
this.throttlers = [{ name: 'auth', ttl: authTtl, limit: authLimit }];
|
|
||||||
} else if (tier === 'strict') {
|
|
||||||
this.throttlers = [{ name: 'strict', ttl: strictTtl, limit: strictLimit }];
|
|
||||||
} else {
|
|
||||||
this.throttlers = [{ name: 'default', ttl: defaultTtl, limit: defaultLimit }];
|
|
||||||
}
|
|
||||||
|
|
||||||
return super.canActivate(context);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -33,11 +33,16 @@ async function bootstrap() {
|
|||||||
// version-neutral at their existing paths (e.g. /search, /bookings) — unchanged for the frontend.
|
// version-neutral at their existing paths (e.g. /search, /bookings) — unchanged for the frontend.
|
||||||
app.enableVersioning({ type: VersioningType.URI });
|
app.enableVersioning({ type: VersioningType.URI });
|
||||||
|
|
||||||
|
// Allowed CORS origins come from a single comma-separated env var (CORS_ORIGINS),
|
||||||
|
// e.g. "https://portal.edr.et,https://backoffice.edr.et". Whitespace around each
|
||||||
|
// entry is trimmed and empties are dropped. Falls back to the local dev ports.
|
||||||
|
const corsOrigins = (process.env.CORS_ORIGINS ?? "http://localhost:5174,http://localhost:5184")
|
||||||
|
.split(",")
|
||||||
|
.map((origin) => origin.trim())
|
||||||
|
.filter((origin) => origin.length > 0);
|
||||||
|
|
||||||
app.enableCors({
|
app.enableCors({
|
||||||
origin: [
|
origin: corsOrigins,
|
||||||
process.env.PORTAL_URL ?? "http://localhost:5174",
|
|
||||||
process.env.BACK_OFFICE_URL ?? "http://localhost:5184",
|
|
||||||
],
|
|
||||||
methods: ['GET', 'POST', 'PUT', 'PATCH', 'DELETE', 'OPTIONS'],
|
methods: ['GET', 'POST', 'PUT', 'PATCH', 'DELETE', 'OPTIONS'],
|
||||||
allowedHeaders: ['Content-Type', 'Authorization', 'Accept-Language', 'X-Request-ID'],
|
allowedHeaders: ['Content-Type', 'Authorization', 'Accept-Language', 'X-Request-ID'],
|
||||||
credentials: true,
|
credentials: true,
|
||||||
|
|||||||
@@ -20,7 +20,6 @@ import {
|
|||||||
ApiBody,
|
ApiBody,
|
||||||
ApiBearerAuth,
|
ApiBearerAuth,
|
||||||
} from "@nestjs/swagger";
|
} from "@nestjs/swagger";
|
||||||
import { Throttle, SkipThrottle } from "@nestjs/throttler";
|
|
||||||
import { IsPublic } from "@tria-plc/api-common/modules/auth/decorators/public.decorator";
|
import { IsPublic } from "@tria-plc/api-common/modules/auth/decorators/public.decorator";
|
||||||
import { PassengerAuthService } from "./passenger-auth.service";
|
import { PassengerAuthService } from "./passenger-auth.service";
|
||||||
import {
|
import {
|
||||||
|
|||||||
@@ -22,7 +22,6 @@ import {
|
|||||||
ApiBody,
|
ApiBody,
|
||||||
} from "@nestjs/swagger";
|
} from "@nestjs/swagger";
|
||||||
import { IsPublic } from "@tria-plc/api-common/modules/auth/decorators/public.decorator";
|
import { IsPublic } from "@tria-plc/api-common/modules/auth/decorators/public.decorator";
|
||||||
import { Throttle } from "@nestjs/throttler";
|
|
||||||
import { BookingsService } from "./bookings.service";
|
import { BookingsService } from "./bookings.service";
|
||||||
import { GuestBookingService } from "./guest-booking.service";
|
import { GuestBookingService } from "./guest-booking.service";
|
||||||
import {
|
import {
|
||||||
|
|||||||
@@ -1,13 +1,11 @@
|
|||||||
import { Controller, Get, HttpStatus, Res } from '@nestjs/common';
|
import { Controller, Get, HttpStatus, Res } from '@nestjs/common';
|
||||||
import { ApiTags, ApiOperation } from '@nestjs/swagger';
|
import { ApiTags, ApiOperation } from '@nestjs/swagger';
|
||||||
import { SkipThrottle } from '@nestjs/throttler';
|
|
||||||
import { IsPublic } from '@tria-plc/api-common/modules/auth/decorators/public.decorator';
|
import { IsPublic } from '@tria-plc/api-common/modules/auth/decorators/public.decorator';
|
||||||
import { PrismaService } from '../../common/prisma.service';
|
import { PrismaService } from '../../common/prisma.service';
|
||||||
import { Response } from 'express';
|
import { Response } from 'express';
|
||||||
|
|
||||||
@ApiTags('Health')
|
@ApiTags('Health')
|
||||||
@Controller('health')
|
@Controller('health')
|
||||||
@SkipThrottle()
|
|
||||||
export class HealthController {
|
export class HealthController {
|
||||||
constructor(private readonly prisma: PrismaService) {}
|
constructor(private readonly prisma: PrismaService) {}
|
||||||
|
|
||||||
|
|||||||
@@ -19,7 +19,6 @@ import {
|
|||||||
ApiResponse,
|
ApiResponse,
|
||||||
ApiQuery,
|
ApiQuery,
|
||||||
} from "@nestjs/swagger";
|
} from "@nestjs/swagger";
|
||||||
import { SkipThrottle, Throttle } from "@nestjs/throttler";
|
|
||||||
import { PassengersService } from "./passengers.service";
|
import { PassengersService } from "./passengers.service";
|
||||||
import {
|
import {
|
||||||
CreateTravelerProfileDto,
|
CreateTravelerProfileDto,
|
||||||
|
|||||||
@@ -7,7 +7,6 @@ import {
|
|||||||
UseGuards,
|
UseGuards,
|
||||||
} from "@nestjs/common";
|
} from "@nestjs/common";
|
||||||
import { ApiOperation, ApiTags } from "@nestjs/swagger";
|
import { ApiOperation, ApiTags } from "@nestjs/swagger";
|
||||||
import { SkipThrottle } from "@nestjs/throttler";
|
|
||||||
import { ServiceAuthGuard } from "../../common/guards/service-auth.guard";
|
import { ServiceAuthGuard } from "../../common/guards/service-auth.guard";
|
||||||
import { PaymentEventDto, MarkPaidResponseDto } from "./internal-payments.dto";
|
import { PaymentEventDto, MarkPaidResponseDto } from "./internal-payments.dto";
|
||||||
import { PaymentsService } from "./payments.service";
|
import { PaymentsService } from "./payments.service";
|
||||||
@@ -21,7 +20,6 @@ import { PaymentsService } from "./payments.service";
|
|||||||
@ApiTags("Internal Payments")
|
@ApiTags("Internal Payments")
|
||||||
@UseGuards(ServiceAuthGuard)
|
@UseGuards(ServiceAuthGuard)
|
||||||
@Controller("internal/payments")
|
@Controller("internal/payments")
|
||||||
@SkipThrottle()
|
|
||||||
export class InternalPaymentsController {
|
export class InternalPaymentsController {
|
||||||
constructor(private readonly paymentsService: PaymentsService) {}
|
constructor(private readonly paymentsService: PaymentsService) {}
|
||||||
|
|
||||||
|
|||||||
@@ -21,7 +21,6 @@ import {
|
|||||||
ApiProduces,
|
ApiProduces,
|
||||||
} from "@nestjs/swagger";
|
} from "@nestjs/swagger";
|
||||||
|
|
||||||
import { SkipThrottle, Throttle } from "@nestjs/throttler";
|
|
||||||
import { Response } from "express";
|
import { Response } from "express";
|
||||||
import { PaymentsService } from "./payments.service";
|
import { PaymentsService } from "./payments.service";
|
||||||
import {
|
import {
|
||||||
|
|||||||
@@ -1,6 +1,5 @@
|
|||||||
import { Body, Controller, Get, Patch, SetMetadata, UseGuards } from '@nestjs/common';
|
import { Body, Controller, Get, Patch, SetMetadata, UseGuards } from '@nestjs/common';
|
||||||
import { ApiTags, ApiBearerAuth, ApiOperation } from '@nestjs/swagger';
|
import { ApiTags, ApiBearerAuth, ApiOperation } from '@nestjs/swagger';
|
||||||
import { SkipThrottle } from '@nestjs/throttler';
|
|
||||||
import { SystemConfigService } from './system-config.service';
|
import { SystemConfigService } from './system-config.service';
|
||||||
import { IamGuard } from '../../common/iam-adapter';
|
import { IamGuard } from '../../common/iam-adapter';
|
||||||
import { Roles } from '../../common/roles.decorator';
|
import { Roles } from '../../common/roles.decorator';
|
||||||
@@ -12,7 +11,6 @@ export class SystemConfigController {
|
|||||||
|
|
||||||
@Get('fayda-status')
|
@Get('fayda-status')
|
||||||
@SetMetadata('isPublic', true)
|
@SetMetadata('isPublic', true)
|
||||||
@SkipThrottle()
|
|
||||||
@ApiOperation({ summary: 'Get Fayda verification enabled status (public)' })
|
@ApiOperation({ summary: 'Get Fayda verification enabled status (public)' })
|
||||||
getFaydaStatus() {
|
getFaydaStatus() {
|
||||||
const enabled = process.env.VERIFAYDA_ENABLED !== 'false';
|
const enabled = process.env.VERIFAYDA_ENABLED !== 'false';
|
||||||
|
|||||||
@@ -15,7 +15,6 @@ import {
|
|||||||
ApiOperation,
|
ApiOperation,
|
||||||
ApiTags,
|
ApiTags,
|
||||||
} from "@nestjs/swagger";
|
} from "@nestjs/swagger";
|
||||||
import { Throttle } from "@nestjs/throttler";
|
|
||||||
import type { TCurrentUser } from "@tria-plc/api-common/modules/auth/types/current-user.type";
|
import type { TCurrentUser } from "@tria-plc/api-common/modules/auth/types/current-user.type";
|
||||||
import { IsPublic } from "@tria-plc/api-common/modules/auth/decorators/public.decorator";
|
import { IsPublic } from "@tria-plc/api-common/modules/auth/decorators/public.decorator";
|
||||||
import { JwtGuard } from "../../common/jwt.guard";
|
import { JwtGuard } from "../../common/jwt.guard";
|
||||||
|
|||||||
@@ -10,7 +10,6 @@ import {
|
|||||||
Query,
|
Query,
|
||||||
} from "@nestjs/common";
|
} from "@nestjs/common";
|
||||||
import { ApiTags, ApiOperation, ApiBearerAuth } from "@nestjs/swagger";
|
import { ApiTags, ApiOperation, ApiBearerAuth } from "@nestjs/swagger";
|
||||||
import { Throttle } from "@nestjs/throttler";
|
|
||||||
import { WalletService } from "./wallet.service";
|
import { WalletService } from "./wallet.service";
|
||||||
import { JwtGuard } from "../../common/jwt.guard";
|
import { JwtGuard } from "../../common/jwt.guard";
|
||||||
|
|
||||||
|
|||||||
@@ -104,7 +104,7 @@ const navigationSections: { title: string; items: NavItem[] }[] = [
|
|||||||
title: 'Customer Services',
|
title: 'Customer Services',
|
||||||
items: [
|
items: [
|
||||||
// { name: 'Loyalty Program', href: '/loyalty', icon: Gift, permission: PERMS.passengers.view },
|
// { name: 'Loyalty Program', href: '/loyalty', icon: Gift, permission: PERMS.passengers.view },
|
||||||
// { name: 'Support Center', href: '/support', icon: MessageSquare, permission: PERMS.bookings.view },
|
{ name: 'Support Center', href: '/support', icon: MessageSquare, permission: PERMS.bookings.view },
|
||||||
{ name: 'Notifications', href: '/notifications', icon: Bell, permission: PERMS.notifications.send },
|
{ name: 'Notifications', href: '/notifications', icon: Bell, permission: PERMS.notifications.send },
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -994,8 +994,20 @@ function PassengersForm() {
|
|||||||
const onInvalid = () => {
|
const onInvalid = () => {
|
||||||
// Sections still behind the Fayda verify screen stay collapsed here — they only expand
|
// Sections still behind the Fayda verify screen stay collapsed here — they only expand
|
||||||
// when the user explicitly clicks "Enter details manually" (shown only when Fayda is
|
// when the user explicitly clicks "Enter details manually" (shown only when Fayda is
|
||||||
// unavailable).
|
// unavailable). Their name/DOB/gender fields are still empty at this point, which would
|
||||||
setSubmitError('Please fix the highlighted errors before continuing.');
|
// otherwise surface as a generic "fix the highlighted errors" message that doesn't point
|
||||||
|
// at the actual cause — tell the user to verify with Fayda instead.
|
||||||
|
const needsFaydaVerification = passengers.some((p, i) => {
|
||||||
|
const isEthiopian = p?.nationality === 'ETHIOPIAN';
|
||||||
|
const isChildPassenger = i >= adultCount;
|
||||||
|
const isLoggedInAndVerified = i === 0 && isAuthenticated && user?.faydaVerified;
|
||||||
|
return isEthiopian && faydaEnabled && !p?.formExpanded && !isLoggedInAndVerified && !isChildPassenger;
|
||||||
|
});
|
||||||
|
setSubmitError(
|
||||||
|
needsFaydaVerification
|
||||||
|
? 'Please verify your identity with Fayda before continuing — tap "Verify with Fayda" above to proceed.'
|
||||||
|
: 'Please fix the highlighted errors before continuing.'
|
||||||
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
const onSubmit = async (data: FormData) => {
|
const onSubmit = async (data: FormData) => {
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
@import "tailwindcss";
|
@import "tailwindcss";
|
||||||
@import "tw-animate-css";
|
@import "tw-animate-css";
|
||||||
@import "./theme.css";
|
@import "./theme.css";
|
||||||
@custom-variant dark (&: where(.dark, .dark *));
|
@custom-variant dark (&:where(.dark, .dark *));
|
||||||
|
|||||||
Reference in New Issue
Block a user