This commit is contained in:
natib21
2026-07-10 11:25:59 +00:00
parent 2696ca7498
commit e6e44e773b
1146 changed files with 200266 additions and 90527 deletions

View File

@@ -0,0 +1,86 @@
import { useEffect, useState } from "react";
import { useQuery } from "@tanstack/react-query";
import {
fetchRequirementSatisfied,
AdminSetupAlertResponse,
} from "@/record-management/services/api/alertService";
import { X } from "lucide-react";
export default function AdminSetupAlert({
unitId: propUnitId,
}: {
unitId?: string;
}) {
const [unitId, setUnitId] = useState<string>(propUnitId || "");
const [visible, setVisible] = useState(true);
// Listen for unitChanged events from Content Management.
useEffect(() => {
const handler = (e: Event) => {
const detail = (e as CustomEvent<string>).detail;
queueMicrotask(() => setUnitId(detail));
};
window.addEventListener("unitChanged", handler);
return () => window.removeEventListener("unitChanged", handler);
}, []);
// TanStack Query for fetching requirements
const { data, isLoading, refetch } = useQuery<AdminSetupAlertResponse>({
queryKey: ["admin-setup-requirements", unitId],
queryFn: () => fetchRequirementSatisfied(unitId),
enabled: !!unitId,
});
const requirements = data?.requirements;
// Optional: refetch automatically on unit change (real-time trigger)
useEffect(() => {
if (unitId) {
refetch();
}
}, [unitId, refetch]);
// Hide if no requirements or all are satisfied
if (!requirements || Object.values(requirements).every(Boolean)) return null;
if (!visible) return null; // hide after dismiss
return (
<div className="relative flex items-center gap-6 p-2 border rounded bg-amber-50 shadow-2xl top-10 left-1/2 -translate-x-1/2 z-[1000] w-full">
{/* Title */}
<h3 className="font-semibold text-amber-700 whitespace-nowrap">
Finish site setup
</h3>
{/* Messages inline */}
{isLoading ? (
<p className="text-sm text-slate-600">Loading requirements...</p>
) : (
<div className="flex flex-wrap gap-6 text-sm text-slate-600">
{!requirements.hasFooter && <span> Missing Footer</span>}
{!requirements.hasSeal && <span> Missing Seal</span>}
{!requirements.hasHeader && <span> Missing Header</span>}
{!requirements.internalPrefix && (
<span> Missing Internal Prefix</span>
)}
{!requirements.externalPrefix && (
<span> Missing External Prefix</span>
)}
{!requirements.internalSuffix && (
<span> Missing Internal Suffix</span>
)}
{!requirements.externalSuffix && (
<span> Missing External Suffix</span>
)}
</div>
)}
{/* Close button */}
<button
onClick={() => setVisible(false)}
className="absolute top-2 right-2 text-amber-700 hover:text-red-600">
<X size={16} />
</button>
</div>
);
}

View File

@@ -0,0 +1,351 @@
import React, { useState } from "react";
import {
FaUpload,
FaCheckCircle,
FaCopy,
FaExclamationCircle,
FaArrowLeft,
} from "react-icons/fa";
import { useTranslation } from "react-i18next";
import { useNavigate } from "react-router-dom";
import { toast } from "sonner";
import Header from "./Header";
import Footer from "./Footer";
import {
submitComplaint,
ComplaintPayload,
FileInfo,
} from "../../shared/services/complaintService";
const ComplaintForm = () => {
const { t } = useTranslation();
const navigate = useNavigate();
const [files, setFiles] = useState<File[]>([]);
const [loading, setLoading] = useState(false);
const [disclaimerAccepted, setDisclaimerAccepted] = useState(false);
const [submissionSuccess, setSubmissionSuccess] = useState(false);
const [complaintId, setComplaintId] = useState("");
const [error, setError] = useState<string | null>(null);
const [formValues, setFormValues] = useState({
fullName: "",
subCity: "",
woreda: "",
houseNumber: "",
phone: "",
institution: "",
complaintDetails: "",
complaintWant: "",
complaintPlace: "",
});
const handleChange = (
e: React.ChangeEvent<HTMLInputElement | HTMLTextAreaElement>
) => {
setFormValues({ ...formValues, [e.target.name]: e.target.value });
};
const handleFileChange = (e: React.ChangeEvent<HTMLInputElement>) => {
if (e.target.files) {
setFiles(Array.from(e.target.files));
}
};
const handleSubmit = async (e: React.FormEvent) => {
e.preventDefault();
if (!disclaimerAccepted) return;
setLoading(true);
setError(null);
try {
// Prepare file info if files are uploaded
let fileInfo: FileInfo | undefined;
if (files.length > 0) {
const file = files[0]; // Take the first file for now
fileInfo = {
fileName: file.name,
contentType: file.type,
size: file.size,
originalname: file.name,
};
}
// Prepare the complaint payload
const payload: ComplaintPayload = {
fullName: formValues.fullName,
phoneNumber: formValues.phone,
subCity: formValues.subCity,
woreda: formValues.woreda,
houseNumber: formValues.houseNumber,
institution: formValues.institution,
complaintPlace: formValues.complaintPlace,
complaintDetail: formValues.complaintDetails,
desiredResolution: formValues.complaintWant,
fileInfo,
};
// Submit the complaint
const response = await submitComplaint(payload);
if (response.data) {
setComplaintId(response.data.complaintNumber || response.data.id);
setSubmissionSuccess(true);
toast.success(t("complaint.success"));
}
} catch (err: any) {
console.error("Error submitting complaint:", err);
const errorMessage =
err.response?.data?.message || err.message || t("complaint.error");
setError(errorMessage);
toast.error(errorMessage);
} finally {
setLoading(false);
}
};
return (
<div>
<Header />
<div className="min-h-screen bg-gradient-to-b from-blue-50 to-white flex flex-col pt-16 md:pt-20">
{/* Hero Section */}
<div className="bg-primary py-16 text-center text-white relative overflow-hidden">
<div className="absolute top-0 left-0 w-full h-full bg-[url('https://www.transparenttextures.com/patterns/cubes.png')] opacity-10"></div>
<h1 className="text-4xl md:text-5xl font-bold mb-4 relative z-10 animate-fade-in">
{t("complaint.title")}
</h1>
<p className="max-w-2xl mx-auto text-lg opacity-90 relative z-10">
{t("complaint.subtitle")}
</p>
</div>
{/* Main Form */}
<div className="flex-1 flex justify-center p-6">
<div className="w-full max-w-3xl bg-white shadow-2xl rounded-2xl p-8 relative border border-gray-100">
{/* Floating circles */}
<div className="absolute -top-6 -left-6 w-16 h-16 bg-blue-100 rounded-full opacity-30 animate-pulse"></div>
<div className="absolute -bottom-6 -right-6 w-24 h-24 bg-blue-100 rounded-full opacity-30 animate-ping"></div>
<h2 className="text-2xl font-semibold text-blue-800 mb-8 text-center">
{t("complaint.formTitle")}
</h2>
{error && (
<div className="mb-6 p-4 bg-red-50 border border-red-200 rounded-lg">
<div className="flex items-center gap-2">
<FaExclamationCircle className="text-red-500" />
<span className="text-red-700 font-medium">
{t("complaint.error")}
</span>
</div>
<p className="text-red-600 text-sm mt-1">{error}</p>
</div>
)}
{submissionSuccess ? (
<div className="text-center animate-fade-in">
<FaCheckCircle className="text-primary-500 text-5xl mx-auto mb-4" />
<h3 className="text-xl font-semibold mb-2">
{t("complaint.success")}
</h3>
<p className="mb-4 text-gray-600">{t("complaint.keepId")}</p>
<div className="bg-gray-100 border rounded-lg p-4 flex justify-center items-center gap-3">
<span className="font-bold text-blue-700">{complaintId}</span>
<button
className="p-2 rounded hover:bg-gray-200"
onClick={() => navigator.clipboard.writeText(complaintId)}
>
<FaCopy />
</button>
</div>
<div className="flex justify-center gap-4 mt-6">
<button
onClick={() => setSubmissionSuccess(false)}
className="px-6 py-3 bg-primary text-primary-foreground rounded-lg shadow hover:bg-primary/90 transition"
>
{t("complaint.close")}
</button>
<button
onClick={() => navigate("/complaints")}
className="px-6 py-3 bg-gray-300 text-gray-800 rounded-lg shadow hover:bg-gray-400 transition flex items-center gap-2"
>
<FaArrowLeft /> {t("complaint.back")}
</button>
</div>
</div>
) : (
<form
onSubmit={handleSubmit}
className="space-y-6 animate-fade-in"
>
{/* Inputs */}
<div className="grid grid-cols-1 md:grid-cols-2 gap-6">
<input
type="text"
name="fullName"
value={formValues.fullName}
onChange={handleChange}
placeholder={t("complaint.fullName")}
required
className="w-full p-3 border rounded-lg focus:ring-2 focus:ring-blue-400 outline-none"
/>
<input
type="tel"
name="phone"
value={formValues.phone}
onChange={handleChange}
placeholder={t("complaint.phone")}
required
className="w-full p-3 border rounded-lg focus:ring-2 focus:ring-blue-400 outline-none"
/>
<input
type="text"
name="subCity"
value={formValues.subCity}
onChange={handleChange}
placeholder={t("complaint.subCity")}
className="w-full p-3 border rounded-lg focus:ring-2 focus:ring-blue-400 outline-none"
/>
<input
type="text"
name="woreda"
value={formValues.woreda}
onChange={handleChange}
placeholder={t("complaint.woreda")}
className="w-full p-3 border rounded-lg focus:ring-2 focus:ring-blue-400 outline-none"
/>
<input
type="text"
name="houseNumber"
value={formValues.houseNumber}
onChange={handleChange}
placeholder={t("complaint.houseNumber")}
className="w-full p-3 border rounded-lg focus:ring-2 focus:ring-blue-400 outline-none"
/>
<input
type="text"
name="institution"
value={formValues.institution}
onChange={handleChange}
placeholder={t("complaint.institution")}
className="w-full p-3 border rounded-lg focus:ring-2 focus:ring-blue-400 outline-none"
/>
</div>
<input
type="text"
name="complaintPlace"
value={formValues.complaintPlace}
onChange={handleChange}
placeholder={t("complaint.place")}
className="w-full p-3 border rounded-lg focus:ring-2 focus:ring-blue-400 outline-none"
/>
<textarea
name="complaintDetails"
value={formValues.complaintDetails}
onChange={handleChange}
placeholder={t("complaint.details")}
rows={4}
className="w-full p-3 border rounded-lg focus:ring-2 focus:ring-blue-400 outline-none"
></textarea>
<textarea
name="complaintWant"
value={formValues.complaintWant}
onChange={handleChange}
placeholder={t("complaint.want")}
rows={4}
className="w-full p-3 border rounded-lg focus:ring-2 focus:ring-blue-400 outline-none"
></textarea>
{/* File Upload */}
{/* <div>
<label className="block text-sm font-medium text-gray-700 mb-2">
{t("complaint.evidence")}
</label>
<div className="flex items-center gap-3">
<input
type="file"
multiple
onChange={handleFileChange}
className="block w-full text-sm text-gray-500 file:mr-4 file:py-2 file:px-4
file:rounded-lg file:border-0
file:text-sm file:font-semibold
file:bg-blue-50 file:text-blue-700
hover:file:bg-blue-100"
/>
<FaUpload className="text-gray-400" />
</div>
{files.length > 0 && (
<p className="text-sm text-gray-500 mt-2">
{t("complaint.selectedFiles")}:{" "}
{files.map((f) => f.name).join(", ")}
</p>
)}
</div> */}
{/* Disclaimer */}
<div className="bg-red-50 border border-red-200 rounded-lg p-4">
<div className="flex items-center gap-2 mb-2">
<FaExclamationCircle className="text-red-500" />
<span className="font-semibold text-red-700">
{t("complaint.disclaimerTitle")}
</span>
</div>
<p className="text-sm text-gray-700">
{t("complaint.disclaimerText")}
</p>
</div>
<div className="flex items-center gap-2">
<input
type="checkbox"
checked={disclaimerAccepted}
onChange={(e) => setDisclaimerAccepted(e.target.checked)}
className="w-4 h-4 text-primary border-gray-300 rounded focus:ring-primary"
/>
<span className="text-sm text-gray-700">
{t("complaint.accept")}
</span>
</div>
{/* Submit + Back */}
<div className="flex justify-between items-center gap-4">
<button
type="button"
onClick={() => navigate("/complaints")}
className="flex items-center gap-2 px-5 py-3 bg-gray-300 text-gray-800 rounded-lg shadow hover:bg-gray-400 transition"
>
<FaArrowLeft /> {t("complaint.back")}
</button>
<button
type="submit"
disabled={!disclaimerAccepted || loading}
className={`px-6 py-3 rounded-lg text-white font-semibold transition flex items-center justify-center gap-2
${
loading || !disclaimerAccepted
? "bg-gray-400 cursor-not-allowed"
: "bg-primary hover:bg-primary-700"
}`}
>
{loading && (
<div className="w-4 h-4 border-2 border-white border-t-transparent rounded-full animate-spin"></div>
)}
{loading
? t("complaint.submitting")
: t("complaint.submit")}
</button>
</div>
</form>
)}
</div>
</div>
</div>
<Footer />
</div>
);
};
export default ComplaintForm;

View File

@@ -0,0 +1,229 @@
import { motion } from "framer-motion";
import { useTranslation } from "react-i18next";
import { useState } from "react";
const FeaturesSection = () => {
const { t } = useTranslation();
const [hoveredIndex, setHoveredIndex] = useState<number | null>(null);
const features = [
{
name: t("landingPage.outgoingRecords"),
description: t("landingPage.outgoingRecordsDesc"),
icon: (
<svg
className="h-6 w-6"
fill="none"
viewBox="0 0 24 24"
stroke="currentColor"
>
<path
strokeLinecap="round"
strokeLinejoin="round"
strokeWidth={2}
d="M12 19l9 2-9-18-9 18 9-2zm0 0v-8"
/>
</svg>
),
color: "bg-primary",
},
{
name: t("landingPage.incomingRecords"),
description: t("landingPage.incomingRecordsDesc"),
icon: (
<svg
className="h-6 w-6"
fill="none"
viewBox="0 0 24 24"
stroke="currentColor"
>
<path
strokeLinecap="round"
strokeLinejoin="round"
strokeWidth={2}
d="M3 8l7.89 5.26a2 2 0 002.22 0L21 8M5 19h14a2 2 0 002-2V7a2 2 0 00-2-2H5a2 2 0 00-2 2v10a2 2 0 002 2z"
/>
</svg>
),
color: "bg-primary-500",
},
{
name: t("landingPage.approvalWorkflows"),
description: t("landingPage.approvalWorkflowsDesc"),
icon: (
<svg
className="h-6 w-6"
fill="none"
viewBox="0 0 24 24"
stroke="currentColor"
>
<path
strokeLinecap="round"
strokeLinejoin="round"
strokeWidth={2}
d="M9 12l2 2 4-4m6 2a9 9 0 11-18 0 9 9 0 0118 0z"
/>
</svg>
),
color: "bg-[#3B82F6]",
},
{
name: t("landingPage.interactiveDashboard"),
description: t("landingPage.interactiveDashboardDesc"),
icon: (
<svg
className="h-6 w-6"
fill="none"
viewBox="0 0 24 24"
stroke="currentColor"
>
<path
strokeLinecap="round"
strokeLinejoin="round"
strokeWidth={2}
d="M9 17v-2m3 2v-4m3 4v-6m2 10H7a2 2 0 01-2-2V5a2 2 0 012-2h5.586a1 1 0 01.707.293l5.414 5.414a1 1 0 01.293.707V19a2 2 0 01-2 2z"
/>
</svg>
),
color: "bg-[#8B5CF6]",
},
{
name: t("landingPage.organizedFolders"),
description: t("landingPage.organizedFoldersDesc"),
icon: (
<svg
className="h-6 w-6"
fill="none"
viewBox="0 0 24 24"
stroke="currentColor"
>
<path
strokeLinecap="round"
strokeLinejoin="round"
strokeWidth={2}
d="M3 7v10a2 2 0 002 2h14a2 2 0 002-2V9a2 2 0 00-2-2h-6l-2-2H5a2 2 0 00-2 2z"
/>
</svg>
),
color: "bg-[#EC4899]",
},
{
name: t("landingPage.securityAccessControl"),
description: t("landingPage.securityAccessControlDesc"),
icon: (
<svg
className="h-6 w-6"
fill="none"
viewBox="0 0 24 24"
stroke="currentColor"
>
<path
strokeLinecap="round"
strokeLinejoin="round"
strokeWidth={2}
d="M12 15v2m-6 4h12a2 2 0 002-2v-6a2 2 0 00-2-2H6a2 2 0 00-2 2v6a2 2 0 002 2zm10-10V7a4 4 0 00-8 0v4h8z"
/>
</svg>
),
color: "bg-[#F59E0B]",
},
];
return (
<div className="py-16 bg-gradient-to-b from-gray-50 to-white dark:from-gray-900 dark:to-gray-800">
<div className="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8">
<motion.div
initial={{ opacity: 0, y: 20 }}
whileInView={{ opacity: 1, y: 0 }}
transition={{ duration: 0.5 }}
viewport={{ once: true }}
className="lg:text-center mb-16"
>
<span className="inline-flex items-center px-3 py-1 rounded-full text-sm font-medium bg-primary/10 text-primary">
{t("landingPage.powerfulFeaturesTitle")}
</span>
<h2 className="mt-4 text-4xl font-extrabold tracking-tight text-gray-900 dark:text-white sm:text-5xl">
<span className="block">
{t("landingPage.powerfulFeaturesSubtitle")}
</span>
<span className="block text-primary">
{t("landingPage.documentWorkflow")}
</span>
</h2>
<p className="mt-6 max-w-3xl text-xl text-gray-600 dark:text-gray-300 lg:mx-auto">
{t("landingPage.powerfulFeaturesDesc")}
</p>
</motion.div>
<div className="mt-12">
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-8">
{features.map((feature, index) => (
<motion.div
key={feature.name}
initial={{ opacity: 0, y: 20 }}
whileInView={{ opacity: 1, y: 0 }}
transition={{ duration: 0.5, delay: index * 0.1 }}
viewport={{ once: true }}
onHoverStart={() => setHoveredIndex(index)}
onHoverEnd={() => setHoveredIndex(null)}
className="relative"
>
<div
className={`absolute -inset-0.5 rounded-xl ${
feature.color
} blur opacity-75 transition duration-500 ${
hoveredIndex === index ? "opacity-100" : "opacity-0"
}`}
></div>
<div className="relative bg-white dark:bg-gray-800 p-6 rounded-xl border border-gray-200 dark:border-gray-700 h-full transition-all duration-300 hover:border-primary/30">
<div
className={`flex items-center justify-center h-12 w-12 rounded-lg ${feature.color} text-white mb-4`}
>
{feature.icon}
</div>
<h3 className="text-xl font-bold text-gray-900 dark:text-white mb-2">
{feature.name}
</h3>
<p className="text-gray-600 dark:text-gray-300">{feature.description}</p>
<div className="mt-4">
</div>
</div>
</motion.div>
))}
</div>
</div>
{/* Stats Section */}
<motion.div
initial={{ opacity: 0 }}
whileInView={{ opacity: 1 }}
transition={{ duration: 0.5, delay: 0.3 }}
viewport={{ once: true }}
className="mt-20 bg-gradient-to-r from-primary to-primary-500 rounded-2xl shadow-xl overflow-hidden"
>
<div className="max-w-7xl mx-auto py-12 px-6 lg:px-8">
<div className="grid grid-cols-1 md:grid-cols-3 gap-8 text-center">
{[
{ number: "95%", label: t("landingPage.fasterApproval") },
{ number: "10x", label: t("landingPage.auditReady") },
{ number: "100%", label: t("landingPage.organizedRecords") },
].map((stat, index) => (
<div key={index} className="px-6 py-8">
<p className="text-4xl font-extrabold text-white">
{stat.number}
</p>
<p className="mt-2 text-lg font-medium text-white/90">
{stat.label}
</p>
</div>
))}
</div>
</div>
</motion.div>
</div>
</div>
);
};
export default FeaturesSection;

