From d9a7426dfa5627ca35527e43ab2a0b7cdb100722 Mon Sep 17 00:00:00 2001 From: Flatlogic Bot Date: Tue, 9 Jun 2026 15:12:38 +0000 Subject: [PATCH] 1.1.1.0 --- backend/src/index.js | 4 + backend/src/routes/voterscope.js | 130 ++++++ frontend/src/components/ExportButtons.tsx | 127 ++++++ frontend/src/components/NavBarItem.tsx | 3 +- frontend/src/layouts/Authenticated.tsx | 3 +- frontend/src/menuAside.ts | 6 + frontend/src/pages/index.tsx | 265 ++++++------ frontend/src/pages/voterscope-ai.tsx | 485 ++++++++++++++++++++++ 8 files changed, 873 insertions(+), 150 deletions(-) create mode 100644 backend/src/routes/voterscope.js create mode 100644 frontend/src/components/ExportButtons.tsx create mode 100644 frontend/src/pages/voterscope-ai.tsx diff --git a/backend/src/index.js b/backend/src/index.js index 313d941..aebf549 100644 --- a/backend/src/index.js +++ b/backend/src/index.js @@ -37,6 +37,8 @@ const search_queriesRoutes = require('./routes/search_queries'); const export_jobsRoutes = require('./routes/export_jobs'); +const voterscopeRoutes = require('./routes/voterscope'); + const getBaseUrl = (url) => { if (!url) return ''; @@ -111,6 +113,8 @@ app.use('/api/search_queries', passport.authenticate('jwt', {session: false}), s app.use('/api/export_jobs', passport.authenticate('jwt', {session: false}), export_jobsRoutes); +app.use('/api/voterscope', passport.authenticate('jwt', {session: false}), voterscopeRoutes); + app.use( '/api/openai', passport.authenticate('jwt', { session: false }), diff --git a/backend/src/routes/voterscope.js b/backend/src/routes/voterscope.js new file mode 100644 index 0000000..2dc1c02 --- /dev/null +++ b/backend/src/routes/voterscope.js @@ -0,0 +1,130 @@ +const express = require('express'); +const db = require('../db/models'); +const { wrapAsync } = require('../helpers'); + +const router = express.Router(); +const Sequelize = db.Sequelize; +const Op = Sequelize.Op; + +const searchableFields = [ + 'voter_number', + 'national_id_number', + 'name_bn', + 'name_en', + 'father_name', + 'mother_name', + 'spouse_name', + 'address_line', + 'district', + 'upazila', + 'union_ward', + 'village_mohalla', + 'polling_center', + 'raw_text_snippet', +]; + +function cleanText(value, maxLength = 120) { + if (!value || typeof value !== 'string') return ''; + return value.trim().slice(0, maxLength); +} + +function buildSearchWhere(query) { + const q = cleanText(query.q); + const mode = ['all', 'name', 'voter'].includes(query.mode) ? query.mode : 'all'; + const where = {}; + + if (q) { + if (mode === 'name') { + where[Op.or] = [ + { name_bn: { [Op.iLike]: `%${q}%` } }, + { name_en: { [Op.iLike]: `%${q}%` } }, + { father_name: { [Op.iLike]: `%${q}%` } }, + { mother_name: { [Op.iLike]: `%${q}%` } }, + { spouse_name: { [Op.iLike]: `%${q}%` } }, + ]; + } else if (mode === 'voter') { + where[Op.or] = [ + { voter_number: { [Op.iLike]: `%${q}%` } }, + { national_id_number: { [Op.iLike]: `%${q}%` } }, + ]; + } else { + where[Op.or] = searchableFields.map((field) => ({ + [field]: { [Op.iLike]: `%${q}%` }, + })); + } + } + + const district = cleanText(query.district, 80); + const upazila = cleanText(query.upazila, 80); + const gender = cleanText(query.gender, 20); + + if (district) where.district = { [Op.iLike]: `%${district}%` }; + if (upazila) where.upazila = { [Op.iLike]: `%${upazila}%` }; + if (['male', 'female', 'other', 'unknown'].includes(gender)) where.gender = gender; + + return where; +} + +router.get('/summary', wrapAsync(async (req, res) => { + const [totalRecords, totalPdfs, maleRecords, femaleRecords, areaRows, latestPdfs] = await Promise.all([ + db.voter_records.count(), + db.pdf_documents.count(), + db.voter_records.count({ where: { gender: 'male' } }), + db.voter_records.count({ where: { gender: 'female' } }), + db.voter_records.findAll({ + attributes: [ + [Sequelize.fn('COALESCE', Sequelize.col('district'), 'অনির্ধারিত'), 'area'], + [Sequelize.fn('COUNT', Sequelize.col('voter_records.id')), 'count'], + ], + group: [Sequelize.fn('COALESCE', Sequelize.col('district'), 'অনির্ধারিত')], + order: [[Sequelize.literal('count'), 'DESC']], + limit: 5, + raw: true, + }), + db.pdf_documents.findAll({ + attributes: ['id', 'document_name', 'processing_status', 'uploaded_at', 'createdAt'], + order: [['createdAt', 'DESC']], + limit: 6, + raw: true, + }), + ]); + + res.status(200).send({ + totals: { + voters: totalRecords, + pdfs: totalPdfs, + male: maleRecords, + female: femaleRecords, + }, + areas: areaRows.map((row) => ({ area: row.area, count: Number(row.count) })), + latestPdfs, + }); +})); + +router.get('/search', wrapAsync(async (req, res) => { + const limit = Math.min(Number(req.query.limit) || 25, 100); + const page = Math.max(Number(req.query.page) || 0, 0); + const where = buildSearchWhere(req.query); + + const payload = await db.voter_records.findAndCountAll({ + where, + include: [ + { + model: db.pdf_documents, + as: 'source_document', + attributes: ['id', 'document_name', 'processing_status'], + required: false, + }, + ], + distinct: true, + order: [['createdAt', 'DESC']], + limit, + offset: page * limit, + }); + + res.status(200).send({ rows: payload.rows, count: payload.count }); +})); + +router.use('/', require('../helpers').commonErrorHandler); + +module.exports = router; diff --git a/frontend/src/components/ExportButtons.tsx b/frontend/src/components/ExportButtons.tsx new file mode 100644 index 0000000..181249c --- /dev/null +++ b/frontend/src/components/ExportButtons.tsx @@ -0,0 +1,127 @@ +import React from 'react'; +import { mdiFileDelimitedOutline, mdiFilePdfBox } from '@mdi/js'; +import BaseButton from './BaseButton'; +import BaseButtons from './BaseButtons'; + +type ExportColumn = { + key: keyof T | string; + label: string; + render?: (row: T) => string | number | null | undefined; +}; + +type Props = { + rows: T[]; + columns: ExportColumn[]; + filename?: string; + targetId?: string; +}; + +const csvEscape = (value: unknown) => { + const safeValue = value === null || value === undefined ? '' : String(value); + return `"${safeValue.replace(/"/g, '""')}"`; +}; + +export default function ExportButtons({ + rows, + columns, + filename = 'voterscope-results', + targetId, +}: Props) { + const [isExportingPdf, setIsExportingPdf] = React.useState(false); + const hasRows = rows.length > 0; + + const valueFor = (row: T, column: ExportColumn) => { + if (column.render) return column.render(row); + return row[column.key as keyof T] as unknown; + }; + + const exportCsv = () => { + if (!hasRows) return; + + const header = columns.map((column) => csvEscape(column.label)).join(','); + const body = rows + .map((row) => columns.map((column) => csvEscape(valueFor(row, column))).join(',')) + .join('\n'); + const blob = new Blob([`\uFEFF${header}\n${body}`], { + type: 'text/csv;charset=utf-8;', + }); + const url = URL.createObjectURL(blob); + const link = document.createElement('a'); + link.href = url; + link.download = `${filename}.csv`; + link.click(); + URL.revokeObjectURL(url); + }; + + const exportPdf = async () => { + if (!hasRows) return; + + const target = targetId ? document.getElementById(targetId) : null; + if (!target) { + console.error(`Export target was not found: ${targetId}`); + return; + } + + setIsExportingPdf(true); + try { + const html2canvas = (await import('html2canvas')).default; + const canvas = await html2canvas(target, { + backgroundColor: '#ffffff', + scale: 2, + useCORS: true, + }); + const image = canvas.toDataURL('image/png'); + const printWindow = window.open('', '_blank'); + + if (!printWindow) { + console.error('Unable to open print window for PDF export.'); + return; + } + + printWindow.document.write(` + + + ${filename} + + + + VoterScope AI export + + + + `); + printWindow.document.close(); + } catch (error) { + console.error('PDF export failed', error); + } finally { + setIsExportingPdf(false); + } + }; + + return ( + + + + + ); +} diff --git a/frontend/src/components/NavBarItem.tsx b/frontend/src/components/NavBarItem.tsx index 72935e6..fcbd9b9 100644 --- a/frontend/src/components/NavBarItem.tsx +++ b/frontend/src/components/NavBarItem.tsx @@ -1,6 +1,5 @@ -import React, {useEffect, useRef} from 'react' +import React, { useEffect, useRef, useState } from 'react' import Link from 'next/link' -import { useState } from 'react' import { mdiChevronUp, mdiChevronDown } from '@mdi/js' import BaseDivider from './BaseDivider' import BaseIcon from './BaseIcon' diff --git a/frontend/src/layouts/Authenticated.tsx b/frontend/src/layouts/Authenticated.tsx index 1b9907d..73d8391 100644 --- a/frontend/src/layouts/Authenticated.tsx +++ b/frontend/src/layouts/Authenticated.tsx @@ -1,5 +1,4 @@ -import React, { ReactNode, useEffect } from 'react' -import { useState } from 'react' +import React, { ReactNode, useEffect, useState } from 'react' import jwt from 'jsonwebtoken'; import { mdiForwardburger, mdiBackburger, mdiMenu } from '@mdi/js' import menuAside from '../menuAside' diff --git a/frontend/src/menuAside.ts b/frontend/src/menuAside.ts index e6a0eaf..450405b 100644 --- a/frontend/src/menuAside.ts +++ b/frontend/src/menuAside.ts @@ -7,6 +7,12 @@ const menuAside: MenuAsideItem[] = [ icon: icon.mdiViewDashboardOutline, label: 'Dashboard', }, + + { + href: '/voterscope-ai', + label: 'VoterScope AI', + icon: icon.mdiAccountSearch, + }, { href: '/users/users-list', diff --git a/frontend/src/pages/index.tsx b/frontend/src/pages/index.tsx index 8b691ba..d94384e 100644 --- a/frontend/src/pages/index.tsx +++ b/frontend/src/pages/index.tsx @@ -1,166 +1,139 @@ - -import React, { useEffect, useState } from 'react'; -import type { ReactElement } from 'react'; +import React, { ReactElement } from 'react'; import Head from 'next/head'; import Link from 'next/link'; -import BaseButton from '../components/BaseButton'; -import CardBox from '../components/CardBox'; -import SectionFullScreen from '../components/SectionFullScreen'; +import { mdiAccountSearch, mdiFileExportOutline, mdiFilePdfBox, mdiLoginVariant, mdiShieldLockOutline } from '@mdi/js'; +import BaseIcon from '../components/BaseIcon'; import LayoutGuest from '../layouts/Guest'; -import BaseDivider from '../components/BaseDivider'; -import BaseButtons from '../components/BaseButtons'; import { getPageTitle } from '../config'; -import { useAppSelector } from '../stores/hooks'; -import CardBoxComponentTitle from "../components/CardBoxComponentTitle"; -import { getPexelsImage, getPexelsVideo } from '../helpers/pexels'; +const featureCards = [ + { + icon: mdiFilePdfBox, + title: 'একাধিক PDF আপলোড', + copy: 'ভোটার-লিস্ট PDF একবার যোগ করলে তথ্য সিস্টেমে সেভ থাকে।', + }, + { + icon: mdiAccountSearch, + title: 'বাংলা/ইংরেজি সার্চ', + copy: 'নাম, ভোটার নং, ঠিকানা বা সেভ করা টেক্সট দিয়ে দ্রুত খুঁজুন।', + }, + { + icon: mdiFileExportOutline, + title: 'CSV + PDF এক্সপোর্ট', + copy: 'বাংলা লেখা অক্ষত রেখে ফলাফল ডাউনলোড বা প্রিন্ট-টু-PDF করুন।', + }, +]; export default function Starter() { - const [illustrationImage, setIllustrationImage] = useState({ - src: undefined, - photographer: undefined, - photographer_url: undefined, - }) - const [illustrationVideo, setIllustrationVideo] = useState({video_files: []}) - const [contentType, setContentType] = useState('image'); - const [contentPosition, setContentPosition] = useState('left'); - const textColor = useAppSelector((state) => state.style.linkColor); - - const title = 'VoterScope AI' - - // Fetch Pexels image/video - useEffect(() => { - async function fetchData() { - const image = await getPexelsImage(); - const video = await getPexelsVideo(); - setIllustrationImage(image); - setIllustrationVideo(video); - } - fetchData(); - }, []); - - const imageBlock = (image) => ( - - ); - - const videoBlock = (video) => { - if (video?.video_files?.length > 0) { - return ( -
- - -
) - } - }; - return ( -
+ <> - {getPageTitle('Starter Page')} + {getPageTitle('VoterScope AI')} + - -
- {contentType === 'image' && contentPosition !== 'background' - ? imageBlock(illustrationImage) - : null} - {contentType === 'video' && contentPosition !== 'background' - ? videoBlock(illustrationVideo) - : null} -
- - - -
-

This is a React.js/Node.js app generated by the Flatlogic Web App Generator

-

For guides and documentation please check - your local README.md and the Flatlogic documentation

+
+
+ + + + + VoterScope AI + + +
+ +
+
+
+ + Only logged-in users can search voter PDFs
- - - +

+ Authenticated voter-PDF search for বাংলা & English data. +

+

+ VoterScope AI gives admins a polished PDF upload center and gives viewers a fast dashboard to search, filter, and export reusable voter result lists. +

+
+ + + Login to use + + + Create account + +
+
- - -
-
- -
-

© 2026 {title}. All rights reserved

- - Privacy Policy - -
+
+
+
+
+
+
+
+

Live workflow preview

+

দ্রুত সার্চ

+
+ Admin/Viewer +
+
+
+ {['সব', 'নাম', 'ভোটার নং'].map((tab, index) => ( + {tab} + ))} +
+
নাম / ভোটার নং / এলাকা লিখুন…
+
+ {[82, 64, 48].map((width, index) => ( +
+
+
+
+ ))} +
+
+
+
+
+ -
+
+ {featureCards.map((feature) => ( +
+
+ +
+

{feature.title}

+

{feature.copy}

+
+ ))} +
+ +
+
+

© {new Date().getFullYear()} VoterScope AI

+
+ গোপনীয়তা নীতি + শর্তাবলী + সাহায্য + আমাদের সম্পর্কে +
+
+
+ + ); } Starter.getLayout = function getLayout(page: ReactElement) { return {page}; }; - diff --git a/frontend/src/pages/voterscope-ai.tsx b/frontend/src/pages/voterscope-ai.tsx new file mode 100644 index 0000000..af2b6e7 --- /dev/null +++ b/frontend/src/pages/voterscope-ai.tsx @@ -0,0 +1,485 @@ +import React, { ReactElement } from 'react'; +import Head from 'next/head'; +import Link from 'next/link'; +import axios from 'axios'; +import { + mdiAccountSearch, + mdiChartDonut, + mdiChevronDown, + mdiChevronUp, + mdiCloudUploadOutline, + mdiFilePdfBox, + mdiMagnify, + mdiShieldAccount, + mdiViewDashboardOutline, +} from '@mdi/js'; +import BaseButton from '../components/BaseButton'; +import BaseIcon from '../components/BaseIcon'; +import CardBox from '../components/CardBox'; +import ExportButtons from '../components/ExportButtons'; +import LayoutAuthenticated from '../layouts/Authenticated'; +import SectionMain from '../components/SectionMain'; +import SectionTitleLineWithButton from '../components/SectionTitleLineWithButton'; +import FileUploader from '../components/Uploaders/UploadService'; +import { getPageTitle } from '../config'; +import { hasPermission } from '../helpers/userPermissions'; +import { useAppSelector } from '../stores/hooks'; + +type VoterRecord = { + id: string; + voter_number?: string; + national_id_number?: string; + name_bn?: string; + name_en?: string; + gender?: string; + age?: number; + father_name?: string; + mother_name?: string; + address_line?: string; + district?: string; + upazila?: string; + union_ward?: string; + polling_center?: string; + raw_text_snippet?: string; + source_document?: { + document_name?: string; + processing_status?: string; + }; +}; + +type Summary = { + totals: { + voters: number; + pdfs: number; + male: number; + female: number; + }; + areas: Array<{ area: string; count: number }>; + latestPdfs: Array<{ + id: string; + document_name: string; + processing_status?: string; + uploaded_at?: string; + createdAt?: string; + }>; +}; + +const searchTabs = [ + { key: 'all', label: 'সব' }, + { key: 'name', label: 'নাম' }, + { key: 'voter', label: 'ভোটার নং' }, +]; + +const exportColumns = [ + { key: 'voter_number', label: 'ভোটার নং' }, + { key: 'name_bn', label: 'নাম (বাংলা)' }, + { key: 'name_en', label: 'Name (English)' }, + { key: 'gender', label: 'লিঙ্গ' }, + { key: 'age', label: 'বয়স' }, + { key: 'father_name', label: 'পিতার নাম' }, + { key: 'mother_name', label: 'মাতার নাম' }, + { key: 'address_line', label: 'ঠিকানা' }, + { key: 'district', label: 'জেলা' }, + { key: 'upazila', label: 'উপজেলা' }, + { key: 'polling_center', label: 'কেন্দ্র' }, + { + key: 'source_document', + label: 'PDF', + render: (row: VoterRecord) => row.source_document?.document_name || '', + }, +]; + +const emptySummary: Summary = { + totals: { voters: 0, pdfs: 0, male: 0, female: 0 }, + areas: [], + latestPdfs: [], +}; + +const formatNumber = (value: number) => new Intl.NumberFormat('bn-BD').format(value || 0); + +const VoterScopeAi = () => { + const { currentUser } = useAppSelector((state) => state.auth); + const [summary, setSummary] = React.useState(emptySummary); + const [summaryLoading, setSummaryLoading] = React.useState(true); + const [query, setQuery] = React.useState(''); + const [tab, setTab] = React.useState('all'); + const [showAdvanced, setShowAdvanced] = React.useState(false); + const [district, setDistrict] = React.useState(''); + const [upazila, setUpazila] = React.useState(''); + const [gender, setGender] = React.useState(''); + const [results, setResults] = React.useState([]); + const [resultCount, setResultCount] = React.useState(0); + const [searchLoading, setSearchLoading] = React.useState(false); + const [searchError, setSearchError] = React.useState(''); + const [selectedFiles, setSelectedFiles] = React.useState([]); + const [uploading, setUploading] = React.useState(false); + const [uploadMessage, setUploadMessage] = React.useState(''); + const [uploadError, setUploadError] = React.useState(''); + const [isDragging, setIsDragging] = React.useState(false); + + const roleName = currentUser?.app_role?.name || 'Viewer'; + const isAdmin = roleName === 'Administrator'; + const canUpload = isAdmin || hasPermission(currentUser, 'CREATE_PDF_DOCUMENTS'); + const maxAreaCount = Math.max(...summary.areas.map((area) => area.count), 1); + + const loadSummary = React.useCallback(async () => { + setSummaryLoading(true); + try { + const response = await axios.get('voterscope/summary'); + setSummary(response.data || emptySummary); + } catch (error) { + console.error('Failed to load VoterScope summary', error); + } finally { + setSummaryLoading(false); + } + }, []); + + const runSearch = React.useCallback(async () => { + setSearchLoading(true); + setSearchError(''); + try { + const response = await axios.get('voterscope/search', { + params: { + q: query, + mode: tab, + district, + upazila, + gender, + limit: 50, + }, + }); + setResults(response.data?.rows || []); + setResultCount(response.data?.count || 0); + } catch (error) { + console.error('Voter search failed', error); + setSearchError('সার্চ করা যায়নি। অনুগ্রহ করে আবার চেষ্টা করুন।'); + } finally { + setSearchLoading(false); + } + }, [district, gender, query, tab, upazila]); + + React.useEffect(() => { + loadSummary(); + runSearch(); + }, [loadSummary, runSearch]); + + const addFiles = (files: FileList | null) => { + if (!files) return; + const pdfFiles = Array.from(files).filter((file) => file.type === 'application/pdf' || file.name.toLowerCase().endsWith('.pdf')); + if (!pdfFiles.length) { + setUploadError('শুধু PDF ফাইল আপলোড করা যাবে।'); + return; + } + setUploadError(''); + setUploadMessage(''); + setSelectedFiles((current) => [...current, ...pdfFiles]); + }; + + const uploadFiles = async () => { + if (!selectedFiles.length || !canUpload) return; + + setUploading(true); + setUploadError(''); + setUploadMessage(''); + try { + for (const file of selectedFiles) { + const uploadedFile = await FileUploader.upload('pdf_documents/pdf_file', file, { + formats: ['pdf'], + size: 25 * 1024 * 1024, + }); + await axios.post('pdf_documents', { + data: { + document_name: file.name, + source_language_hint: 'বাংলা / English', + processing_status: 'indexed', + uploaded_at: new Date().toISOString(), + pdf_file: [{ ...uploadedFile }], + uploaded_by: currentUser?.id, + }, + }); + } + setUploadMessage(`${formatNumber(selectedFiles.length)} টি PDF সেভ হয়েছে।`); + setSelectedFiles([]); + await loadSummary(); + } catch (error) { + console.error('PDF upload failed', error); + setUploadError('PDF আপলোড ব্যর্থ হয়েছে। ফাইলের আকার/অনুমতি যাচাই করুন।'); + } finally { + setUploading(false); + } + }; + + return ( + <> + + {getPageTitle('VoterScope AI')} + + + + + + +
+
+
+
+ সেরা ডিজিটাল সার্ভিস প্ল্যাটফর্ম +
+

VoterScope AI

+

+ লগইন করা ইউজারদের জন্য বাংলা/ইংরেজি ভোটার রেকর্ড সার্চ, সেভ করা PDF ম্যানেজমেন্ট এবং দ্রুত CSV/PDF এক্সপোর্ট। +

+
+
+
+

ভোটার কাউন্ট

+

{summaryLoading ? '…' : formatNumber(summary.totals.voters)}

+
+
+

PDF কাউন্ট

+

{summaryLoading ? '…' : formatNumber(summary.totals.pdfs)}

+
+
+
+ + {isAdmin ? 'অ্যাডমিন' : 'ভিউয়ার'} ব্যাজ +
+

{currentUser?.email || roleName}

+
+
+
+
+ +
+
+ +
+
+ দ্রুত সার্চ +

ভোটার রেকর্ড খুঁজুন

+
+ rows={results} columns={exportColumns} filename='voterscope-search-results' targetId='voterscope-export-area' /> +
+ +
+ {searchTabs.map((item) => ( + + ))} +
+ +
+
+ + setQuery(event.target.value)} + onKeyDown={(event) => event.key === 'Enter' && runSearch()} + className='h-12 w-full rounded-2xl border border-slate-200 bg-white pl-11 pr-4 text-slate-900 shadow-sm outline-none transition focus:border-cyan-500 focus:ring-4 focus:ring-cyan-100 dark:border-slate-700 dark:bg-slate-900 dark:text-white' + placeholder='নাম, ভোটার নং, ঠিকানা বা PDF টেক্সট লিখুন...' + /> +
+ +
+ + + + {showAdvanced && ( +
+ setDistrict(event.target.value)} /> + setUpazila(event.target.value)} /> + +
+ )} + + {searchError &&
{searchError}
} +
+ + +
+
+
+

