From 0d2b1e1bf9cea1f024f96577131ab0d0bae37897 Mon Sep 17 00:00:00 2001 From: Flatlogic Bot Date: Thu, 4 Jun 2026 16:07:56 +0000 Subject: [PATCH] Qweli 1.0.1 --- frontend/src/components/AsideMenuLayer.tsx | 3 +- frontend/src/components/NavBarItem.tsx | 3 +- frontend/src/layouts/Authenticated.tsx | 3 +- frontend/src/menuAside.ts | 6 + frontend/src/pages/index.tsx | 308 ++++++------- frontend/src/pages/qweli-command-center.tsx | 473 ++++++++++++++++++++ frontend/src/pages/search.tsx | 4 +- 7 files changed, 637 insertions(+), 163 deletions(-) create mode 100644 frontend/src/pages/qweli-command-center.tsx 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) => ( -
-
- - Photo by {image?.photographer} on Pexels - -
-
- ); - - const videoBlock = (video) => { - if (video?.video_files?.length > 0) { - return ( -
- -
- - Video by {video.user.name} on Pexels - -
-
) - } - }; - +export default function QweliLanding() { return ( -
+
- {getPageTitle('Starter Page')} + {getPageTitle('Qweli')} + - -
- {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

-
- - - - - -
+
+
+ + + Q + + + Qweli + value-chain finance + + + +
-
- -
-

© 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}

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

© 2026 Qweli. Built for modern business finance.

+
+ Privacy Policy + Login +
+
+
- ); + ) } -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. +

+
+ +
+
+ {(['invoice', 'expense'] as WorkflowType[]).map((type) => ( + + ))} +
+ + + +
+ + + +
+ +