View File

@@ -0,0 +1,172 @@
import React, { useState } from "react";
import { FaExclamationCircle, FaArrowLeft } from "react-icons/fa";
import { useTranslation } from "react-i18next";
import { useNavigate } from "react-router-dom";
import Header from "./Header";
import Footer from "./Footer";
import { getComplaintById } from "../../shared/services/complaintService";
const FollowCompliant = () => {
const { t } = useTranslation();
const navigate = useNavigate();
const [loading, setLoading] = useState(false);
const [searchButton, setSearchButton] = useState(false);
const [error, setError] = useState<string | null>(null);
const [searchId, setSearchId] = useState("");
const [formValues, setFormValues] = useState({
complaintNumber: "",
status: "",
createdAt: "",
message: "",
});
const handleSubmit = async (id: string) => {
try {
setLoading(true); // start spinner
const response = await getComplaintById(id);
if (response) {
setFormValues({
complaintNumber: response.data.complaintNumber || "",
status: response.data.status || "",
createdAt: response.data.createdAt || "",
message: response.data.message || "",
});
}
} catch (error) {
setError(t("complaint.errorMsg"));
} finally {
setLoading(false); // stop spinner
}
};
return (
<div>
<Header />
<div className="min-h-screen bg-gradient-to-b from-blue-50 to-white flex flex-col pt-16 md:pt-20">
{/* Hero Section */}
<div className="bg-primary py-16 text-center text-white relative overflow-hidden">
<div className="absolute top-0 left-0 w-full h-full bg-[url('https://www.transparenttextures.com/patterns/cubes.png')] opacity-10"></div>
<h1 className="text-4xl md:text-5xl font-bold mb-4 relative z-10 animate-fade-in">
{t("complaint.followTitle")}
</h1>
<p className="max-w-2xl mx-auto text-lg opacity-90 relative z-10">
{t("complaint.followSubtitle")}
</p>
</div>
{/* Main Form */}
<div className="flex-1 flex justify-center p-6">
<div className="w-full max-w-3xl bg-white shadow-2xl rounded-2xl p-8 relative border border-gray-100">
{/* Floating circles */}
<div className="absolute -top-6 -left-6 w-16 h-16 bg-blue-100 rounded-full opacity-30 animate-pulse"></div>
<h2 className="text-2xl font-semibold text-blue-800 mb-8 text-center">
{t("complaint.followTitle")}
</h2>
{error && (
<div className="mb-6 p-4 bg-red-50 border border-red-200 rounded-lg">
<div className="flex items-center gap-2">
<FaExclamationCircle className="text-red-500" />
<span className="text-red-700 font-medium">
{t("complaint.error")}
</span>
</div>
<p className="text-red-600 text-sm mt-1">{error}</p>
</div>
)}
<form className="space-y-6 animate-fade-in">
{/* View Fields */}
<div className="flex gap-2">
<label htmlFor="complaintId" className="self-center">
{t("complaint.compliantId")}
</label>
<input
id="complaintId"
type="text"
value={searchId}
onChange={(e) => {
const value = e.target.value;
setSearchId(value);
setSearchButton(value.trim() !== "");
}}
placeholder={t("complaint.enterComNum")}
className="border rounded px-3 py-2 w-full"
/>
<button
type="button"
disabled={!searchButton || loading}
onClick={() => handleSubmit(searchId)}
className={`flex items-center justify-center gap-2 px-4 py-2 rounded transition ${
searchButton && !loading
? "bg-primary text-primary-foreground hover:bg-primary/90"
: "bg-gray-300 text-gray-500 cursor-not-allowed"
}`}
>
{loading ? (
<svg
className="animate-spin h-5 w-5 text-white"
xmlns="http://www.w3.org/2000/svg"
fill="none"
viewBox="0 0 24 24"
>
<circle
className="opacity-25"
cx="12"
cy="12"
r="10"
stroke="currentColor"
strokeWidth="4"
></circle>
<path
className="opacity-75"
fill="currentColor"
d="M4 12a8 8 0 018-8v4a4 4 0 00-4 4H4z"
></path>
</svg>
) : (
t("userRecord.Search")
)}
</button>
</div>
<div className="grid grid-cols-1 md:grid-cols-2 gap-6">
<p className="w-full p-3 border rounded-lg bg-gray-50 text-gray-700">
{formValues.complaintNumber || t("complaint.complaintNo")}
</p>
<p className="w-full p-3 border rounded-lg bg-gray-50 text-gray-700">
{formValues.status || t("userRecord.Status")}
</p>
<p className="w-full p-3 border rounded-lg bg-gray-50 text-gray-700">
{formValues.createdAt || t("userRecord.createdAt")}
</p>
</div>
{/* Message in full width */}
<div className="mt-6">
<p className="w-full min-h-[120px] p-3 border rounded-lg bg-gray-50 text-gray-700 whitespace-pre-line">
{formValues.message || t("complaint.message")}
</p>
</div>
{/* Back */}
<div className="flex justify-end items-center gap-4">
<button
type="button"
onClick={() => navigate("/")}
className="flex items-center cursor-pointer gap-2 px-5 py-3 bg-gray-300 text-gray-800 rounded-lg shadow hover:bg-gray-400 transition"
>
<FaArrowLeft /> {t("complaint.back")}
</button>
</div>
</form>
</div>
</div>
</div>
<Footer />
</div>
);
};
export default FollowCompliant;

View File

@@ -0,0 +1,374 @@
import {
TooltipContent,
TooltipProvider,
TooltipTrigger,
Tooltip,
} from "@/shared/common/ui/tooltip";
import { motion } from "framer-motion";
import { Share2 as Facebook, Globe, Globe as Linkedin, Mail, MapPin, Phone } from "lucide-react";
import { FaTelegramPlane, FaTiktok } from "react-icons/fa";
import { FaXTwitter } from "react-icons/fa6";
import { useState } from "react";
import { useTranslation } from "react-i18next";
import { useNavigate } from "react-router-dom";
import { useTenantConfig } from "@/layout/components/TenantConfig";
const Footer = () => {
const [activeTab, setActiveTab] = useState("");
const navigate = useNavigate();
const { t } = useTranslation();
const { config: tenantConfig } = useTenantConfig();
const defaultFooterData = {
address: "XQXP+J6C Lingo Tower, Namibia St, Addis Ababa",
phone: "+251-955-232323",
email: "sales@triaplc.com",
};
const footerData = tenantConfig?.footerData || defaultFooterData;
const navItems = [
{ id: "overview", label: t("landingPage.overview") },
{ id: "features", label: t("landingPage.features") },
{ id: "workflow", label: t("landingPage.workflow") },
{ id: "howto", label: t("landingPage.manual") },
];
const navigateTo = "/";
const handleAddressClick = () => {
const address =
footerData.address || "XQXP+J6C Lingo Tower, Namibia St, Addis Ababa";
const query = encodeURIComponent(address);
window.open(
`https://www.google.com/maps/search/?api=1&query=${query}`,
"_blank",
"noopener noreferrer",
);
};
const handleEmailClick = () => {
const email = footerData.email || "sales@triaplc.com";
const subject = "";
const body = "";
const gmailParams = new URLSearchParams();
gmailParams.append("to", email);
if (subject) gmailParams.append("su", subject);
if (body) gmailParams.append("body", body);
const gmailUrl = `https://mail.google.com/mail/?view=cm&fs=1&${gmailParams.toString()}`;
const mailtoUrl = `mailto:${email}?subject=${encodeURIComponent(subject || "")}&body=${encodeURIComponent(body || "")}`;
window.open(gmailUrl, "_blank", "noopener noreferrer");
setTimeout(() => {
window.open(mailtoUrl, "_blank", "noopener noreferrer");
}, 500);
};
const socials = tenantConfig?.socials || {};
const socialLinks = {
facebook: socials.facebook || "https://web.facebook.com/Triaplc",
twitter: socials.twitter || "https://x.com/Triaplc",
linkedin: socials.linkedin || "https://www.linkedin.com/company/triaplc",
telegram: socials.telegram || "",
tiktok: socials.tiktok || "",
website: socials.website || "https://triaplc.com/",
};
const optionalSocialLinks = [
{
href: socialLinks.telegram,
label: t("landingPage.telegram"),
icon: FaTelegramPlane,
},
{
href: socialLinks.tiktok,
label: t("landingPage.tiktok"),
icon: FaTiktok,
},
].filter((item) => item.href);
return (
<footer className="bg-gradient-to-b from-gray-900 to-gray-800 dark:bg-gradient-to-b dark:from-gray-900 dark:to-gray-800">
<div className="max-w-7xl mx-auto py-12 px-4 sm:px-6 lg:py-16 lg:px-8">
<div className="xl:grid xl:grid-cols-3 xl:gap-8">
<div className="space-y-8 xl:col-span-1">
<div className="flex items-center">
{tenantConfig?.logo ? (
<div className="relative group inline-flex">
{/* Glow */}
<div className="absolute inset-0 rounded-2xl bg-gradient-to-r from-primary/20 to-secondary/20 blur-xl opacity-0 group-hover:opacity-100 transition-all duration-500 scale-110" />
{/* Footer Logo Wrapper */}
<motion.div
whileHover={{ scale: 1.04 }}
whileTap={{ scale: 0.97 }}
transition={{ type: "spring", stiffness: 220, damping: 16 }}
className="relative inline-flex items-center justify-center cursor-pointer group"
onClick={() => (window.location.href = "/")}
title={t("nav.homePage")}
>
<motion.img
src={tenantConfig.logo}
alt="Footer Logo"
className="
h-35
w-auto
object-contain
group-hover:scale-105
transition-transform
duration-300
"
onError={(e) => {
const fallback =
e.currentTarget.parentElement?.querySelector(
".footer-logo-fallback",
);
if (fallback) {
fallback.classList.remove("hidden");
}
e.currentTarget.remove();
}}
/>
{/* Fallback when logo fails to load */}
<div
className="
footer-logo-fallback
hidden
h-20
w-20
items-center
justify-center
rounded-full
bg-primary
text-primary-foreground
font-bold
text-2xl
pointer-events-none
"
>
{tenantConfig?.organizationName
?.split(/[\s\-–—]+/)
.filter(Boolean)
.map((word) => word.charAt(0))
.join("")
.toUpperCase() || "SO"}
</div>
</motion.div>
</div>
) : (
<div className="h-10 w-10 rounded-full bg-primary flex items-center justify-center text-white font-bold text-xl">
SO
</div>
)}
<span className="ml-3 text-2xl font-bold text-white">
{tenantConfig?.organizationName || t("landingPage.smartOffice")}
</span>
</div>
<p className="text-gray-300 text-lg max-h-20 overflow-y-auto pr-2">
{tenantConfig?.footerText ||
t("landingPage.digitalTransformation")}
</p>
<div className="flex space-x-6">
<a
href={socialLinks.facebook}
target="_blank"
rel="noopener noreferrer"
className="text-gray-400 hover:text-primary transition-colors duration-300"
>
<li className="flex items-center space-x-2 text-base text-gray-400 hover:text-primary transition-colors duration-300 cursor-pointer">
<TooltipProvider>
<Tooltip>
<TooltipTrigger asChild>
<Facebook className="h-6 w-6 hover:text-primary" />
</TooltipTrigger>
<TooltipContent>
<span>{t("landingPage.facebook")}</span>
</TooltipContent>
</Tooltip>
</TooltipProvider>
<span className="sr-only">{t("landingPage.facebook")}</span>
</li>
</a>
<a
href={socialLinks.twitter}
target="_blank"
rel="noopener noreferrer"
className="text-gray-400 hover:text-primary transition-colors duration-300"
>
<li className="flex items-center space-x-2 text-base text-gray-400 hover:text-primary transition-colors duration-300 cursor-pointer">
<TooltipProvider>
<Tooltip>
<TooltipTrigger asChild>
<FaXTwitter className="h-6 w-6 hover:text-primary" />
</TooltipTrigger>
<TooltipContent>
<span>{t("landingPage.twitter")}</span>
</TooltipContent>
</Tooltip>
</TooltipProvider>
<span className="sr-only">{t("landingPage.twitter")}</span>
</li>
</a>
<a
href={socialLinks.linkedin}
target="_blank"
rel="noopener noreferrer"
className="text-gray-400 hover:text-primary transition-colors duration-300"
>
<li className="flex items-center space-x-2 text-base text-gray-400 hover:text-primary transition-colors duration-300 cursor-pointer">
<TooltipProvider>
<Tooltip>
<TooltipTrigger asChild>
<Linkedin className="h-6 w-6 hover:text-primary" />
</TooltipTrigger>
<TooltipContent>
<span>{t("landingPage.linkedIn")}</span>
</TooltipContent>
</Tooltip>
</TooltipProvider>
<span className="sr-only">{t("landingPage.linkedIn")}</span>
</li>
</a>
{optionalSocialLinks.map(({ href, label, icon: Icon }) => (
<a
key={label}
href={href}
target="_blank"
rel="noopener noreferrer"
className="text-gray-400 hover:text-primary transition-colors duration-300"
>
<li className="flex items-center space-x-2 text-base text-gray-400 hover:text-primary transition-colors duration-300 cursor-pointer">
<TooltipProvider>
<Tooltip>
<TooltipTrigger asChild>
<Icon className="h-6 w-6 hover:text-primary" />
</TooltipTrigger>
<TooltipContent>
<span>{label}</span>
</TooltipContent>
</Tooltip>
</TooltipProvider>
<span className="sr-only">{label}</span>
</li>
</a>
))}
<a
href={socialLinks.website}
target="_blank"
rel="noopener noreferrer"
className="text-gray-400 hover:text-primary transition-colors duration-300"
>
<li className="flex items-center space-x-2 text-base text-gray-400 hover:text-primary transition-colors duration-300 cursor-pointer">
<TooltipProvider>
<Tooltip>
<TooltipTrigger asChild>
<Globe className="h-6 w-6 hover:text-primary" />
</TooltipTrigger>
<TooltipContent>
<span>{t("landingPage.website")}</span>
</TooltipContent>
</Tooltip>
</TooltipProvider>
<span className="sr-only">{t("landingPage.website")}</span>
</li>
</a>
</div>
</div>
<div className="mt-12 xl:mt-0 xl:col-span-2">
<div className="grid grid-cols-1 md:grid-cols-2 gap-8">
<div>
<h3 className="text-sm font-semibold text-gray-300 tracking-wider uppercase">
{t("landingPage.contactUs")}
</h3>
<ul className="mt-4 space-y-4">
<li
onClick={handleEmailClick}
title={footerData.email || "sales@triaplc.com"}
className="flex items-center space-x-3 text-base text-gray-400 hover:text-primary transition-colors duration-300 cursor-pointer"
>
<Mail className="h-5 w-5 hover:text-primary" />
<span className="font-medium">
{footerData.email || "sales@triaplc.com"}
</span>
</li>
<li className="flex items-center space-x-3 text-base text-gray-400 hover:text-primary transition-colors duration-300 cursor-pointer">
<Phone className="h-5 w-5 hover:text-primary" />
<span className="font-medium">
{footerData.phone || "+251-955-232323"}
</span>
</li>
<li
onClick={handleAddressClick}
title={
footerData.address ||
"XQXP+J6C Lingo Tower, Namibia St, Addis Ababa"
}
className="flex items-center space-x-3 text-base text-gray-400 hover:text-primary transition-colors duration-300 cursor-pointer"
>
<MapPin className="h-5 w-5 hover:text-primary" />
<span className="font-medium">
{footerData.address || t("landingPage.address")}
</span>
</li>
</ul>
</div>
<div>
<h3 className="text-sm font-semibold text-gray-300 tracking-wider uppercase">
{t("landingPage.quickLinks")}
</h3>
<ul className="mt-4 space-y-4">
{navItems.map((item) => (
<button
key={item.id}
onClick={() => {
setActiveTab(item.id);
const el = document.getElementById(item.id);
if (el) {
el.scrollIntoView({
behavior: "smooth",
block: "start",
});
}
}}
className={`cursor-pointer text-base text-gray-400 hover:text-primary transition-colors duration-300 flex items-center ${
activeTab === item.id
? "text-gray-400"
: "text-gray-400"
}`}
>
{item.label}
{activeTab === item.id && (
<motion.div
layoutId="activeTabIndicator"
className="absolute bottom-0 left-0 right-0 h-0.5 bg-primary"
transition={{
type: "spring",
bounce: 0.2,
duration: 0.6,
}}
/>
)}
</button>
))}
</ul>
</div>
</div>
</div>
</div>
<div className="mt-12 border-t border-gray-700 pt-8 flex flex-col items-center justify-center text-center">
<p className="text-base text-gray-400">
&copy; {t("landingPage.copyright")}
{new Date().getFullYear()} {t("landingPage.rightsReserved")}
</p>
</div>
</div>
</footer>
);
};
export default Footer;