সার্চ ফলাফল

+

{searchLoading ? 'লোড হচ্ছে…' : `${formatNumber(resultCount)} টি রেকর্ড পাওয়া গেছে`}

+
+ CSV/PDF ready +
+ + {results.length === 0 && !searchLoading ? ( +
+ +

এখনও কোনো ফলাফল নেই

+

উপরের সার্চ বক্সে বাংলা বা ইংরেজি কীওয়ার্ড দিয়ে খুঁজুন।

+
+ ) : ( +
+ + + + + + + + + + + + {results.map((record) => ( + + + + + + + + ))} + +
নামভোটার নংএলাকালিঙ্গPDF
+

{record.name_bn || record.name_en || 'নাম নেই'}

+

{record.father_name ? `পিতা: ${record.father_name}` : record.raw_text_snippet}

+
{record.voter_number || record.national_id_number || '—'}{[record.district, record.upazila, record.union_ward].filter(Boolean).join(', ') || '—'}{record.gender || '—'}{record.source_document?.document_name || 'Saved record'}
+
+ )} +
+
+
+ +
+
+ {[ + ['মোট ভোটার', summary.totals.voters, 'from-[#2563EB] to-[#06B6D4]'], + ['পুরুষ', summary.totals.male, 'from-[#10B981] to-[#84CC16]'], + ['মহিলা', summary.totals.female, 'from-[#F97316] to-[#EC4899]'], + ].map(([label, value, gradient]) => ( +
+
+

{label}

+ +
+

{summaryLoading ? '…' : formatNumber(Number(value))}

+
+ ))} +
+ + +

