diff --git a/frontend/src/components/AsideMenuLayer.tsx b/frontend/src/components/AsideMenuLayer.tsx
index 2332d4e..b018326 100644
--- a/frontend/src/components/AsideMenuLayer.tsx
+++ b/frontend/src/components/AsideMenuLayer.tsx
@@ -3,10 +3,9 @@ import { mdiLogout, mdiClose } from '@mdi/js'
import BaseIcon from './BaseIcon'
import AsideMenuList from './AsideMenuList'
import { MenuAsideItem } from '../interfaces'
-import { useAppSelector } from '../stores/hooks'
+import { useAppDispatch, useAppSelector } from '../stores/hooks'
import Link from 'next/link';
-import { useAppDispatch } from '../stores/hooks';
import { createAsyncThunk } from '@reduxjs/toolkit';
import axios from 'axios';
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 17204aa..d7b22b4 100644
--- a/frontend/src/menuAside.ts
+++ b/frontend/src/menuAside.ts
@@ -7,6 +7,12 @@ const menuAside: MenuAsideItem[] = [
icon: icon.mdiViewDashboardOutline,
label: 'Dashboard',
},
+ {
+ href: '/qweli-command-center',
+ icon: 'mdiCashSync' in icon ? icon['mdiCashSync' as keyof typeof icon] : icon.mdiViewDashboardOutline,
+ label: 'Qweli Command Center',
+ permissions: 'READ_INVOICES'
+ },
{
href: '/users/users-list',
diff --git a/frontend/src/pages/index.tsx b/frontend/src/pages/index.tsx
index c8ade3a..a959298 100644
--- a/frontend/src/pages/index.tsx
+++ b/frontend/src/pages/index.tsx
@@ -1,166 +1,166 @@
+import React from 'react'
+import type { ReactElement } from 'react'
+import Head from 'next/head'
+import Link from 'next/link'
+import BaseButton from '../components/BaseButton'
+import LayoutGuest from '../layouts/Guest'
+import { getPageTitle } from '../config'
-import React, { useEffect, useState } from 'react';
-import type { 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 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 modules = [
+ 'Contacts and partner profiles',
+ 'Invoices, quotes, and payments',
+ 'Bills, expenses, and approvals',
+ 'Inventory and service catalog',
+ 'Bank movement and audit trail',
+ 'Cashflow and tax readiness',
+]
+const valueChain = [
+ { title: 'Owners', text: 'See cash position, upcoming dues, and business health at a glance.' },
+ { title: 'Accountants', text: 'Review clean source records before month-end and tax filing.' },
+ { title: 'Suppliers', text: 'Track bills, receipts, and the cost side of every relationship.' },
+ { title: 'Customers', text: 'Move from quote to invoice to payment with fewer handoffs.' },
+]
-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('video');
- const [contentPosition, setContentPosition] = useState('left');
- const textColor = useAppSelector((state) => state.style.linkColor);
-
- const title = 'Qweli Accounting Suite'
-
- // 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 (
-
-
-
- Your browser does not support the video tag.
-
-
-
)
- }
- };
-
+export default function QweliLanding() {
return (
-
+
-
{getPageTitle('Starter Page')}
+
{getPageTitle('Qweli')}
+
-
-
- {contentType === 'image' && contentPosition !== 'background'
- ? imageBlock(illustrationImage)
- : null}
- {contentType === 'video' && contentPosition !== 'background'
- ? videoBlock(illustrationVideo)
- : null}
-
-
-
-
-
-
-
-
-
-
-
+
+
+
+
+ Q
+
+
+ Qweli
+ value-chain finance
+
+
+
+ Modules
+ Workflow
+ Login
+
+
-
-
-
-
© 2026 {title} . All rights reserved
-
- Privacy Policy
-
-
+
+
+
+
+
+
+
+
+ Xero-inspired, value-chain ready
+
+
+ Accounting that sees the whole business network.
+
+
+ Qweli brings owners, accountants, customers, suppliers, products, invoices, expenses, and payments into one
+ clean operating workspace so cashflow decisions happen faster.
+
+
+
+
+
+
+ {['Invoices', 'Expenses', 'Inventory'].map((item) => (
+
+
{item}
+
ready module
+
+ ))}
+
+
+
+
+
+
+
+
Today’s cash signal
+
+18.4%
+
+
+ live slice
+
+
+
+ {[
+ ['Customer invoice drafted', '$2,400', 'bg-[#5CF2B8]'],
+ ['Supplier expense submitted', '$380', 'bg-[#FFB84D]'],
+ ['Payment matching queued', '$1,120', 'bg-[#61A5FF]'],
+ ].map(([label, amount, color]) => (
+
+
+
+ {label}
+
+
{amount}
+
+ ))}
+
+
+
+
+
+
+
+
+
Core modules
+
One product layer for the entire value chain.
+
+
+ {modules.map((module) => (
+
+
{module}
+
+ Connected to Qweli’s authenticated workspace so teams can move from capture to review to source detail.
+
+
+ ))}
+
+
+
+
+
+
+ {valueChain.map((item, index) => (
+
+
+ {index + 1}
+
+
{item.title}
+
{item.text}
+
+ ))}
+
+
+
+
+
+
- );
+ )
}
-Starter.getLayout = function getLayout(page: ReactElement) {
- return {page} ;
-};
-
+QweliLanding.getLayout = function getLayout(page: ReactElement) {
+ return {page}
+}
diff --git a/frontend/src/pages/qweli-command-center.tsx b/frontend/src/pages/qweli-command-center.tsx
new file mode 100644
index 0000000..d9f3e7b
--- /dev/null
+++ b/frontend/src/pages/qweli-command-center.tsx
@@ -0,0 +1,473 @@
+import { mdiChartTimelineVariant } from '@mdi/js'
+import axios from 'axios'
+import Head from 'next/head'
+import React, { ReactElement, useEffect, useMemo, useState } from 'react'
+import BaseButton from '../components/BaseButton'
+import CardBox from '../components/CardBox'
+import LayoutAuthenticated from '../layouts/Authenticated'
+import SectionMain from '../components/SectionMain'
+import SectionTitleLineWithButton from '../components/SectionTitleLineWithButton'
+import { getPageTitle } from '../config'
+
+type WorkflowType = 'invoice' | 'expense'
+
+type QweliEntry = {
+ id: string
+ type: WorkflowType
+ label: string
+ amount: number
+ status?: string
+ date?: string
+ href: string
+ description?: string
+}
+
+type MetricCard = {
+ label: string
+ value: string
+ caption: string
+ accent: string
+}
+
+const currency = new Intl.NumberFormat('en-US', {
+ style: 'currency',
+ currency: 'USD',
+ maximumFractionDigits: 0,
+})
+
+const parseAmount = (value: unknown) => {
+ const amount = Number(value ?? 0)
+ return Number.isFinite(amount) ? amount : 0
+}
+
+const normalizeRows = (response: unknown) => {
+ const data = response as { data?: { rows?: unknown[] } }
+ return Array.isArray(data?.data?.rows) ? data.data.rows : []
+}
+
+const formatDate = (value?: string) => {
+ if (!value) return 'Today'
+ const date = new Date(value)
+ if (Number.isNaN(date.getTime())) return 'Today'
+ return date.toLocaleDateString('en-US', { month: 'short', day: 'numeric', year: 'numeric' })
+}
+
+const QweliCommandCenter = () => {
+ const [entries, setEntries] = useState([])
+ const [contactsCount, setContactsCount] = useState(0)
+ const [productsCount, setProductsCount] = useState(0)
+ const [isLoading, setIsLoading] = useState(true)
+ const [isSaving, setIsSaving] = useState(false)
+ const [message, setMessage] = useState('')
+ const [error, setError] = useState('')
+ const [selectedEntry, setSelectedEntry] = useState(null)
+ const [form, setForm] = useState({
+ type: 'invoice' as WorkflowType,
+ counterparty: '',
+ amount: '1250',
+ tax: '0',
+ dueInDays: '14',
+ description: 'Website services and support retainer',
+ })
+
+ const loadWorkspace = async () => {
+ setIsLoading(true)
+ setError('')
+
+ try {
+ const [invoicesResult, expensesResult, contactsResult, productsResult] = await Promise.allSettled([
+ axios.get('invoices?limit=8&page=0'),
+ axios.get('expenses?limit=8&page=0'),
+ axios.get('contacts?limit=1&page=0'),
+ axios.get('products?limit=1&page=0'),
+ ])
+
+ const failed = [invoicesResult, expensesResult, contactsResult, productsResult].filter(
+ (result) => result.status === 'rejected',
+ )
+
+ if (failed.length) {
+ console.error('Qweli workspace load failed', failed)
+ setError('Some Qweli records could not be loaded. Check permissions or try again.')
+ }
+
+ const invoices = invoicesResult.status === 'fulfilled' ? normalizeRows(invoicesResult.value) : []
+ const expenses = expensesResult.status === 'fulfilled' ? normalizeRows(expensesResult.value) : []
+
+ const normalizedInvoices = invoices.map((item: any) => ({
+ id: item.id,
+ type: 'invoice' as WorkflowType,
+ label: item.invoice_number || item.reference || 'Customer invoice',
+ amount: parseAmount(item.amount_due || item.total),
+ status: item.status || 'draft',
+ date: item.due_date || item.issue_date,
+ href: `/invoices/${item.id}`,
+ description: item.customer_notes || item.reference,
+ }))
+
+ const normalizedExpenses = expenses.map((item: any) => ({
+ id: item.id,
+ type: 'expense' as WorkflowType,
+ label: item.description || 'Supplier expense',
+ amount: parseAmount(item.amount),
+ status: item.status || 'draft',
+ date: item.expense_date,
+ href: `/expenses/${item.id}`,
+ description: item.description,
+ }))
+
+ setContactsCount(
+ contactsResult.status === 'fulfilled' ? Number(contactsResult.value.data?.count ?? 0) : 0,
+ )
+ setProductsCount(
+ productsResult.status === 'fulfilled' ? Number(productsResult.value.data?.count ?? 0) : 0,
+ )
+
+ const nextEntries = [...normalizedInvoices, ...normalizedExpenses]
+ .sort((a, b) => new Date(b.date || '').getTime() - new Date(a.date || '').getTime())
+ .slice(0, 10)
+
+ setEntries(nextEntries)
+ setSelectedEntry(nextEntries[0] ?? null)
+ } catch (loadError) {
+ console.error('Qweli workspace load crashed', loadError)
+ setError('Unable to load the Qweli command center right now.')
+ } finally {
+ setIsLoading(false)
+ }
+ }
+
+ useEffect(() => {
+ loadWorkspace()
+ }, [])
+
+ const totals = useMemo(() => {
+ const receivables = entries
+ .filter((entry) => entry.type === 'invoice')
+ .reduce((sum, entry) => sum + entry.amount, 0)
+ const spend = entries
+ .filter((entry) => entry.type === 'expense')
+ .reduce((sum, entry) => sum + entry.amount, 0)
+
+ return {
+ receivables,
+ spend,
+ net: receivables - spend,
+ }
+ }, [entries])
+
+ const metricCards: MetricCard[] = [
+ {
+ label: 'Receivables',
+ value: currency.format(totals.receivables),
+ caption: 'Customer invoices in the workspace',
+ accent: 'from-emerald-400 to-teal-600',
+ },
+ {
+ label: 'Spend captured',
+ value: currency.format(totals.spend),
+ caption: 'Supplier costs and reimbursables',
+ accent: 'from-orange-400 to-rose-500',
+ },
+ {
+ label: 'Net cash signal',
+ value: currency.format(totals.net),
+ caption: 'Simple cashflow view for this slice',
+ accent: 'from-sky-400 to-indigo-600',
+ },
+ ]
+
+ const submitCapture = async (event: React.FormEvent) => {
+ event.preventDefault()
+ setMessage('')
+ setError('')
+
+ const amount = parseAmount(form.amount)
+ const tax = parseAmount(form.tax)
+ const dueInDays = Number(form.dueInDays || 0)
+
+ if (!form.counterparty.trim()) {
+ setError('Add a customer, supplier, or partner name before saving.')
+ return
+ }
+
+ if (amount <= 0) {
+ setError('Amount must be greater than zero.')
+ return
+ }
+
+ setIsSaving(true)
+
+ try {
+ const today = new Date()
+ const dueDate = new Date(today)
+ dueDate.setDate(today.getDate() + (Number.isFinite(dueInDays) ? dueInDays : 14))
+
+ if (form.type === 'invoice') {
+ const subtotal = Math.max(amount - tax, 0)
+ await axios.post('invoices', {
+ data: {
+ invoice_number: `QW-${Date.now().toString().slice(-6)}`,
+ status: 'draft',
+ issue_date: today.toISOString(),
+ due_date: dueDate.toISOString(),
+ subtotal,
+ tax_total: tax,
+ total: amount,
+ amount_due: amount,
+ reference: form.counterparty.trim(),
+ customer_notes: form.description.trim(),
+ attachments: [],
+ },
+ })
+ } else {
+ await axios.post('expenses', {
+ data: {
+ expense_type: 'general',
+ status: 'submitted',
+ expense_date: today.toISOString(),
+ amount,
+ description: `${form.counterparty.trim()} — ${form.description.trim()}`,
+ receipt_files: [],
+ },
+ })
+ }
+
+ setMessage(`${form.type === 'invoice' ? 'Invoice' : 'Expense'} captured and added to Qweli.`)
+ await loadWorkspace()
+ } catch (saveError) {
+ console.error('Qweli quick capture failed', saveError)
+ setError('Could not save this record. Please check required permissions and try again.')
+ } finally {
+ setIsSaving(false)
+ }
+ }
+
+ return (
+ <>
+
+ {getPageTitle('Qweli Command Center')}
+
+
+
+
+
+
+
+
+
+
+ Value-chain finance
+
+
+ Capture money in, money out, and the people behind every transaction.
+
+
+ This first Qweli workflow turns the generic accounting tables into a guided operating console for owners,
+ accountants, suppliers, and customer-facing teams.
+
+
+
+
Connected value chain
+
+
+
{contactsCount}
+
Contacts
+
+
+
{productsCount}
+
Products
+
+
+
+
+
+
+
+ {metricCards.map((metric) => (
+
+
+ {metric.label}
+ {metric.value}
+ {metric.caption}
+
+ ))}
+
+
+
+
+
+
Quick capture
+
+ Create a draft invoice or submitted expense without leaving the cashflow cockpit.
+
+
+
+
+
+
+
+
+
+
Recent money movement
+
Create, confirm, select, and open the source record.
+
+
+
+
+ {isLoading ? (
+
+ Loading the latest Qweli records...
+
+ ) : entries.length === 0 ? (
+
+
No invoices or expenses yet.
+
Use quick capture to create the first money movement.
+
+ ) : (
+
+
+ {entries.map((entry) => (
+
setSelectedEntry(entry)}
+ className={`w-full rounded-2xl border p-4 text-left transition hover:-translate-y-0.5 hover:shadow-md ${
+ selectedEntry?.id === entry.id
+ ? 'border-emerald-400 bg-emerald-50 dark:bg-emerald-900/20'
+ : 'border-slate-200 bg-white dark:border-dark-700 dark:bg-dark-900'
+ }`}
+ >
+
+
+ {entry.type}
+
+ {currency.format(entry.amount)}
+
+ {entry.label}
+ {formatDate(entry.date)} · {entry.status}
+
+ ))}
+
+
+
+ {selectedEntry ? (
+ <>
+
Selected record
+
{selectedEntry.label}
+
+ {selectedEntry.description || 'No memo was added to this record yet.'}
+
+
+
+
Amount
+ {currency.format(selectedEntry.amount)}
+
+
+
Status
+ {selectedEntry.status}
+
+
+
+ >
+ ) : (
+
Select a record to see detail.
+ )}
+
+
+ )}
+
+
+
+ >
+ )
+}
+
+QweliCommandCenter.getLayout = function getLayout(page: ReactElement) {
+ return {page}
+}
+
+export default QweliCommandCenter
diff --git a/frontend/src/pages/search.tsx b/frontend/src/pages/search.tsx
index 00f5168..005eb07 100644
--- a/frontend/src/pages/search.tsx
+++ b/frontend/src/pages/search.tsx
@@ -1,9 +1,7 @@
import React, { ReactElement, useEffect, useState } from 'react';
import Head from 'next/head';
import 'react-datepicker/dist/react-datepicker.css';
-import { useAppDispatch } from '../stores/hooks';
-
-import { useAppSelector } from '../stores/hooks';
+import { useAppDispatch, useAppSelector } from '../stores/hooks';
import { useRouter } from 'next/router';
import LayoutAuthenticated from '../layouts/Authenticated';