View File

@@ -0,0 +1,703 @@
import { useState, useEffect, useMemo } from "react";
import { motion, AnimatePresence } from "framer-motion";
import { useNavigate, useLocation } from "react-router-dom";
import { ExternalPortal } from "@/external-portal/components/External-Portal-Navigation/PortalHeader";
import { useTranslation } from "react-i18next";
import {
Select,
SelectContent,
SelectItem,
SelectTrigger,
SelectValue,
} from "@/shared/common/ui/select";
import { toast } from "sonner";
import {
ChevronDown,
FileText,
Search,
Info,
Menu,
X,
Moon,
Sun,
} from "lucide-react";
import { useUser } from "@/shared/context/UserContext";
import { useDarkMode } from "@/shared/hooks/useDarkMode";
import { useTenantConfig, resolveModuleConfig } from "@/layout/components/TenantConfig";
import {
UI_LANGUAGE_OPTIONS,
resolveUiLanguage,
} from "@/shared/i18n/uiLanguages";
import {
hasComplaintVerification,
isComplaintAuthContext,
} from "@/complaints/utils/complaintVerificationStorage";
import { COMPLAINT_RECORDS_PATH } from "@/complaints/utils/complaintRoutes";
import { useExternalPortalSession } from "@/shared/hooks/useExternalPortalSession";
interface NavItem {
id: string;
label: string;
path?: string;
requiresCompleteRegistration?: true;
children?: NavItem[];
}
const languageOptions = UI_LANGUAGE_OPTIONS;
const Header = () => {
const [activeTab, setActiveTab] = useState("overview");
const [scrolled, setScrolled] = useState(false);
const [mobileMenuOpen, setMobileMenuOpen] = useState(false);
const [servicesOpen, setServicesOpen] = useState(false);
const [navigateTo, setNaviageTo] = useState("/");
const navigate = useNavigate();
const location = useLocation();
const isComplaintContext = isComplaintAuthContext(location.pathname);
const { isAuthenticated, showExternalPortalChrome } = useExternalPortalSession();
const { isDarkMode, toggleDarkMode } = useDarkMode();
const { config: tenantConfig } = useTenantConfig();
const moduleConfig = resolveModuleConfig(tenantConfig);
const userDetails = useUser();
const { t, i18n } = useTranslation();
const currentLanguage = resolveUiLanguage(i18n.language);
const hasCompletedRegistration = userDetails?.hasFinishedRegistration;
const changeLanguage = (lng: string) => i18n.changeLanguage(lng);
useEffect(() => {
if (!localStorage.getItem("i18nextLng")) {
i18n.changeLanguage("am");
localStorage.setItem("i18nextLng", "am");
}
}, [i18n]);
useEffect(() => {
const handleScroll = () => {
setScrolled(window.scrollY > 10);
};
window.addEventListener("scroll", handleScroll);
return () => window.removeEventListener("scroll", handleScroll);
}, []);
const handleSignIn = () => {
navigate("/login");
};
useEffect(() => {
if (location.pathname.includes("/external-portal/portal-outgoing")) {
setActiveTab("records");
return;
}
if (location.pathname.startsWith("/complaints")) {
setActiveTab("complaint");
}
}, [location.pathname]);
const navItems: NavItem[] = useMemo(() => {
if (showExternalPortalChrome) {
return [];
}
if (isAuthenticated) {
return [
{ id: "overview", label: t("landingPage.overview") },
{ id: "workflow", label: t("landingPage.workflow") },
];
}
const complaintServices: NavItem[] = moduleConfig.complaint
? [
{
id: "complaint",
label: t("landingPage.fileComplaint"),
path: "/complaints",
},
{
id: "follow-complaint",
label: t("landingPage.followComplaint"),
path: "/follow-complaint",
},
]
: [];
const items: NavItem[] = [
{ id: "overview", label: t("landingPage.overview") },
{ id: "features", label: t("landingPage.features") },
];
if (complaintServices.length > 0) {
items.push({
id: "services",
label: t("landingPage.services"),
children: complaintServices,
});
}
items.push(
{ id: "workflow", label: t("landingPage.workflow") },
{ id: "howto", label: t("landingPage.manual") },
);
return items;
}, [isAuthenticated, moduleConfig.complaint, showExternalPortalChrome, t]);
const handleNavClick = (item: NavItem) => {
if (
item.requiresCompleteRegistration &&
!hasCompletedRegistration &&
!hasComplaintVerification()
) {
toast.error(t("registration.registrationRequired"));
return;
}
if (item.path) {
navigate(item.path);
setMobileMenuOpen(false);
}
};
// Animation variants
const mobileMenuVariants = {
hidden: {
opacity: 0,
height: 0,
transition: {
duration: 0.3,
when: "afterChildren",
},
},
visible: {
opacity: 1,
height: "auto",
transition: {
duration: 0.3,
when: "beforeChildren",
staggerChildren: 0.1,
},
},
};
const mobileItemVariants = {
hidden: { x: -20, opacity: 0 },
visible: {
x: 0,
opacity: 1,
transition: {
x: { stiffness: 1000, velocity: -100 },
},
},
};
const servicesVariants = {
hidden: {
opacity: 0,
y: -10,
scale: 0.95,
transition: {
duration: 0.2,
},
},
visible: {
opacity: 1,
y: 0,
scale: 1,
transition: {
duration: 0.2,
staggerChildren: 0.05,
},
},
};
const serviceItemVariants = {
hidden: { opacity: 0, y: -5 },
visible: { opacity: 1, y: 0 },
};
const getServiceIcon = (id: string) => {
switch (id) {
case "complaint":
return <FileText className="w-4 h-4" />;
case "follow-complaint":
return <Search className="w-4 h-4" />;
case "about-us":
return <Info className="w-4 h-4" />;
default:
return <FileText className="w-4 h-4" />;
}
};
// Get current language display name
const getCurrentLanguageDisplay = () => {
const currentLang = languageOptions.find(
(lang) => lang.value === currentLanguage,
);
return currentLang ? currentLang.label : "English";
};
useMemo(() => {
if (showExternalPortalChrome) {
setNaviageTo(COMPLAINT_RECORDS_PATH);
}
}, [showExternalPortalChrome]);
return (
<>
{/* Registration Alert - Fixed for mobile */}
<nav
className={`fixed top-0 left-0 right-0 z-50 transition-all duration-300 ${
scrolled
? "bg-white dark:bg-gray-900 shadow-lg"
: "bg-white dark:bg-gray-900 shadow-sm"
} `}
>
<div className="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8">
<div className="flex justify-between items-center gap-4 h-16 md:h-20">
{/* Logo Section */}
<div className="flex items-center">
{!showExternalPortalChrome ? (
<>
{tenantConfig?.logo ? (
<div className="relative group inline-flex">
{/* Glow */}
<div className="absolute inset-0 rounded-2xl bg-gradient-to-r from-primary/20 to-secondary/20 blur-xl opacity-0 group-hover:opacity-100 transition-all duration-500 scale-110" />
{/* Footer Logo Wrapper */}
<motion.div
whileHover={{ scale: 1.04 }}
whileTap={{ scale: 0.97 }}
transition={{ type: "spring", stiffness: 220, damping: 16 }}
className="relative inline-flex items-center justify-center cursor-pointer group"
onClick={() =>
navigate(
showExternalPortalChrome
? COMPLAINT_RECORDS_PATH
: isComplaintContext
? "/complaints"
: "/",
)
}
title={t("nav.homePage")}
>
<motion.img
src={tenantConfig.logo}
alt="Footer Logo"
className="
h-15 md:h-30
w-auto
object-contain
group-hover:scale-105
transition-transform
duration-300
"
onError={(e) => {
const fallback =
e.currentTarget.parentElement?.querySelector(
".footer-logo-fallback",
);
if (fallback) {
fallback.classList.remove("hidden");
}
e.currentTarget.remove();
}}
/>
{/* Fallback when logo fails to load */}
<div
className="
footer-logo-fallback
hidden
h-20
w-20
items-center
justify-center
rounded-full
bg-primary
text-primary-foreground
font-bold
text-2xl
pointer-events-none
"
>
{tenantConfig?.organizationName
?.split(/[\s\-–—]+/)
.filter(Boolean)
.map((word) => word.charAt(0))
.join("")
.toUpperCase() || "SO"}
</div>
</motion.div>
</div>
) : (
<motion.div
whileHover={{ scale: 1.05 }}
whileTap={{ scale: 0.95 }}
className="h-10 w-10 md:h-12 md:w-12 rounded-full bg-primary flex items-center justify-center text-white font-bold cursor-pointer"
onClick={() => navigate(navigateTo)}
title={t("nav.homePage")}
>
SO
</motion.div>
)}
<motion.span
initial={{ opacity: 0, x: -10 }}
animate={{ opacity: 1, x: 0 }}
className="ml-2 md:ml-3 text-lg md:text-xl font-bold text-gray-800 dark:text-white cursor-pointer"
onClick={() => navigate(navigateTo)}
title={t("nav.homePage")}
>
{tenantConfig?.organizationName ||
tenantConfig?.organizationName == ""
? tenantConfig?.organizationName
: t("landingPage.smartOffice")}
</motion.span>
</>
) : null}
{/* Desktop Navigation */}
<div className="hidden md:ml-6 lg:ml-10 md:flex md:space-x-4 lg:space-x-6">
{navItems.map((item) => (
<div key={item.id} className="relative">
{item.children ? (
<div
className="relative"
onMouseEnter={() => setServicesOpen(true)}
onMouseLeave={() => setServicesOpen(false)}
>
<button
onClick={() => setServicesOpen(!servicesOpen)}
className={`relative inline-flex items-center px-1 pt-1 text-sm lg:text-base font-medium transition-colors duration-200 cursor-pointer ${
activeTab === item.id
? "text-gray-900 dark:text-white"
: "text-gray-600 dark:text-gray-300 hover:text-gray-800 dark:hover:text-white hover:underline"
}`}
>
{item.label}
<ChevronDown
className={`ml-1 w-4 h-4 transition-transform ${
servicesOpen ? "rotate-180" : ""
}`}
/>
</button>
<AnimatePresence>
{servicesOpen && (
<motion.div
initial="hidden"
animate="visible"
exit="hidden"
variants={servicesVariants}
className="absolute top-full left-0 mt-2 w-64 bg-white rounded-lg shadow-xl border border-gray-200 z-50 overflow-hidden"
>
<div className="py-2">
{item.children.map((child) => (
<motion.button
key={child.id}
variants={serviceItemVariants}
onClick={() => handleNavClick(child)}
className="w-full flex items-center px-4 py-3 text-sm text-gray-700 hover:bg-blue-50 hover:text-blue-700 transition-colors duration-200"
>
<span className="mr-3 text-primary">
{getServiceIcon(child.id)}
</span>
{child.label}
</motion.button>
))}
</div>
</motion.div>
)}
</AnimatePresence>
</div>
) : (
<button
onClick={() => {
setActiveTab(item.id);
if (item.path) {
handleNavClick(item);
} else {
const el = document.getElementById(item.id);
if (el) {
el.scrollIntoView({
behavior: "smooth",
block: "start",
});
}
}
}}
className={`relative inline-flex items-center px-1 pt-1 text-sm lg:text-base font-medium transition-colors duration-200 cursor-pointer ${
(item.path
? location.pathname.startsWith(item.path)
: activeTab === item.id)
? "text-gray-900 dark:text-white"
: "text-gray-600 dark:text-gray-300 hover:text-gray-800 dark:hover:text-white hover:underline"
}`}
>
{item.label}
{(item.path
? location.pathname.startsWith(item.path)
: activeTab === item.id) && (
<motion.div
layoutId="activeTabIndicator"
className="absolute bottom-0 left-0 right-0 h-0.5 bg-primary"
transition={{
type: "spring",
bounce: 0.2,
duration: 0.6,
}}
/>
)}
</button>
)}
</div>
))}
</div>
</div>
{/* Desktop Sign Up Button */}
{!isAuthenticated && (
<div className="hidden md:flex items-center space-x-2 lg:space-x-4">
<div className="w-28 lg:w-32">
<Select
value={currentLanguage}
onValueChange={(lng) => changeLanguage(lng)}
>
<SelectTrigger className="w-full text-sm rounded-md border border-gray-300 dark:border-gray-600 dark:bg-gray-700 dark:text-gray-200 px-2 py-1.5 lg:px-3 lg:py-2 shadow-sm focus:outline-none focus:ring-2 focus:ring-primary focus:border-primary">
<SelectValue>{getCurrentLanguageDisplay()}</SelectValue>
</SelectTrigger>
<SelectContent className="dark:bg-gray-800 dark:border-gray-700">
{languageOptions.map((lang) => (
<SelectItem
key={lang.value}
value={lang.value}
className="dark:text-gray-200 dark:hover:bg-gray-700"
>
{lang.label}
</SelectItem>
))}
</SelectContent>
</Select>
</div>
{/* Dark Mode Toggle */}
<motion.button
onClick={toggleDarkMode}
whileHover={{ scale: 1.05 }}
whileTap={{ scale: 0.95 }}
className="p-2 rounded-lg border border-gray-300 dark:border-gray-600 bg-gray-100 dark:bg-gray-700 hover:bg-gray-200 dark:hover:bg-gray-600 transition-colors duration-200"
title={
isDarkMode ? "Switch to light mode" : "Switch to dark mode"
}
>
{isDarkMode ? (
<Sun className="h-5 w-5 text-yellow-500" />
) : (
<Moon className="h-5 w-5 text-gray-600" />
)}
</motion.button>
<motion.button
onClick={handleSignIn}
whileHover={{
scale: 1.03,
boxShadow: "0 4px 12px rgba(24, 170, 157, 0.2)",
}}
whileTap={{ scale: 0.98 }}
className="inline-flex items-center px-3 py-1.5 lg:px-4 lg:py-2.5 border border-transparent text-sm font-medium rounded-lg shadow-sm text-white bg-primary hover:bg-primary-700 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-primary transition-all duration-200 whitespace-nowrap"
>
{t("landingPage.signIn")}
</motion.button>
</div>
)}
{showExternalPortalChrome && (
<div className="hidden md:flex items-center">
<ExternalPortal />
</div>
)}
{/* Mobile Menu Button */}
<div className="flex items-center md:hidden">
<motion.button
onClick={() => setMobileMenuOpen(!mobileMenuOpen)}
className="inline-flex items-center justify-center p-2 rounded-md text-gray-600 hover:text-gray-900 hover:bg-gray-100 focus:outline-none focus:ring-2 focus:ring-inset focus:ring-primary transition-colors duration-200"
aria-expanded="false"
whileTap={{ scale: 0.9 }}
>
<span className="sr-only">Open main menu</span>
{mobileMenuOpen ? (
<X className="h-6 w-6" />
) : (
<Menu className="h-6 w-6" />
)}
</motion.button>
</div>
</div>
</div>
{/* Mobile Menu */}
<AnimatePresence>
{mobileMenuOpen && (
<motion.div
initial="hidden"
animate="visible"
exit="hidden"
variants={mobileMenuVariants}
className="md:hidden overflow-hidden bg-white dark:bg-gray-800 shadow-xl border-t border-gray-200 dark:border-gray-700"
>
<motion.div className="pt-2 pb-4 space-y-1 px-4">
{navItems.map((item) => (
<div key={item.id}>
{item.children ? (
<div>
<button
onClick={() => setServicesOpen(!servicesOpen)}
className={`block w-full text-left pl-3 pr-4 py-3 border-l-4 text-base font-medium transition-all duration-200 ${
activeTab === item.id
? "bg-primary-500/15 border-primary text-primary"
: "border-transparent text-gray-600 hover:bg-gray-50 hover:border-gray-300 hover:text-gray-800"
}`}
>
<div className="flex items-center justify-between">
{item.label}
<ChevronDown
className={`w-4 h-4 transition-transform ${
servicesOpen ? "rotate-180" : ""
}`}
/>
</div>
</button>
<AnimatePresence>
{servicesOpen && (
<motion.div
initial={{ opacity: 0, height: 0 }}
animate={{ opacity: 1, height: "auto" }}
exit={{ opacity: 0, height: 0 }}
className="pl-6 overflow-hidden"
>
{item.children.map((child) => (
<motion.button
key={child.id}
onClick={() => handleNavClick(child)}
className="block w-full text-left pl-3 pr-4 py-2 text-sm text-gray-600 hover:bg-blue-50 hover:text-blue-700 transition-colors duration-200"
>
<div className="flex items-center">
<span className="mr-2 text-primary">
{getServiceIcon(child.id)}
</span>
{child.label}
</div>
</motion.button>
))}
</motion.div>
)}
</AnimatePresence>
</div>
) : (
<motion.button
variants={mobileItemVariants}
onClick={() => {
setActiveTab(item.id);
setMobileMenuOpen(false);
if (item.path) {
handleNavClick(item);
}
}}
whileHover={{ scale: 1.02 }}
whileTap={{ scale: 0.98 }}
className={`block w-full text-left pl-3 pr-4 py-3 border-l-4 text-base font-medium transition-all duration-200 ${
activeTab === item.id
? "bg-primary-500/15 border-primary text-primary"
: "border-transparent text-gray-600 dark:text-gray-300 hover:bg-gray-50 dark:hover:bg-gray-700 hover:border-gray-300 hover:text-gray-800 dark:hover:text-white"
}`}
>
{item.label}
</motion.button>
)}
</div>
))}
{!isAuthenticated && (
<motion.div
variants={mobileItemVariants}
className="pt-4 pb-2 border-t border-gray-200 dark:border-gray-700 space-y-3"
>
<div className="px-2">
<Select
value={currentLanguage}
onValueChange={(lng) => changeLanguage(lng)}
>
<SelectTrigger className="w-full text-sm bg-gray-100 dark:bg-gray-700 border-none rounded-md px-3 py-2.5 shadow-sm focus:outline-none focus:ring-2 focus:ring-primary dark:text-gray-200">
<SelectValue>
{getCurrentLanguageDisplay()}
</SelectValue>
</SelectTrigger>
<SelectContent className="dark:bg-gray-800 dark:border-gray-700">
{languageOptions.map((lang) => (
<SelectItem
key={lang.value}
value={lang.value}
className="dark:text-gray-200 dark:hover:bg-gray-700"
>
{lang.label}
</SelectItem>
))}
</SelectContent>
</Select>
</div>
{/* Dark Mode Toggle for Mobile */}
<motion.button
onClick={toggleDarkMode}
whileHover={{ scale: 1.02 }}
whileTap={{ scale: 0.98 }}
className="block w-full px-4 py-3 text-base font-medium text-center text-gray-700 dark:text-gray-200 bg-gray-100 dark:bg-gray-700 rounded-lg shadow hover:bg-gray-200 dark:hover:bg-gray-600 transition-all duration-200 flex items-center justify-center gap-2"
>
{isDarkMode ? (
<>
<Sun className="h-5 w-5 text-yellow-500" />
{t("landingPage.lightMode")}
</>
) : (
<>
<Moon className="h-5 w-5 text-gray-600" />
{t("landingPage.darkMode")}
</>
)}
</motion.button>
<motion.button
onClick={() => {
handleSignIn();
setMobileMenuOpen(false);
}}
whileHover={{ scale: 1.02 }}
whileTap={{ scale: 0.98 }}
className="block w-full px-4 py-3 text-base font-medium text-center text-white bg-primary rounded-lg shadow hover:bg-primary-700 transition-all duration-200"
>
{t("landingPage.signIn")}
</motion.button>
</motion.div>
)}
{showExternalPortalChrome && (
<motion.div
variants={mobileItemVariants}
className="pt-3 border-t border-gray-200"
>
<ExternalPortal
mobileView={true}
onItemClick={() => setMobileMenuOpen(false)}
/>
</motion.div>
)}
</motion.div>
</motion.div>
)}
</AnimatePresence>
</nav>
</>
);
};
export default Header;