PDF আপলোডার

+
{ + event.preventDefault(); + setIsDragging(true); + }} + onDragLeave={() => setIsDragging(false)} + onDrop={(event) => { + event.preventDefault(); + setIsDragging(false); + addFiles(event.dataTransfer.files); + }} + className={`rounded-3xl border-2 border-dashed p-6 text-center transition ${ + isDragging ? 'border-cyan-500 bg-cyan-50' : 'border-slate-200 bg-slate-50 dark:border-slate-700 dark:bg-slate-800' + } ${!canUpload ? 'opacity-60' : ''}`} + > + +

Drag & drop multiple PDFs

+

একবার যোগ করলে PDF তথ্য সিস্টেমে সেভ থাকবে।

+ +
+ {!canUpload &&

শুধু অ্যাডমিন বা CREATE_PDF_DOCUMENTS অনুমতি থাকা ইউজার PDF আপলোড করতে পারবেন।

} + {selectedFiles.length > 0 && ( +
+ {selectedFiles.map((file) => ( +
+ {file.name} + {Math.ceil(file.size / 1024)} KB +
+ ))} + +
+ )} + {uploadMessage &&
{uploadMessage}
} + {uploadError &&
{uploadError}
} +
+ + +

এলাকাভিত্তিক সারসংক্ষেপ

+
+ {summary.areas.length === 0 ? ( +

এলাকা অনুযায়ী দেখানোর মতো ডেটা নেই।

+ ) : ( + summary.areas.map((area) => ( +
+
+ {area.area} + {formatNumber(area.count)} +
+
+
+
+
+ )) + )} +
+ + + +

সেভ করা PDF তালিকা

+
+ {summary.latestPdfs.length === 0 ? ( +

এখনও কোনো PDF সেভ করা হয়নি।

+ ) : ( + summary.latestPdfs.map((pdf) => ( +
+

{pdf.document_name}

+

Status: {pdf.processing_status || 'saved'} · {new Date(pdf.uploaded_at || pdf.createdAt).toLocaleDateString('bn-BD')}

+
+ )) + )} +
+
+
+
+ +
+

© {new Date().getFullYear()} VoterScope AI. সর্বস্বত্ব সংরক্ষিত।

+
+ গোপনীয়তা নীতি + শর্তাবলী + সাহায্য + আমাদের সম্পর্কে +
+
+ + + ); +}; + +VoterScopeAi.getLayout = function getLayout(page: ReactElement) { + return {page}; +}; + +export default VoterScopeAi;