View File

@@ -0,0 +1,560 @@
import { useEffect, useState } from "react";
import { motion, useAnimation, Variants } from "framer-motion";
import { useInView } from "react-intersection-observer";
import { easeInOut } from "framer-motion";
import { useTranslation } from "react-i18next";
import { useNavigate } from "react-router-dom";
import Cookies from "js-cookie";
import { ArrowRight, BookOpenText, ShieldCheck } from "lucide-react";
import {
useTenantConfig,
resolveModuleConfig,
} from "@/layout/components/TenantConfig";
import {
Dialog,
DialogContent,
DialogDescription,
DialogHeader,
DialogTitle,
DialogTrigger,
} from "@/shared/common/ui/dialog";
const HeroSection = () => {
const controls = useAnimation();
const { t } = useTranslation();
const navigate = useNavigate();
const { config: tenantConfig } = useTenantConfig();
const moduleConfig = resolveModuleConfig(tenantConfig);
const primary = tenantConfig.primaryColor || "#18AA9D";
const secondary = tenantConfig.secondaryColor || primary;
const [ref, inView] = useInView({
threshold: 0.1,
triggerOnce: true,
});
useEffect(() => {
if (inView) {
controls.start("visible");
} else {
controls.start("hidden");
}
}, [controls, inView]);
const containerVariants: Variants = {
hidden: { opacity: 0 },
visible: {
opacity: 1,
transition: {
staggerChildren: 0.15,
delayChildren: 0.2,
},
},
};
const itemVariants: Variants = {
hidden: { y: 30, opacity: 0 },
visible: {
y: 0,
opacity: 1,
transition: {
duration: 0.6,
ease: easeInOut,
},
},
};
const isTokenPresent = Boolean(Cookies.get("auth-token"));
const heroTitle =
tenantConfig?.organizationName || t("landingPage.smartOffice");
const welcomeMessage =
tenantConfig?.welcomeMessage || t("landingPage.initiativeDescription");
const organizationInitials =
heroTitle
?.split(/[\s\-–—]+/)
.filter(Boolean)
.map((word) => word.charAt(0))
.join("")
.toUpperCase() || "SO";
const cardVariants: Variants = {
hidden: { scale: 0.85, opacity: 0, rotateX: 10 },
visible: {
scale: 1,
opacity: 1,
rotateX: 0,
transition: {
delay: 0.4,
duration: 0.8,
type: "spring",
stiffness: 100,
damping: 15,
},
},
};
const floatingElementVariants: Variants = {
float: {
y: [0, -20, 0],
transition: {
duration: 4,
repeat: Infinity,
ease: "easeInOut", // ✅ correct literal type
},
},
};
return (
<section className="relative bg-gradient-to-br from-white via-blue-50/30 to-primary-50/50 dark:from-gray-900 dark:via-gray-800 dark:to-gray-900 overflow-x-hidden min-h-screen flex items-start pt-8">
{/* Enhanced Background */}
<div className="absolute inset-0 overflow-hidden">
{/* Gradient Orbs */}
<div className="absolute -top-40 -left-40 w-80 h-80 bg-gradient-to-r from-primary to-primary-500 rounded-full mix-blend-multiply filter blur-3xl opacity-15 animate-orb-slow dark:opacity-10"></div>
<div className="absolute -top-20 -right-20 w-96 h-96 bg-gradient-to-r from-primary-500 to-primary rounded-full mix-blend-multiply filter blur-3xl opacity-10 animate-orb-medium dark:opacity-5"></div>
<div className="absolute -bottom-40 left-1/3 w-72 h-72 bg-gradient-to-r from-primary-700 to-primary-500 rounded-full mix-blend-multiply filter blur-3xl opacity-20 animate-orb-fast dark:opacity-10"></div>
{/* Grid Pattern */}
<div className="absolute inset-0 bg-[linear-gradient(rgba(24,170,157,0.03)_1px,transparent_1px),linear-gradient(90deg,rgba(24,170,157,0.03)_1px,transparent_1px)] bg-[size:60px_60px] [mask-image:radial-gradient(ellipse_80%_50%_at_50%_50%,black,transparent)] dark:bg-[linear-gradient(rgba(24,170,157,0.05)_1px,transparent_1px),linear-gradient(90deg,rgba(24,170,157,0.05)_1px,transparent_1px)]"></div>
</div>
<div className="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8 relative z-10 pt-12 lg:pt-20">
<div className="grid grid-cols-1 lg:grid-cols-2 gap-16 lg:gap-20 items-start">
{/* Enhanced Content */}
<motion.div
ref={ref}
initial="hidden"
animate={controls}
variants={containerVariants}
className="space-y-8 lg:space-y-10"
>
{!isTokenPresent && (
<motion.div
variants={itemVariants}
className="inline-flex items-center px-4 py-2 rounded-full bg-gradient-to-r from-primary/10 to-primary-500/10 border border-primary/20 text-primary text-sm font-medium mb-4"
>
<span className="relative flex h-2 w-2 mr-2">
<span className="animate-ping absolute inline-flex h-full w-full rounded-full bg-primary opacity-75"></span>
<span className="relative inline-flex rounded-full h-2 w-2 bg-primary"></span>
</span>
{"Online"}
</motion.div>
)}
<motion.h1
variants={itemVariants}
className="text-4xl sm:text-5xl md:text-6xl lg:text-7xl font-bold tracking-tight text-gray-900 dark:text-white leading-tight"
>
<span className="block bg-gradient-to-r from-gray-900 to-gray-700 dark:from-white dark:to-gray-300 bg-clip-text text-transparent">
{tenantConfig?.organizationName || t("landingPage.smartOffice")}
</span>
<span className="block text-transparent bg-clip-text bg-gradient-to-r from-primary via-primary-500 to-primary-500">
{t("landingPage.platformSystem")}
</span>
</motion.h1>
<motion.p
variants={itemVariants}
className="text-lg md:text-xl text-gray-600 dark:text-gray-300 max-w-lg leading-relaxed"
>
{welcomeMessage}
</motion.p>
<motion.div variants={itemVariants}>
<Dialog>
<DialogTrigger asChild>
<button className="group inline-flex items-center gap-3 rounded-full border border-primary/25 bg-white/75 px-5 py-3 text-sm font-semibold text-primary shadow-sm shadow-primary/10 backdrop-blur-md transition-all duration-300 hover:-translate-y-0.5 hover:border-primary/45 hover:bg-primary/10 hover:shadow-lg hover:shadow-primary/15 focus:outline-none focus:ring-2 focus:ring-primary/40 focus:ring-offset-2 dark:bg-gray-900/60 dark:hover:bg-primary/15 dark:focus:ring-offset-gray-900">
<span className="flex size-9 items-center justify-center rounded-full bg-primary/10 text-primary transition-colors duration-300 group-hover:bg-primary group-hover:text-white">
<BookOpenText className="size-4" />
</span>
<span>{t("newssection.readMore")}</span>
<ArrowRight className="size-4 transition-transform duration-300 group-hover:translate-x-1" />
</button>
</DialogTrigger>
<DialogContent className="max-h-[86vh] max-w-3xl overflow-hidden rounded-2xl border border-primary/15 bg-white/95 p-0 shadow-2xl shadow-primary/10 backdrop-blur-xl dark:border-primary/20 dark:bg-gray-900/95">
<div className="h-1.5 bg-gradient-to-r from-primary via-primary-500 to-primary" />
<DialogHeader className="relative gap-0 px-6 pb-5 pt-6 text-left sm:px-8">
<div className="absolute right-8 top-8 hidden h-24 w-24 rounded-full bg-primary/10 blur-2xl sm:block" />
<div className="relative flex items-start gap-4">
{tenantConfig?.logo ? (
<div className="relative group inline-flex shrink-0">
<div className="absolute inset-0 rounded-2xl bg-gradient-to-r from-primary/20 to-secondary/20 blur-xl opacity-0 transition-all duration-500 scale-110 group-hover:opacity-100" />
<motion.div
whileHover={{ scale: 1.04 }}
whileTap={{ scale: 0.97 }}
transition={{
type: "spring",
stiffness: 220,
damping: 16,
}}
className="relative flex h-16 w-24 items-center justify-center overflow-hidden rounded-2xl border-none bg-white/5 shadow-md dark:bg-slate-900/40"
title={heroTitle}
>
<motion.img
src={tenantConfig.logo}
alt={`${heroTitle} logo`}
className="h-full w-full object-contain drop-shadow-lg transition-transform duration-300 group-hover:scale-105"
onError={(e) => {
const fallback =
e.currentTarget.parentElement?.querySelector(
".modal-logo-fallback",
);
if (fallback) {
fallback.classList.remove("hidden");
fallback.classList.add("flex");
}
e.currentTarget.remove();
}}
/>
<div className="modal-logo-fallback absolute inset-0 hidden items-center justify-center rounded-2xl bg-gradient-to-br from-primary to-primary-500 text-xl font-bold text-white">
{organizationInitials || "SO"}
</div>
<div className="absolute inset-0 -translate-x-full bg-gradient-to-r from-transparent via-white/15 to-transparent transition-transform duration-1000 group-hover:translate-x-full" />
</motion.div>
</div>
) : (
<div className="relative shrink-0">
<div className="absolute inset-0 rounded-2xl bg-primary/20 blur-xl scale-110" />
<div className="relative flex h-16 w-24 items-center justify-center overflow-hidden rounded-2xl bg-gradient-to-br from-primary to-primary-500 text-xl font-bold text-white shadow-lg shadow-primary/25 ring-1 ring-white/20">
<span className="drop-shadow-sm">
{organizationInitials || "SO"}
</span>
<div className="absolute inset-x-0 top-0 h-1/2 bg-white/15" />
</div>
</div>
)}
<div className="min-w-0 pt-1">
<DialogTitle className="text-2xl font-bold leading-tight text-gray-950 dark:text-white sm:text-3xl">
{heroTitle}
</DialogTitle>
<DialogDescription className="sr-only">
{welcomeMessage}
</DialogDescription>
<div className="mt-3 h-1 w-20 rounded-full bg-gradient-to-r from-primary to-primary-500" />
</div>
</div>
</DialogHeader>
<div className="border-t border-gray-100 bg-gradient-to-b from-gray-50/80 to-white px-6 py-6 dark:border-gray-800 dark:from-gray-950/40 dark:to-gray-900 sm:px-8">
<div className="rounded-xl border border-gray-100 bg-white p-5 text-base leading-8 text-gray-700 shadow-sm dark:border-gray-800 dark:bg-gray-900/80 dark:text-gray-300 sm:p-6 sm:text-lg">
<p className="whitespace-pre-line">{welcomeMessage}</p>
</div>
</div>
</DialogContent>
</Dialog>
</motion.div>
<motion.div
variants={itemVariants}
className="flex flex-col sm:flex-row flex-wrap gap-4 pt-2"
>
{moduleConfig.complaint && (
<motion.button
type="button"
whileHover={{
scale: 1.05,
boxShadow: `0 20px 40px ${primary}4d`,
}}
whileTap={{ scale: 0.95 }}
onClick={() => navigate("/complaints")}
className="group relative px-8 py-4 rounded-xl bg-gradient-to-r from-primary to-primary-500 text-white font-semibold text-lg shadow-lg hover:shadow-xl transition-all duration-300 transform hover:-translate-y-1 overflow-hidden"
>
<div className="absolute inset-0 bg-gradient-to-r from-white/20 to-transparent opacity-0 group-hover:opacity-100 transition-opacity duration-300"></div>
<span className="relative flex items-center justify-center">
{t("complaint.fayda.submitComplaint")}
<ShieldCheck className="w-5 h-5 ml-2" />
</span>
</motion.button>
)}
{!isTokenPresent && (
<motion.a
whileHover={{
scale: 1.05,
boxShadow: `0 20px 40px ${primary}4d`,
}}
whileTap={{ scale: 0.95 }}
href="/login"
className="group relative px-8 py-4 rounded-xl bg-gradient-to-r from-primary to-primary-500 text-white font-semibold text-lg shadow-lg hover:shadow-xl transition-all duration-300 transform hover:-translate-y-1 overflow-hidden"
>
<div className="absolute inset-0 bg-gradient-to-r from-white/20 to-transparent opacity-0 group-hover:opacity-100 transition-opacity duration-300"></div>
<span className="relative flex items-center justify-center">
{t("landingPage.signIn")}
<svg
className="ml-2 w-4 h-4 group-hover:translate-x-1 transition-transform"
fill="none"
stroke="currentColor"
viewBox="0 0 24 24"
>
<path
strokeLinecap="round"
strokeLinejoin="round"
strokeWidth={2}
d="M13 7l5 5m0 0l-5 5m5-5H6"
/>
</svg>
</span>
</motion.a>
)}
<motion.a
whileHover={{
scale: 1.05,
backgroundColor: "rgba(24, 170, 157, 0.08)",
}}
whileTap={{ scale: 0.95 }}
href="#features"
className="group px-8 py-4 rounded-xl border-2 border-primary text-primary font-semibold text-lg hover:shadow-lg transition-all duration-300 flex items-center justify-center"
>
{t("landingPage.learnMore")}
<svg
className="ml-2 w-4 h-4 group-hover:translate-y-0.5 transition-transform"
fill="none"
stroke="currentColor"
viewBox="0 0 24 24"
>
<path
strokeLinecap="round"
strokeLinejoin="round"
strokeWidth={2}
d="M19 14l-7 7m0 0l-7-7m7 7V3"
/>
</svg>
</motion.a>
</motion.div>
{/* Stats */}
<motion.div
variants={itemVariants}
className="flex flex-wrap gap-8 pt-5 pb-10"
>
<div className="text-center">
<div className="text-2xl font-bold text-gray-900 dark:text-white">
95%
</div>
<div className="text-sm text-gray-600 dark:text-gray-400 mt-1">
{t("landingPage.uptime")}
</div>
</div>
<div className="text-center">
<div className="text-2xl font-bold text-gray-900 dark:text-white">
25+
</div>
<div className="text-sm text-gray-600 dark:text-gray-400 mt-1">
{t("landingPage.bureaus")}
</div>
</div>
<div className="text-center">
<div className="text-2xl font-bold text-gray-900 dark:text-white">
24/7
</div>
<div className="text-sm text-gray-600 dark:text-gray-400 mt-1">
{t("landingPage.support")}
</div>
</div>
</motion.div>
</motion.div>
{/* Enhanced Card */}
<motion.div
initial="hidden"
animate={controls}
variants={cardVariants}
className="relative"
>
<div className="absolute -inset-4 bg-gradient-to-r from-primary to-primary-500 rounded-3xl opacity-20 blur-xl animate-pulse-slow"></div>
<div className="absolute -inset-2 bg-gradient-to-r from-primary to-primary-500 rounded-2xl opacity-10 blur-lg"></div>
<motion.div
whileHover={{ y: -5, rotateX: 5 }}
transition={{ type: "spring", stiffness: 300, damping: 20 }}
className="relative h-full bg-white/80 dark:bg-gray-800/80 backdrop-blur-sm rounded-2xl shadow-2xl border border-white/20 dark:border-gray-700 overflow-hidden"
>
{/* Card Header */}
<div className="absolute top-0 left-0 right-0 h-2 bg-gradient-to-r from-primary to-primary-500"></div>
<div className="p-8 lg:p-10 h-full flex flex-col items-center justify-center bg-gradient-to-br from-white/90 to-gray-50/80 dark:from-gray-800/90 dark:to-gray-700/80">
<div className="text-center space-y-6">
{/* Logo/Brand */}
<div className="mb-6">
<div
className="w-16 h-16 mx-auto mb-4 rounded-2xl shadow-lg flex items-center justify-center"
style={{
background: `linear-gradient(135deg, ${primary}, ${secondary})`,
}}
>
<span className="text-white font-bold text-xl">SO</span>
</div>
</div>
<div className="text-4xl lg:text-5xl font-bold mb-2 tracking-tight text-slate-900 dark:text-white">
{t("landingPage.sops")}
</div>
<div className="text-lg font-semibold text-gray-800 dark:text-gray-200">
{tenantConfig.appName}
</div>
<div className="text-gray-600 dark:text-gray-400 max-w-md leading-relaxed">
{t("landingPage.revolutionDescription")}
</div>
{/* Features List */}
<div className="grid grid-cols-2 gap-4 pt-4">
{[
"features1",
"features2",
"features3",
"features4",
"features5",
"features6",
].map((feature, index) => (
<motion.div
key={feature}
initial={{ opacity: 0, x: -10 }}
animate={{ opacity: 1, x: 0 }}
transition={{ delay: 0.8 + index * 0.1 }}
className="flex items-center text-sm text-gray-600 dark:text-gray-400"
>
<div
className="w-2 h-2 rounded-full mr-2"
style={{ backgroundColor: primary }}
></div>
{t(`landingPage.${feature}`)}
</motion.div>
))}
</div>
<div className="pt-6">
<motion.div
whileHover={{ scale: 1.05 }}
className="inline-flex items-center px-6 py-3 rounded-full font-semibold backdrop-blur-sm"
style={{
background: `linear-gradient(to right, ${primary}1a, ${secondary}1a)`,
color: primary,
border: `1px solid ${primary}33`,
}}
>
<span className="relative flex h-3 w-3 mr-3">
<span
className="animate-ping absolute inline-flex h-full w-full rounded-full opacity-75"
style={{ backgroundColor: primary }}
></span>
<span
className="relative inline-flex rounded-full h-3 w-3"
style={{ backgroundColor: primary }}
></span>
</span>
{t("landingPage.liveDemo")}
</motion.div>
</div>
</div>
</div>
</motion.div>
</motion.div>
</div>
</div>
{/* Enhanced Floating Elements */}
<motion.div
variants={floatingElementVariants}
animate="float"
className="hidden lg:block absolute bottom-20 left-20"
>
<div className="w-12 h-12 rounded-2xl bg-gradient-to-br from-primary/20 to-primary-500/20 border border-primary/10 backdrop-blur-sm rotate-45"></div>
</motion.div>
<motion.div
variants={floatingElementVariants}
animate="float"
transition={{ delay: 1 }}
className="hidden lg:block absolute top-32 right-32"
>
<div className="w-16 h-16 rounded-full bg-gradient-to-br from-primary-500/15 to-primary/15 border border-primary-500/10 backdrop-blur-sm"></div>
</motion.div>
<motion.div
variants={floatingElementVariants}
animate="float"
transition={{ delay: 2 }}
className="hidden lg:block absolute top-1/2 left-1/4"
>
<div className="w-8 h-8 rounded-lg bg-gradient-to-br from-primary-700/10 to-primary-500/10 border border-primary-700/10 backdrop-blur-sm rotate-12"></div>
</motion.div>
{/* Scroll Indicator */}
<motion.div
initial={{ opacity: 0 }}
animate={{ opacity: 1 }}
transition={{ delay: 2 }}
className="absolute bottom-8 left-1/2 transform -translate-x-1/2 hidden lg:block"
>
<motion.div
animate={{ y: [0, 10, 0] }}
transition={{ duration: 2, repeat: Infinity }}
className="w-6 h-10 border-2 border-gray-300 rounded-full flex justify-center"
>
<motion.div
animate={{ y: [0, 12, 0] }}
transition={{ duration: 2, repeat: Infinity }}
className="w-1 h-3 bg-gray-400 rounded-full mt-2"
></motion.div>
</motion.div>
</motion.div>
<style>
{`
@keyframes orb-slow {
0%, 100% {
transform: translate(0px, 0px) scale(1);
}
33% {
transform: translate(40px, -60px) scale(1.1);
}
66% {
transform: translate(-30px, 30px) scale(0.9);
}
}
@keyframes orb-medium {
0%, 100% {
transform: translate(0px, 0px) scale(1);
}
33% {
transform: translate(-50px, 40px) scale(1.05);
}
66% {
transform: translate(20px, -20px) scale(0.95);
}
}
@keyframes orb-fast {
0%, 100% {
transform: translate(0px, 0px) scale(1);
}
50% {
transform: translate(20px, -40px) scale(1.08);
}
}
.animate-orb-slow {
animation: orb-slow 15s infinite ease-in-out;
}
.animate-orb-medium {
animation: orb-medium 12s infinite ease-in-out;
}
.animate-orb-fast {
animation: orb-fast 10s infinite ease-in-out;
}
@keyframes pulse-slow {
0%, 100% {
opacity: 0.2;
}
50% {
opacity: 0.3;
}
}
.animate-pulse-slow {
animation: pulse-slow 4s infinite ease-in-out;
}
`}
</style>
</section>
);
};
export default HeroSection;

View File

@@ -0,0 +1,609 @@
import { useState } from "react";
import { motion, AnimatePresence } from "framer-motion";
import { useNavigate } from "react-router-dom";
import { useTranslation } from "react-i18next";
const RecordManagementGuide = () => {
const [activeTab, setActiveTab] = useState("dashboard");
const { t } = useTranslation();
const [expandedStep, setExpandedStep] = useState<string | null>(null);
const navigate = useNavigate();
const handleTabChange = (tab: string) => {
setActiveTab(tab);
setExpandedStep(null);
//window.scrollTo({ top: 0, behavior: "smooth" });
};
const toggleStep = (stepId: string) => {
setExpandedStep(expandedStep === stepId ? null : stepId);
};
const [openStatus, setOpenStatus] = useState<string | null>(null);
const toggle = (status: string) => {
setOpenStatus((prev) => (prev === status ? null : status));
};
const steps = [
{
id: "login",
title: t("landingPage.login"),
content: (
<div className="space-y-4">
<p className="dark:text-gray-300">
{t("landingPage.step1")}{" "}
<span className="font-semibold text-primary">
{t("landingPage.signIn")}
</span>{" "}
{t("landingPage.headerBtn")}
</p>
<p className="dark:text-gray-300">{t("landingPage.step2")}</p>
<p className="dark:text-gray-300">
{t("landingPage.step3")}{" "}
<span className="font-semibold text-primary">
{t("auth.login")}
</span>{" "}
{t("landingPage.accessBtn")}
</p>
<div className="bg-gray-50 dark:bg-gray-800 p-4 rounded-lg border border-gray-200 dark:border-gray-700 mt-4">
<p className="text-sm text-gray-600 dark:text-gray-300">
<span className="font-semibold">{t("landingPage.note")}</span>{" "}
{t("landingPage.forgotIntro")}
</p>
</div>
</div>
),
image: "/images/login-screen.png",
},
{
id: "dashboard",
title: t("landingPage.dashboardTitle"),
content: (
<div className="space-y-4">
<p className="dark:text-gray-300">{t("landingPage.dashboardDesc")}</p>
<ul className="list-disc pl-5 space-y-2 dark:text-gray-300">
<li>
<span className="font-semibold">
{t("landingPage.recordsCreated")}:
</span>{" "}
{t("landingPage.recordsTotal")}
</li>
<li>
<span className="font-semibold">
{t("landingPage.recordsReceived")}:
</span>{" "}
{t("landingPage.incomingDocs")}
</li>
<li>
<span className="font-semibold">
{t("landingPage.breakdown")}:
</span>{" "}
{t("landingPage.visuals")}
</li>
<li>
<span className="font-semibold">
{t("landingPage.approvalStatus")}:
</span>{" "}
{t("landingPage.approvalTypes")}
</li>
</ul>
<div className="grid grid-cols-1 md:grid-cols-2 gap-4 mt-4">
<div className="bg-white dark:bg-gray-800 p-4 rounded-lg border border-gray-200 dark:border-gray-700 shadow-sm">
<h4 className="font-medium text-gray-900 dark:text-white">
{t("landingPage.quickActions")}
</h4>
<p className="text-sm text-gray-600 dark:text-gray-300 mt-2">
{t("landingPage.createOrCheck")}
</p>
</div>
<div className="bg-white dark:bg-gray-800 p-4 rounded-lg border border-gray-200 dark:border-gray-700 shadow-sm">
<h4 className="font-medium text-gray-900 dark:text-white">
{t("landingPage.recentActivity")}
</h4>
<p className="text-sm text-gray-600 dark:text-gray-300 mt-2">
{t("landingPage.trackUpdates")}
</p>
</div>
</div>
</div>
),
image: "/images/dashboard-screen.png",
},
{
id: "outgoing",
title: t("landingPage.manageOut"),
content: (
<div className="space-y-6">
<div>
<h4 className="font-semibold text-gray-900 dark:text-white">
{t("landingPage.createNew")}
</h4>
<ol className="list-decimal pl-5 space-y-2 mt-2 dark:text-gray-300">
<li>
{t("landingPage.goToTab")}{" "}
<span className="font-semibold text-primary">
{t("statusBar.Outgoing")}
</span>{" "}
{t("landingPage.tab")}
</li>
<li>
{t("landingPage.click")}{" "}
<span className="font-semibold text-primary">
{t("userRecord.Add Record")}
</span>{" "}
{t("landingPage.button")}
</li>
<li>{t("landingPage.fillFields")}</li>
<li>{t("landingPage.attach")}</li>
<li>{t("landingPage.saveOrSubmit")}</li>
</ol>
</div>
<div>
<h4 className="font-semibold text-gray-900 dark:text-white">
{t("landingPage.trackStatus")}
</h4>
<div className="mt-2 grid grid-cols-2 md:grid-cols-4 gap-3">
{[
{
status: "Draft",
color: "bg-gray-200 dark:bg-gray-700",
detail: t("landingPage.notForwarded"),
},
{
status: "Submitted",
color: "bg-purple-100 dark:bg-purple-900/50",
detail: t("landingPage.awaiting"),
},
{
status: "Accepted",
color: "bg-primary-100 dark:bg-primary-900/50",
detail: t("landingPage.accepted"),
},
{
status: "Approved",
color: "bg-primary-200 dark:bg-primary-800/50",
detail: t("landingPage.approvedSent"),
},
{
status: "Adjustment",
color: "bg-yellow-100 dark:bg-yellow-900/50",
detail: t("landingPage.returned"),
},
{
status: "Rejected",
color: "bg-red-100 dark:bg-red-900/50",
detail: t("landingPage.rejected"),
},
{
status: "Sent",
color: "bg-primary-300 dark:bg-primary-700",
detail: t("landingPage.sent"),
},
{
status: "Returned",
color: "bg-gray-400 dark:bg-gray-600",
detail: t("landingPage.returnedByOfficer"),
},
].map((item) => (
<div
key={item.status}
className={`${item.color} p-2 rounded text-center text-sm font-medium relative`}
>
<div className="flex items-center justify-between">
<span className="mx-auto dark:text-gray-900">
{t(`statusBar.${item.status}`)}
</span>
<button
onClick={() => toggle(item.status)}
className="ml-2 text-xs text-primary hover:underline"
>
<span
className={`inline-block transition-transform duration-200 ${
openStatus === item.status ? "rotate-180" : ""
}`}
>
</span>
</button>
</div>
{openStatus === item.status && (
<div className="mt-2 p-2 text-xs bg-white dark:bg-gray-700 border dark:border-gray-600 rounded shadow absolute top-full left-0 w-full z-10 dark:text-gray-200">
{item.detail}
</div>
)}
</div>
))}
</div>
<p className="text-sm text-gray-600 dark:text-gray-400 mt-3">
{t("landingPage.monitor")}
</p>
</div>
</div>
),
image: "/images/outgoing-screen.png",
},
{
id: "incoming",
title: t("landingPage.manageIncoming"),
content: (
<div className="space-y-6">
<div>
<h4 className="font-semibold text-gray-900 dark:text-white">
{t("landingPage.incomingTypes")}
</h4>
<div className="mt-3 grid grid-cols-1 md:grid-cols-3 gap-3">
{[
{ type: "external", desc: t("landingPage.fromOthers") },
{ type: "internal", desc: t("landingPage.withinOrg") },
{ type: "cc", desc: t("landingPage.copied") },
].map((item) => (
<div
key={item.type}
className="bg-white dark:bg-gray-800 p-3 rounded-lg border border-gray-200 dark:border-gray-700 shadow-sm"
>
<h5 className="font-medium text-primary">
{t(`nav.${item.type}`)}
</h5>
<p className="text-sm text-gray-600 dark:text-gray-300 mt-1">{item.desc}</p>
</div>
))}
</div>
</div>
<div>
<h4 className="font-semibold text-gray-900 dark:text-white">
{t("landingPage.processIncoming")}
</h4>
<ul className="list-disc pl-5 space-y-2 mt-2">
<li className="dark:text-gray-300">
<span className="font-semibold">{t("userRecord.View")}:</span>{" "}
{t("landingPage.readDoc")}
</li>
<li className="dark:text-gray-300">
<span className="font-semibold">{t("statusBar.Accept")}:</span>{" "}
{t("landingPage.acknowledge")}
</li>
<li className="dark:text-gray-300">
<span className="font-semibold">{t("statusBar.Assign")}:</span>{" "}
{t("landingPage.forward")}
</li>
<li className="dark:text-gray-300">
<span className="font-semibold">
{t("landingPage.archive")}:
</span>{" "}
{t("landingPage.fileRef")}
</li>
</ul>
</div>
</div>
),
image: "/images/incoming-screen.png",
},
{
id: "approval",
title: t("landingPage.approvalworkflow"),
content: (
<div className="space-y-6">
<div>
<h4 className="font-semibold text-gray-900 dark:text-white">
{t("landingPage.process")}
</h4>
<div className="mt-4">
<h5 className="text-sm font-medium text-gray-700 dark:text-gray-300">
{t("landingPage.internalFlow")}
</h5>
<div className="flex items-center justify-between mt-2">
{[
t("landingPage.creator"),
t("landingPage.leader"),
t("landingPage.director"),
t("landingPage.officer"),
].map((role, i) => (
<div key={i} className="flex flex-col items-center">
<div className="h-10 w-10 rounded-full bg-primary flex items-center justify-center text-white font-medium text-sm">
{i + 1}
</div>
<span className="text-xs mt-1 text-center dark:text-gray-300">{role}</span>
</div>
))}
</div>
</div>
<div className="mt-6">
<h5 className="text-sm font-medium text-gray-700 dark:text-gray-300">
{t("landingPage.externalFlow")}
</h5>
<div className="flex items-center justify-between mt-2">
{[
t("landingPage.creator"),
t("landingPage.leader"),
t("landingPage.director"),
t("landingPage.officer"),
].map((role, i) => (
<div key={i} className="flex flex-col items-center">
<div className="h-10 w-10 rounded-full bg-primary-500 flex items-center justify-center text-white font-medium text-sm">
{i + 1}
</div>
<span className="text-xs mt-1 text-center dark:text-gray-300">{role}</span>
</div>
))}
</div>
</div>
</div>
<div>
<h4 className="font-semibold text-gray-900 dark:text-white">
{t("landingPage.actions")}
</h4>
<div className="mt-3 grid grid-cols-1 md:grid-cols-3 gap-3">
{[
{
action: "Approve",
desc:t("landingPage.addTeeterAndSignature"),
color: "bg-primary-50 dark:bg-primary-900/30 border-primary-200 dark:border-primary-800",
},
{
action: "Reject",
desc: t("landingPage.returnWithComments"),
color: "bg-red-50 dark:bg-red-900/30 border-red-200 dark:border-red-800",
},
{
action: "Adjust",
desc: t("landingPage.requestModifications"),
color: "bg-yellow-50 dark:bg-yellow-900/30 border-yellow-200 dark:border-yellow-800",
},
].map((item) => (
<div
key={item.action}
className={`${item.color} p-3 rounded-lg border`}
>
<h5 className="font-medium dark:text-gray-200">
{t(`statusBar.${item.action}`)}
</h5>
<p className="text-sm text-gray-600 dark:text-gray-300 mt-1">{item.desc}</p>
</div>
))}
</div>
</div>
</div>
),
image: "/images/approval-screen.png",
},
{
id: "delegation",
title: t("landingPage.delegation"),
content: (
<div className="space-y-4">
<p className="dark:text-gray-300">{t("landingPage.delegateInfo")}</p>
<ol className="list-decimal pl-5 space-y-2">
<li className="dark:text-gray-300">
{t("landingPage.delegateNav")}{" "}
<span className="font-semibold text-primary">
{t("delegation.title")}
</span>{" "}
{t("landingPage.tab")}
</li>
<li className="dark:text-gray-300">{t("landingPage.selectColleague")}</li>
<li className="dark:text-gray-300">{t("landingPage.setDates")}</li>
<li className="dark:text-gray-300">{t("landingPage.setPerms")}</li>
<li className="dark:text-gray-300">{t("landingPage.saveDelegate")}</li>
</ol>
<div className="bg-gray-50 dark:bg-gray-800 p-4 rounded-lg border border-gray-200 dark:border-gray-700 mt-4">
<p className="text-sm text-gray-600 dark:text-gray-300">
<span className="font-semibold">{t("landingPage.note")}</span>{" "}
{t("landingPage.autoExpire")}
</p>
</div>
</div>
),
image: "/images/delegation-screen.png",
},
{
id: "collaboration",
title: t("landingPage.collab"),
content: (
<div className="space-y-4">
<p className="dark:text-gray-300">{t("landingPage.collabInfo")}</p>
<ul className="list-disc pl-5 space-y-2">
<li className="dark:text-gray-300">{t("landingPage.viewDocs")}</li>
<li className="dark:text-gray-300">{t("landingPage.addComments")}</li>
<li className="dark:text-gray-300">{t("landingPage.approve")}</li>
<li className="dark:text-gray-300">{t("landingPage.trackChanges")}</li>
</ul>
<div className="grid grid-cols-1 md:grid-cols-2 gap-4 mt-4">
<div className="bg-white dark:bg-gray-800 p-4 rounded-lg border border-gray-200 dark:border-gray-700 shadow-sm">
<h4 className="font-medium text-gray-900 dark:text-white">
{t("landingPage.realTime")}
</h4>
<p className="text-sm text-gray-600 dark:text-gray-300 mt-2">
{t("landingPage.liveChanges")}
</p>
</div>
<div className="bg-white dark:bg-gray-800 p-4 rounded-lg border border-gray-200 dark:border-gray-700 shadow-sm">
<h4 className="font-medium text-gray-900 dark:text-white">
{t("landingPage.notifications")}
</h4>
<p className="text-sm text-gray-600 dark:text-gray-300 mt-2">
{t("landingPage.alerts")}
</p>
</div>
</div>
</div>
),
image: "/images/collaboration-screen.png",
},
{
id: "teeter",
title: t("landingPage.signatureMgmt"),
content: (
<div className="space-y-4">
<p className="dark:text-gray-300">{t("landingPage.leadersOnly")}</p>
<ol className="list-decimal pl-5 space-y-2">
<li className="dark:text-gray-300">
{t("landingPage.goToTab")}{" "}
<span className="font-semibold text-primary">
{t("landingPage.signatureTitle")}
</span>{" "}
{t("landingPage.tab")}
</li>
<li className="dark:text-gray-300">{t("landingPage.uploadTeeter")}</li>
<li className="dark:text-gray-300">{t("landingPage.uploadSignature")}</li>
<li className="dark:text-gray-300">{t("landingPage.defaultSignature")}</li>
<li className="dark:text-gray-300">{t("landingPage.updateDesignation")}</li>
</ol>
<div className="bg-red-50 dark:bg-red-900/30 p-4 rounded-lg border border-red-200 dark:border-red-800 mt-4">
<p className="text-sm text-red-600 dark:text-red-400">
<span className="font-semibold">{t("landingPage.security")}</span>{" "}
{t("landingPage.encrypted")}.
</p>
</div>
</div>
),
image: "/images/teeter-screen.png",
},
];
return (
<div className="min-h-screen bg-gray-50 dark:bg-gray-900">
{/* Header */}
<header className="bg-white dark:bg-gray-800 shadow-sm">
<div className="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8 py-6">
<div className="flex flex-col md:flex-row justify-between items-start md:items-center">
<div>
<h1 className="text-3xl font-bold text-gray-900 dark:text-white">
{t("landingPage.recordModule")}
</h1>
<p className="mt-2 text-lg text-gray-600 dark:text-gray-300">
{t("landingPage.guide")}
</p>
</div>
<button
onClick={() => navigate("/")}
className="mt-4 md:mt-0 px-4 py-2 border border-transparent text-sm font-medium rounded-md text-white bg-primary hover:bg-primary-700 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-primary"
>
{t("landingPage.back")}
</button>
</div>
</div>
</header>
{/* Main Content */}
<main className="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8 py-8">
{/* Navigation Tabs */}
<div className="border-b border-gray-200 dark:border-gray-700">
<nav className="-mb-px flex space-x-8 overflow-x-auto scrollbar-hidden">
{steps.map((step) => (
<button
key={step.id}
onClick={() => handleTabChange(step.id)}
className={`whitespace-nowrap py-4 px-1 border-b-2 font-medium text-sm cursor-pointer ${
activeTab === step.id
? "border-primary text-primary"
: "border-transparent text-gray-500 dark:text-gray-400 hover:text-gray-700 dark:hover:text-gray-300 hover:border-gray-300 dark:hover:border-gray-600"
}`}
>
{step.title}
</button>
))}
</nav>
</div>
{/* Content Area */}
<div className="mt-8 grid grid-cols-1 lg:grid-cols-3 gap-8">
{/* Steps List */}
<div className="lg:col-span-1">
<div className="bg-white dark:bg-gray-800 shadow-sm rounded-lg overflow-hidden">
<div className="p-4 bg-primary">
<h2 className="text-lg font-medium text-white">
{t("landingPage.moduleGuide")}
</h2>
</div>
<div className="divide-y divide-gray-200 dark:divide-gray-700">
{steps.map((step) => (
<div
key={step.id}
onClick={() => handleTabChange(step.id)}
className={`p-4 cursor-pointer transition-colors ${
activeTab === step.id
? "bg-primary-500/5"
: "hover:bg-gray-50 dark:hover:bg-gray-700"
}`}
>
<div className="flex items-center">
<div
className={`h-8 w-8 rounded-full flex items-center justify-center mr-3 ${
activeTab === step.id
? "bg-primary text-white"
: "bg-gray-200 dark:bg-gray-700 text-gray-600 dark:text-gray-300"
}`}
>
{steps.findIndex((s) => s.id === step.id) + 1}
</div>
<h3 className="text-sm font-medium dark:text-gray-200">{step.title}</h3>
</div>
</div>
))}
</div>
</div>
</div>
{/* Active Tab Content */}
<div className="lg:col-span-2">
<div className="bg-white dark:bg-gray-800 shadow-sm rounded-lg overflow-hidden">
<div className="p-6">
<h2 className="text-2xl font-bold text-gray-900 dark:text-white">
{steps.find((step) => step.id === activeTab)?.title}
</h2>
<div className="mt-6 prose prose-sm max-w-none">
{steps.find((step) => step.id === activeTab)?.content}
</div>
{/* Image placeholder - replace with actual image */}
<div className="mt-8 bg-gray-100 dark:bg-gray-700 rounded-lg border border-gray-200 dark:border-gray-600 p-4 flex items-center justify-center">
<p className="text-gray-500 dark:text-gray-400">
{t("landingPage.screenshotOf")}{" "}
{steps.find((step) => step.id === activeTab)?.title}
</p>
</div>
</div>
{/* Navigation Buttons */}
<div className="px-6 py-4 bg-gray-50 dark:bg-gray-700 flex justify-between">
{steps.findIndex((step) => step.id === activeTab) > 0 && (
<button
onClick={() =>
handleTabChange(
steps[
steps.findIndex((step) => step.id === activeTab) - 1
].id
)
}
className="inline-flex items-center px-4 py-2 border border-gray-300 dark:border-gray-600 shadow-sm text-sm font-medium rounded-md text-gray-700 dark:text-gray-200 bg-white dark:bg-gray-800 hover:bg-gray-50 dark:hover:bg-gray-700 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-primary"
>
{t("landingPage.previous")}
</button>
)}
{steps.findIndex((step) => step.id === activeTab) <
steps.length - 1 && (
<button
onClick={() =>
handleTabChange(
steps[
steps.findIndex((step) => step.id === activeTab) + 1
].id
)
}
className="ml-auto inline-flex items-center px-4 py-2 border border-transparent text-sm font-medium rounded-md shadow-sm text-white bg-primary hover:bg-primary-700 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-primary"
>
{t("landingPage.next")}
</button>
)}
</div>
</div>
</div>
</div>
</main>
</div>
);
};
export default RecordManagementGuide;

View File

@@ -0,0 +1,17 @@
import React, { useState } from "react";
import { FaExclamationCircle, FaArrowLeft } from "react-icons/fa";
import { useTranslation } from "react-i18next";
import { useNavigate } from "react-router-dom";
import Header from "./Header";
import Footer from "./Footer";
import { getComplaintById } from "../../shared/services/complaintService";
const KnowAs = () => {
return (
<div>
<div>comming soon</div>
</div>
);
};
export default KnowAs;

View File

@@ -0,0 +1,591 @@
import { useMemo, useEffect, type ReactNode } from "react";
export interface FooterData {
address: string;
phone: string;
email: string;
}
export interface Socials {
facebook?: string;
twitter?: string;
linkedin?: string;
instagram?: string;
youtube?: string;
telegram?: string;
tiktok?: string;
website?: string;
}
export type fileInfo = {
size?: number;
bucket?: string;
fileName?: string;
contentType?: string;
originalname?: string;
};
export interface ModuleConfig {
recordManagement: boolean;
siteManagement: boolean;
dms: boolean;
performance: boolean;
objective: boolean;
complaint: boolean;
}
export interface TenantConfig {
appName: string;
organizationName: string;
canUseAttachmentFromDMS?: boolean;
logo: string;
primaryColor: string;
secondaryColor?: string;
footerText: string;
footerData: FooterData;
socials: Socials;
welcomeMessage: string;
moduleConfig?: Partial<ModuleConfig>;
dashboardPreviewImage?: string;
}
export interface brandingDTO {
organizationName: {
am: string;
en: string;
};
logo: {
presigned: string;
fileInfo: fileInfo;
};
primaryColor: string;
secondaryColor?: string;
footerText: string;
footerData: FooterData;
socials: Socials;
welcomeMessage: string;
loginImage?: {
presigned: string;
fileInfo: fileInfo;
};
favicon?: {
presigned: string;
fileInfo: fileInfo;
};
moduleConfig?: Partial<ModuleConfig>;
}
/**
* Resolve effective module visibility for a tenant.
* Record Management and Site Management default to enabled.
* Complaint controls public landing-page complaint entry points only (not module navigation).
* Other optional modules default to disabled unless enabled by the tenant.
*/
export const resolveModuleConfig = (config: TenantConfig): ModuleConfig => ({
recordManagement: config.moduleConfig?.recordManagement ?? true,
siteManagement: true,
dms: config.moduleConfig?.dms ?? false,
performance: config.moduleConfig?.performance ?? false,
objective: config.moduleConfig?.objective ?? false,
complaint: config.moduleConfig?.complaint ?? false,
});
const defaultConfig: TenantConfig = {
appName: "Smart Office",
organizationName: "Smart Office",
canUseAttachmentFromDMS: false,
logo: "/assets/TriaTradinglogo.png",
primaryColor: "#1b354d",
secondaryColor: "#0f5a3a",
footerText: "COMMITED TO EXCELLENCE",
footerData: {
address: "Addis Ababa, Ethiopia",
phone: "+251955232323",
email: "info@triaplc.com",
},
socials: {
facebook: "",
twitter: "",
linkedin: "",
instagram: "",
youtube: "",
telegram: "",
tiktok: "",
website: "https://triaplc.com/",
},
welcomeMessage:
"At Tria Trading PLC, we empower organizations with innovative software solutions and expert consulting services. Our commitment to excellence, innovation, and customer success enables us to deliver technology that drives growth, efficiency, and lasting impact.",
};
const tenantConfigs: Record<string, TenantConfig> = {
localhost: {
appName: "Smart Office",
organizationName: "Addis Ababa City Administration",
logo: "",
primaryColor: "#115005",
moduleConfig: {
dms: true,
performance: true,
objective: true,
complaint: true,
},
canUseAttachmentFromDMS: true,
secondaryColor: "#dad9db",
footerText: "COMMITED TO EXCELLENCE",
footerData: {
address: "Addis Ababa, Ethiopia",
phone: "+251955232323",
email: "info@triaplc.com",
},
socials: {
facebook: "",
twitter: "",
linkedin: "",
instagram: "",
youtube: "",
telegram: "",
tiktok: "",
website: "https://triaplc.com/",
},
welcomeMessage:
"Welcome to the Smart Office of the Addis Ababa City Administration. Addis Ababa is Ethiopias capital and a center of public service, innovation, culture, and opportunity. Through this platform, we aim to strengthen efficient service delivery, modernize administrative workflows, improve transparency, and support responsive governance for residents, institutions, and stakeholders across the city.",
},
"127.0.0.1": {
appName: "Smart Office EDR",
organizationName: "Ethio-Djibouti Railways",
canUseAttachmentFromDMS: true,
logo: "/assets/edrlogo.png",
primaryColor: "#DC143C",
moduleConfig: {
dms: true,
performance: true,
objective: true,
complaint: true,
},
secondaryColor: "#0f5a3a",
dashboardPreviewImage: "/edrheadoffice.jpg",
footerText:
"The Ethio-Djibouti Railway was established in April 2017 following a bilateral agreement signed on December 16, 2016, between Ethiopia and Djibouti.",
footerData: {
address: "Addis Ababa, Ethiopia",
phone: "9546",
email: "edr_@edrsc.com",
},
socials: {
facebook: "",
twitter: "",
linkedin: "",
instagram: "",
youtube: "",
telegram: "",
tiktok: "",
website: "https://edrsc.com/",
},
welcomeMessage:
"The Ethio-Djibouti Railway was established in April 2017 following a bilateral agreement signed on December 16, 2016, between Ethiopia and Djibouti. A Shareholders Agreement was signed on January 11, 2017, among public bodies and state enterprises from both nations, governed by Ethiopian commercial law.\n\nThe shareholders formed a Share Company with an initial capital of USD 500 million, dedicated to operating and maintaining the Addis AbabaDjibouti Railway and providing freight and passenger transport services.",
},
"smartoffice.edrsc.com": {
appName: "Smart Office EDR",
organizationName: "Ethio-Djibouti Railways",
canUseAttachmentFromDMS: true,
logo: "/assets/edrlogo.png",
primaryColor: "#13724D",
moduleConfig: {
dms: true,
performance: false,
objective: false,
complaint: true,
},
secondaryColor: "#0f5a3a",
dashboardPreviewImage: "/edrheadoffice.jpg",
footerText:
"The Ethio-Djibouti Railway was established in April 2017 following a bilateral agreement signed on December 16, 2016, between Ethiopia and Djibouti.",
footerData: {
address: "Addis Ababa, Ethiopia",
phone: "9546",
email: "edr_@edrsc.com",
},
socials: {
facebook: "https://web.facebook.com/ethiodjiboutirailwaysc",
twitter: "https://edrsc.com/",
linkedin: "https://edrsc.com/",
instagram: "https://edrsc.com/",
youtube: "https://edrsc.com/",
telegram: "",
tiktok: "",
website: "https://edrsc.com/",
},
welcomeMessage:
"The Ethio-Djibouti Railway was established in April 2017 following a bilateral agreement signed on December 16, 2016, between Ethiopia and Djibouti. A Shareholders Agreement was signed on January 11, 2017, among public bodies and state enterprises from both nations, governed by Ethiopian commercial law.\n\nThe shareholders formed a Share Company with an initial capital of USD 500 million, dedicated to operating and maintaining the Addis AbabaDjibouti Railway and providing freight and passenger transport services.",
},
"smartoffice.eiar.gov.et": {
appName: "Smart Office EIAR",
organizationName: "Ethiopian Institute of Agricultural Research",
logo: "/assets/eiarLogo.jpg",
primaryColor: "#388e4b",
moduleConfig: { dms: false, performance: false, objective: false },
secondaryColor: "#fff",
footerText:
"The Ethiopian Institute of Agricultural Research (EIAR) is one of the oldest and largest agricultural research institutes in Africa. EIAR has evolved through several stages since its first initiation during the late 1940s, following the establishment of agricultural and technical schools at Ambo and Jimma. In 1955, a full-fledged agricultural experiment station was established at Debre Zeit (now named Debre Zeit Agricultural Research Center) under the then Imperial College of Agricultural and mechanical Arts (now called Haramaya University) and had been continued as the major research entity until the mid-1960s. In 1966, Institute of Agricultural Research (IAR) was established as the first nationally coordinated agricultural research institute in Ethiopia. IAR was established with a mission to formulate national agricultural research guidelines, coordinate national agricultural research system, and undertake research in its centers and sub-centers located in various agro-ecological zones of Ethiopia.",
footerData: {
address: "Addis Ababa, Ethiopia",
phone: "+251-116-454441",
email: "eiar.gov.et",
},
canUseAttachmentFromDMS: false,
socials: {
facebook: "https://web.facebook.com/EIARPR",
website: "http://www.eiar.gov.et",
twitter: "http://www.eiar.gov.et",
linkedin: "http://www.eiar.gov.et",
instagram: "http://www.eiar.gov.et",
youtube: "http://www.eiar.gov.et",
},
welcomeMessage:
"welcome you to the SmartOffice of the Ethiopian Institute of Agricultural Research. The Institute, since its establishment in 1966, has released over more 3000 agricultural technologies and improved farming practices by undertaking scientific research activities on various areas in crop, livestock, land and water, biotechnology, climate, farm machinery and agricultural economics. Of these technologies, 1190 are crop varieties and the rest include livestock breeds, pest and disease control methods, crop production and livestock husbandry methods, pre- and post-harvest technologies and recommendations. EIAR, in the five decades of its existence, has reached a large number of beneficiaries with its technologies and information found in different agro-ecologies throughout the country. The Institute has engaged itself in multiplication of initial technologies based on the demand created by the beneficiaries and supply to public and private technology multiplication actors for their wider reproduction and distribution.",
},
"smartoffice.ebi.gov.et": {
appName: "EBI Smart Office",
organizationName: "Ethiopian Biodiversity Institute - EBI",
logo: "/assets/ebiLogoWhite.png",
primaryColor: "#2a741d",
moduleConfig: { dms: false, performance: false, objective: false },
secondaryColor: "#2c246d",
footerText: "Welcome to the Center of Origin & Diversity, Ethiopia",
footerData: {
address: "Addis Ababa, Ethiopia",
phone: "+251-116-615607",
email: " info@ebi.gov.et",
},
canUseAttachmentFromDMS: false,
socials: {
facebook: "https://www.facebook.com/EthiopiaEBI",
twitter: "https://x.com/EthiopiaEBI",
linkedin: "https://www.linkedin.com/company/EthiopiaEBI",
youtube: "https://www.youtube.com/@EthiopiaEBI",
instagram: "https://www.youtube.com/@EthiopiaEBI",
website: "https://ebi.gov.et",
telegram: "https://t.me/EthiopiaEBI",
tiktok: "https://www.tiktok.com/@ethiopiaebi",
},
welcomeMessage:
"Ethiopia is recognized as one of the worlds most biodiverse nations. Our rugged highlands, vast lowlands, and the rift valley lakes hold the genetic codes of the wild relatives of our staple food, and the endemic wildlife that defines our national identity. The Ethiopian Biodiversity Institute - EBI bears the profound responsibility of being the steward of these natural assets since its establishment in the 1970s.",
},
"smartoffice.aaca.gov.et": {
appName: "Smart Office",
organizationName: "Addis Ababa City Administration",
logo: "",
primaryColor: "#115005",
moduleConfig: { dms: false, performance: false, objective: false },
secondaryColor: "#dad9db",
footerText: "COMMITED TO EXCELLENCE",
footerData: {
address: "Addis Ababa, Ethiopia",
phone: "+251955232323",
email: "info@triaplc.com",
},
canUseAttachmentFromDMS: false,
socials: {
facebook: "",
twitter: "",
linkedin: "",
instagram: "",
youtube: "",
telegram: "",
tiktok: "",
website: "https://triaplc.com/",
},
welcomeMessage:
"Welcome to the Smart Office of the Addis Ababa City Administration. Addis Ababa is Ethiopias capital and a center of public service, innovation, culture, and opportunity. Through this platform, we aim to strengthen efficient service delivery, modernize administrative workflows, improve transparency, and support responsive governance for residents, institutions, and stakeholders across the city.",
},
"smartoffice.efd.moa.gov.et": {
appName: "Smart Office EFD",
organizationName: "Ethiopian Forest Development",
logo: "/assets/efdlogo.jpg",
primaryColor: "#006400",
moduleConfig: { dms: false, performance: false, objective: false },
secondaryColor: "#a52a2a",
footerText:
"Ethiopian Forestry Development (EFD) is an autonomous federal institution, established by Proclamation No. 1263/2021 on 25th January, as referred on article 81-No. 8 and by the federal government of Ethiopia council of ministers regulation No. 505/2022. EFD was resulted by merging the former research institute (The Ethiopian Environment and Forest Research Institute (EEFRI) together with the forestry sector from the then (Environment, Forest and climate change commission) having the following powers and duties.",
footerData: {
address: "Addis Ababa, Ethiopia",
phone: "9546",
email: "dg-office@efd.gov.et",
},
canUseAttachmentFromDMS: false,
socials: {
facebook:
"https://web.facebook.com/Ethiopian-Forestry-Development-EFD-100064767336706/?_rdc=1&_rdr#",
twitter: "Ethiopian Forestry Development@EthiopianFores1",
youtube: "https://www.youtube.com/@infoefd",
website: "https://www.efd.gov.et",
},
welcomeMessage:
"Organized forestry research in Ethiopia was started by the establishment of Forestry Research Centre (FRC) and the then Wood Utilization Research Centre (WUARC) in 1975 and 1979, respectively under the Forestry and Wildlife Conservation Development Authority (FaWCDA). The centres were incorporated to the then Ministry of Natural Resources Development and Environmental Protection in 1992 and again re-transferred to the Ministry of Agriculture in 1995. The Federal Government of Ethiopia reorganized the National Agricultural Research System and established the then Ethiopian Agricultural Research Organization (EARO) in 1997 (Negarit Gazeta, 1997). As a result, FRC and WUARC were transferred to EARO as one research centre (FRC) and one of the research sectors of EARO (now Ethiopian Institute of Agricultural Research (EIAR)). Then, the Government of Ethiopia found it necessary to give due attention to activities of environmental protection and forest development, protection and utilization by linking forestry research with environmental protection research at an institutional level for the attainment of the objectives of the Government that resulted in the establishment of the Ministry of Environment, Forest and Climate Change.",
},
"smartoffice.moa.gov.et": {
appName: "Smart Office MOA",
organizationName: "Ministry of Agriculture",
logo: "/assets/ministryOfAgriculture.jpg",
primaryColor: "#006400",
moduleConfig: {
dms: false,
performance: false,
objective: true,
recordManagement: false,
},
secondaryColor: "#a52a2a",
footerText:
"The Ministry of Agriculture (MoA) is a federal government institution of Ethiopia responsible for leading and coordinating the country's agricultural development. The Ministry is mandated to formulate policies, strategies, and programs that enhance agricultural productivity, ensure food security, promote sustainable natural resource management, and support rural transformation. Through its various sectors and agencies, the Ministry oversees crop and livestock development, agricultural extension services, research coordination, and the implementation of national agricultural initiatives aimed at improving the livelihoods of farmers and contributing to the country's economic growth.",
footerData: {
address: "Addis Ababa, Ethiopia",
phone: "9546",
email: "dg-office@efd.gov.et",
},
canUseAttachmentFromDMS: false,
socials: {
facebook: "https://web.facebook.com/MoAEthiopia/?_rdc=1&_rdr#",
twitter: "https://x.com/MoA_Ethiopia",
youtube: "https://www.youtube.com/@publicrelationmoa",
website: "https://www.moa.gov.et",
},
welcomeMessage:
"The Ministry of Agriculture (MoA) has played a central role in Ethiopias agricultural development and transformation efforts. Over the years, the Ministry has undergone various reforms and restructuring initiatives to strengthen the country's agricultural sector and improve food security. The Ministry is responsible for guiding agricultural research, extension services, livestock and crop development, natural resource management, and rural development programs. It works closely with research institutions, regional bureaus, development partners, and stakeholders to promote sustainable agricultural practices, increase productivity, and support the livelihoods of farmers and pastoral communities. Through its ongoing efforts, the Ministry continues to contribute significantly to Ethiopias economic growth, environmental sustainability, and national development objectives.",
},
"smartofficedev.moa.gov.et": {
appName: "Smart Office MOA",
organizationName: "Ministry of Agriculture",
logo: "/assets/ministryOfAgriculture.jpg",
primaryColor: "#006400",
moduleConfig: {
dms: false,
performance: false,
objective: true,
recordManagement: true,
},
secondaryColor: "#a52a2a",
footerText:
"The Ministry of Agriculture (MoA) is a federal government institution of Ethiopia responsible for leading and coordinating the country's agricultural development. The Ministry is mandated to formulate policies, strategies, and programs that enhance agricultural productivity, ensure food security, promote sustainable natural resource management, and support rural transformation. Through its various sectors and agencies, the Ministry oversees crop and livestock development, agricultural extension services, research coordination, and the implementation of national agricultural initiatives aimed at improving the livelihoods of farmers and contributing to the country's economic growth.",
footerData: {
address: "Addis Ababa, Ethiopia",
phone: "9546",
email: "dg-office@efd.gov.et",
},
canUseAttachmentFromDMS: false,
socials: {
facebook: "https://web.facebook.com/MoAEthiopia/?_rdc=1&_rdr#",
twitter: "https://x.com/MoA_Ethiopia",
youtube: "https://www.youtube.com/@publicrelationmoa",
website: "https://www.moa.gov.et",
},
welcomeMessage:
"The Ministry of Agriculture (MoA) has played a central role in Ethiopias agricultural development and transformation efforts. Over the years, the Ministry has undergone various reforms and restructuring initiatives to strengthen the country's agricultural sector and improve food security. The Ministry is responsible for guiding agricultural research, extension services, livestock and crop development, natural resource management, and rural development programs. It works closely with research institutions, regional bureaus, development partners, and stakeholders to promote sustainable agricultural practices, increase productivity, and support the livelihoods of farmers and pastoral communities. Through its ongoing efforts, the Ministry continues to contribute significantly to Ethiopias economic growth, environmental sustainability, and national development objectives.",
},
"triadms-dev.smartoffice.aaca.gov.et": {
appName: "Smart Office Tria",
organizationName: "Tria Trading PLC",
logo: "/assets/TriaTradinglogo.png",
primaryColor: "#1b354d",
moduleConfig: { dms: true, performance: true, objective: true },
secondaryColor: "#0f5a3a",
footerText: "COMMITED TO EXCELLENCE",
footerData: {
address: "Addis Ababa, Ethiopia",
phone: "+251955232323",
email: "info@triaplc.com",
},
canUseAttachmentFromDMS: true,
socials: {
facebook: "",
twitter: "",
linkedin: "",
instagram: "",
youtube: "",
telegram: "",
tiktok: "",
website: "https://triaplc.com/",
},
welcomeMessage:
"At Tria Trading PLC, we empower organizations with innovative software solutions and expert consulting services. Our commitment to excellence, innovation, and customer success enables us to deliver technology that drives growth, efficiency, and lasting impact.",
},
"triadms.smartoffice.aaca.gov.et": {
appName: "Smart Office Tria",
organizationName: "Tria Trading PLC",
logo: "/assets/TriaTradinglogo.png",
primaryColor: "#1b354d",
moduleConfig: { dms: true, performance: true, objective: true },
secondaryColor: "#0f5a3a",
footerText: "COMMITED TO EXCELLENCE",
footerData: {
address: "Addis Ababa, Ethiopia",
phone: "+251955232323",
email: "info@triaplc.com",
},
socials: {
facebook: "",
twitter: "",
linkedin: "",
instagram: "",
youtube: "",
telegram: "",
tiktok: "",
website: "https://triaplc.com/",
},
welcomeMessage:
"At Tria Trading PLC, we empower organizations with innovative software solutions and expert consulting services. Our commitment to excellence, innovation, and customer success enables us to deliver technology that drives growth, efficiency, and lasting impact.",
},
"smart-dev.smart.aaca.gov.et": {
appName: "Smart Office Tria",
organizationName: "Tria Trading PLC",
logo: "/assets/TriaTradinglogo.png",
primaryColor: "#1b354d",
moduleConfig: { dms: true, performance: true, objective: true },
secondaryColor: "#0f5a3a",
footerText: "COMMITED TO EXCELLENCE",
footerData: {
address: "Addis Ababa, Ethiopia",
phone: "+251955232323",
email: "info@triaplc.com",
},
socials: {
facebook: "",
twitter: "",
linkedin: "",
instagram: "",
youtube: "",
telegram: "",
tiktok: "",
website: "https://triaplc.com/",
},
welcomeMessage:
"At Tria Trading PLC, we empower organizations with innovative software solutions and expert consulting services. Our commitment to excellence, innovation, and customer success enables us to deliver technology that drives growth, efficiency, and lasting impact.",
},
"performance-dev.smart.aaca.gov.et": {
appName: "Smart Office Tria",
organizationName: "Tria Trading PLC",
logo: "/assets/TriaTradinglogo.png",
primaryColor: "#1b354d",
moduleConfig: { dms: true, performance: true, objective: true },
secondaryColor: "#0f5a3a",
footerText: "COMMITED TO EXCELLENCE",
footerData: {
address: "Addis Ababa, Ethiopia",
phone: "+251955232323",
email: "info@triaplc.com",
},
socials: {
facebook: "",
twitter: "",
linkedin: "",
instagram: "",
youtube: "",
telegram: "",
tiktok: "",
website: "https://triaplc.com/",
},
welcomeMessage:
"At Tria Trading PLC, we empower organizations with innovative software solutions and expert consulting services. Our commitment to excellence, innovation, and customer success enables us to deliver technology that drives growth, efficiency, and lasting impact.",
},
"performance.smart.aaca.gov.et": {
appName: "Smart Office Tria",
organizationName: "Tria Trading PLC",
logo: "/assets/TriaTradinglogo.png",
primaryColor: "#1b354d",
moduleConfig: { dms: true, performance: true, objective: true },
secondaryColor: "#0f5a3a",
footerText: "COMMITED TO EXCELLENCE",
footerData: {
address: "Addis Ababa, Ethiopia",
phone: "+251955232323",
email: "info@triaplc.com",
},
socials: {
facebook: "",
twitter: "",
linkedin: "",
instagram: "",
youtube: "",
telegram: "",
tiktok: "",
website: "https://triaplc.com/",
},
welcomeMessage:
"At Tria Trading PLC, we empower organizations with innovative software solutions and expert consulting services. Our commitment to excellence, innovation, and customer success enables us to deliver technology that drives growth, efficiency, and lasting impact.",
},
};
/** Resolve tenant config from hostname (defaults to localhost config). */
export const resolveTenantConfig = (
hostname: string = typeof window !== "undefined"
? window.location.hostname
: "localhost",
): TenantConfig => {
// Normalize host input so callers can pass full URLs or mixed-case values safely.
const normalizedHost = hostname
?.trim()
.toLowerCase()
.replace(/^https?:\/\//, "")
.replace(/\/.*$/, "")
.replace(/:\d+$/, "");
return tenantConfigs[normalizedHost] || defaultConfig;
};
/** Apply tenant theme tokens to the document root (CSS variables, title, favicon). */
export const applyTenantTheme = (config: TenantConfig) => {
if (typeof document === "undefined") {
return;
}
const root = document.documentElement;
const primary =
config.primaryColor?.trim() || "var(--primary-default, #18aa9d)";
root.style.setProperty("--primary", primary);
root.style.setProperty("--ring", primary);
root.style.setProperty("--sidebar-primary", primary);
root.style.setProperty("--accent", primary);
root.style.setProperty("--chart-1", primary);
if (config.organizationName) {
document.title = config.organizationName;
}
if (config.logo) {
let link = document.querySelector<HTMLLinkElement>('link[rel="icon"]');
if (!link) {
link = document.createElement("link");
link.rel = "icon";
document.head.appendChild(link);
}
link.href = config.logo;
}
};
// Apply tenant theme before React mounts so deep-linked routes (e.g. record-management)
// keep branding on hard refresh without relying on login/landing page components.
if (typeof window !== "undefined") {
applyTenantTheme(resolveTenantConfig());
}
export const useTenantConfig = () => {
const hostname = useMemo(() => window.location.hostname, []);
const config = useMemo(() => resolveTenantConfig(hostname), [hostname]);
useEffect(() => {
applyTenantTheme(config);
}, [config]);
return {
config,
hostname,
};
};
/** Ensures tenant theme is applied on every route, including lazy-loaded modules. */
export const TenantConfigProvider = ({ children }: { children: ReactNode }) => {
useTenantConfig();
return children;
};

View File

@@ -0,0 +1,396 @@
import { useTranslation } from "react-i18next";
const WorkflowSection = () => {
const { t } = useTranslation();
const steps = [
{
id: "01",
name: t("landingPage.recordCreation"),
description: t("landingPage.recordCreationDesc"),
status: "complete",
icon: (
<svg
xmlns="http://www.w3.org/2000/svg"
className="h-5 w-5"
viewBox="0 0 20 20"
fill="currentColor"
>
<path
fillRule="evenodd"
d="M6 2a2 2 0 00-2 2v12a2 2 0 002 2h8a2 2 0 002-2V7.414A2 2 0 0015.414 6L12 2.586A2 2 0 0010.586 2H6zm5 6a1 1 0 10-2 0v2H7a1 1 0 100 2h2v2a1 1 0 102 0v-2h2a1 1 0 100-2h-2V8z"
clipRule="evenodd"
/>
</svg>
),
},
{
id: "02",
name: t("landingPage.teamLeaderReview"),
description: t("landingPage.teamLeaderReviewDesc"),
status: "complete",
icon: (
<svg
xmlns="http://www.w3.org/2000/svg"
className="h-5 w-5"
viewBox="0 0 20 20"
fill="currentColor"
>
<path d="M9 6a3 3 0 11-6 0 3 3 0 016 0zM17 6a3 3 0 11-6 0 3 3 0 016 0zM12.93 17c.046-.327.07-.66.07-1a6.97 6.97 0 00-1.5-4.33A5 5 0 0119 16v1h-6.07zM6 11a5 5 0 015 5v1H1v-1a5 5 0 015-5z" />
</svg>
),
},
{
id: "03",
name: t("landingPage.directorApproval"),
description: t("landingPage.directorApprovalDesc"),
status: "current",
icon: (
<svg
xmlns="http://www.w3.org/2000/svg"
className="h-5 w-5"
viewBox="0 0 20 20"
fill="currentColor"
>
<path
fillRule="evenodd"
d="M10 9a3 3 0 100-6 3 3 0 000 6zm-7 9a7 7 0 1114 0H3z"
clipRule="evenodd"
/>
</svg>
),
},
{
id: "04",
name: t("landingPage.deputyHeadReview"),
description: t("landingPage.deputyHeadReviewDesc"),
status: "upcoming",
icon: (
<svg
xmlns="http://www.w3.org/2000/svg"
className="h-5 w-5"
viewBox="0 0 20 20"
fill="currentColor"
>
<path
fillRule="evenodd"
d="M18 10a8 8 0 11-16 0 8 8 0 0116 0zm-7-4a1 1 0 11-2 0 1 1 0 012 0zM9 9a1 1 0 000 2v3a1 1 0 001 1h1a1 1 0 100-2v-3a1 1 0 00-1-1H9z"
clipRule="evenodd"
/>
</svg>
),
},
{
id: "05",
name: t("landingPage.recordOfficerProcessing"),
description: t("landingPage.recordOfficerProcessingDesc"),
status: "upcoming",
icon: (
<svg
xmlns="http://www.w3.org/2000/svg"
className="h-5 w-5"
viewBox="0 0 20 20"
fill="currentColor"
>
<path d="M9 2a1 1 0 000 2h2a1 1 0 100-2H9z" />
<path
fillRule="evenodd"
d="M4 5a2 2 0 012-2 3 3 0 003 3h2a3 3 0 003-3 2 2 0 012 2v11a2 2 0 01-2 2H6a2 2 0 01-2-2V5zm3 4a1 1 0 000 2h.01a1 1 0 100-2H7zm3 0a1 1 0 000 2h3a1 1 0 100-2h-3zm-3 4a1 1 0 100 2h.01a1 1 0 100-2H7zm3 0a1 1 0 100 2h3a1 1 0 100-2h-3z"
clipRule="evenodd"
/>
</svg>
),
},
];
return (
<div className="py-16 bg-gradient-to-b from-gray-50 to-white dark:from-gray-900 dark:to-gray-800">
<div className="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8">
<div className="lg:text-center mb-16">
<span className="inline-flex items-center px-3 py-1 rounded-full text-sm font-medium bg-primary/10 text-primary animate-pulse">
{t("landingPage.streamlinedProcess")}
</span>
<h2 className="mt-4 text-4xl font-extrabold tracking-tight text-gray-900 dark:text-white sm:text-5xl">
<span className="block">
{t("landingPage.recordManagementApprovalWorkflows")}
</span>
<span className="block text-primary">
{t("landingPage.workflow")}
</span>
</h2>
<p className="mt-6 max-w-3xl text-xl text-gray-600 dark:text-gray-300 lg:mx-auto">
{t("landingPage.smartOfficeDescription")}
</p>
</div>
<div className="mt-12">
<div className="relative">
{/* Progress bar */}
<div className="hidden md:block absolute top-0 left-16 h-full w-0.5 bg-gray-200 dark:bg-gray-700">
<div
className="absolute top-0 left-0 h-full bg-primary transition-all duration-1000 ease-in-out"
style={{ height: "60%" }} // Adjust based on current step
/>
</div>
<ul className="space-y-10 md:space-y-12">
{steps.map((step, stepIdx) => (
<li
key={step.name}
className="relative group transition-all duration-300 hover:scale-[1.02]"
data-aos="fade-up"
data-aos-delay={stepIdx * 100}
>
<div className="relative flex items-start md:items-center">
{/* Step indicator */}
<div className="flex-shrink-0 relative z-10">
<div
className={`flex items-center justify-center w-12 h-12 rounded-full transition-all duration-300 shadow-lg ${
step.status === "complete"
? "bg-primary text-white transform group-hover:scale-110"
: step.status === "current"
? "bg-white dark:bg-gray-800 border-4 border-primary shadow-[var(--primary)]/30"
: "bg-white dark:bg-gray-800 border-2 border-gray-300 dark:border-gray-600 group-hover:border-gray-400"
}`}
>
{step.status === "complete" ? (
<svg
className="w-6 h-6"
xmlns="http://www.w3.org/2000/svg"
viewBox="0 0 20 20"
fill="currentColor"
>
<path
fillRule="evenodd"
d="M16.707 5.293a1 1 0 010 1.414l-8 8a1 1 0 01-1.414 0l-4-4a1 1 0 011.414-1.414L8 12.586l7.293-7.293a1 1 0 011.414 0z"
clipRule="evenodd"
/>
</svg>
) : step.status === "current" ? (
<div className="animate-ping absolute inline-flex h-3 w-3 rounded-full bg-primary opacity-75"></div>
) : step.status === "upcoming" ? (
<span className="text-gray-400 dark:text-gray-500 font-medium">{step.id}</span>
) : (
<span className="text-gray-400 dark:text-gray-500">{step.icon}</span>
)}
</div>
</div>
{/* Step content */}
<div
className={`ml-6 p-6 rounded-xl flex-1 transition-all duration-300 ${
step.status === "current"
? "bg-white dark:bg-gray-800 border-l-4 border-primary shadow-lg"
: "bg-white dark:bg-gray-800 shadow-md group-hover:shadow-lg"
}`}
>
<div className="flex items-center justify-between">
<div>
<span
className={`text-xs font-semibold tracking-wider ${
step.status === "complete"
? "text-primary"
: "text-gray-500 dark:text-gray-400"
}`}
>
{t("landingPage.step")} {step.id}
</span>
<h3 className="text-lg font-bold text-gray-900 dark:text-white mt-1">
{step.name}
</h3>
</div>
{step.status === "current" && (
<span className="inline-flex items-center px-2.5 py-0.5 rounded-full text-xs font-medium bg-primary text-white animate-pulse">
{t("statusBar.In Progress")}
</span>
)}
</div>
<p className="mt-2 text-gray-600 dark:text-gray-300">{step.description}</p>
{step.status === "current" && (
<div className="mt-4 pt-4 border-t border-gray-100 dark:border-gray-700">
<div className="flex space-x-4">
<button className="px-4 py-2 bg-primary text-white rounded-md hover:bg-primary-700 transition-colors">
{t("statusBar.Approve")}
</button>
<button className="px-4 py-2 bg-white dark:bg-gray-700 border border-gray-300 dark:border-gray-600 text-gray-700 dark:text-gray-200 rounded-md hover:bg-gray-50 dark:hover:bg-gray-600 transition-colors">
{t("landingPage.requestChanges")}
</button>
</div>
</div>
)}
</div>
</div>
</li>
))}
</ul>
</div>
</div>
{/* Features section */}
<div className="mt-20 bg-white dark:bg-gray-800 rounded-2xl shadow-xl overflow-hidden">
<div className="grid grid-cols-1 md:grid-cols-2">
<div className="p-10 bg-gradient-to-br from-primary to-primary-800 text-white">
<h3 className="text-2xl font-bold mb-6">
{t("landingPage.workflowAutomationBenefits")}
</h3>
<p className="mb-8 opacity-90">
{t("landingPage.workflowAutomationDesc")}
</p>
<div className="space-y-6">
<div className="flex items-start">
<div className="flex-shrink-0 mt-1">
<div className="flex items-center justify-center h-8 w-8 rounded-full bg-white/20">
<svg
className="h-5 w-5"
fill="none"
stroke="currentColor"
viewBox="0 0 24 24"
>
<path
strokeLinecap="round"
strokeLinejoin="round"
strokeWidth="2"
d="M13 7h8m0 0v8m0-8l-8 8-4-4-6 6"
></path>
</svg>
</div>
</div>
<div className="ml-4">
<h4 className="text-sm font-semibold">
{t("landingPage.fasterApprovals")}
</h4>
<p className="mt-1 text-sm opacity-80">
{t("landingPage.reduceDelays")}
</p>
</div>
</div>
<div className="flex items-start">
<div className="flex-shrink-0 mt-1">
<div className="flex items-center justify-center h-8 w-8 rounded-full bg-white/20">
<svg
className="h-5 w-5"
fill="none"
stroke="currentColor"
viewBox="0 0 24 24"
>
<path
strokeLinecap="round"
strokeLinejoin="round"
strokeWidth="2"
d="M9 12l2 2 4-4m5.618-4.016A11.955 11.955 0 0112 2.944a11.955 11.955 0 01-8.618 3.04A12.02 12.02 0 003 9c0 5.591 3.824 10.29 9 11.622 5.176-1.332 9-6.03 9-11.622 0-1.042-.133-2.052-.382-3.016z"
></path>
</svg>
</div>
</div>
<div className="ml-4">
<h4 className="text-sm font-semibold">
{t("landingPage.auditTrail")}
</h4>
<p className="mt-1 text-sm opacity-80">
{t("landingPage.completeRecord")}
</p>
</div>
</div>
</div>
</div>
<div className="p-10">
<h3 className="text-2xl font-bold text-gray-900 dark:text-white mb-6">
{t("landingPage.advancedFeatures")}
</h3>
<div className="grid grid-cols-1 gap-8">
<div className="flex items-start">
<div className="flex-shrink-0">
<div className="flex items-center justify-center h-12 w-12 rounded-xl bg-primary-500/5 text-primary">
<svg
className="h-6 w-6"
fill="none"
viewBox="0 0 24 24"
stroke="currentColor"
>
<path
strokeLinecap="round"
strokeLinejoin="round"
strokeWidth={2}
d="M12 8v4l3 3m6-3a9 9 0 11-18 0 9 9 0 0118 0z"
/>
</svg>
</div>
</div>
<div className="ml-4">
<h4 className="text-lg font-medium text-gray-900 dark:text-white">
{t("landingPage.slaMonitoring")}
</h4>
<p className="mt-2 text-gray-600 dark:text-gray-300">
{t("landingPage.slaMonitoringDesc")}
</p>
</div>
</div>
<div className="flex items-start">
<div className="flex-shrink-0">
<div className="flex items-center justify-center h-12 w-12 rounded-xl bg-primary-500/5 text-primary">
<svg
className="h-6 w-6"
fill="none"
viewBox="0 0 24 24"
stroke="currentColor"
>
<path
strokeLinecap="round"
strokeLinejoin="round"
strokeWidth={2}
d="M9 5H7a2 2 0 00-2 2v12a2 2 0 002 2h10a2 2 0 002-2V7a2 2 0 00-2-2h-2M9 5a2 2 0 002 2h2a2 2 0 002-2M9 5a2 2 0 012-2h2a2 2 0 012 2"
/>
</svg>
</div>
</div>
<div className="ml-4">
<h4 className="text-lg font-medium text-gray-900 dark:text-white">
{t("landingPage.conditionalRouting")}
</h4>
<p className="mt-2 text-gray-600 dark:text-gray-300">
{t("landingPage.conditionalRoutingDesc")}
</p>
</div>
</div>
<div className="flex items-start">
<div className="flex-shrink-0">
<div className="flex items-center justify-center h-12 w-12 rounded-xl bg-primary-500/5 text-primary">
<svg
className="h-6 w-6"
fill="none"
viewBox="0 0 24 24"
stroke="currentColor"
>
<path
strokeLinecap="round"
strokeLinejoin="round"
strokeWidth={2}
d="M8 7V3m8 4V3m-9 8h10M5 21h14a2 2 0 002-2V7a2 2 0 00-2-2H5a2 2 0 00-2 2v12a2 2 0 002 2z"
/>
</svg>
</div>
</div>
<div className="ml-4">
<h4 className="text-lg font-medium text-gray-900 dark:text-white">
{t("delegation.title")}
</h4>
<p className="mt-2 text-gray-600 dark:text-gray-300">
{t("landingPage.temporaryApprovalDelegation")}
</p>
</div>
</div>
</div>
</div>
</div>
</div>
</div>
</div>
);
};
export default WorkflowSection;