Compare commits
3 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
547bf79d10 | ||
|
|
c15f7371d4 | ||
|
|
b0b2dc530c |
16419
backend/package-lock.json
generated
Normal file
16419
backend/package-lock.json
generated
Normal file
File diff suppressed because it is too large
Load Diff
@ -31,9 +31,9 @@
|
||||
"passport-google-oauth2": "^0.2.0",
|
||||
"passport-jwt": "^4.0.1",
|
||||
"passport-microsoft": "^0.1.0",
|
||||
"pg": "8.4.1",
|
||||
"pg-hstore": "2.3.4",
|
||||
"sequelize": "6.35.2",
|
||||
"pg": "^8.4.1",
|
||||
"pg-hstore": "^2.3.4",
|
||||
"sequelize": "^6.35.2",
|
||||
"sequelize-json-schema": "^2.1.1",
|
||||
"sqlite": "4.0.15",
|
||||
"swagger-jsdoc": "^6.2.8",
|
||||
|
||||
@ -461,12 +461,12 @@ module.exports = class Courier_profilesDBApi {
|
||||
|
||||
|
||||
{
|
||||
model: db.file,
|
||||
model: db.files,
|
||||
as: 'verification_files',
|
||||
},
|
||||
|
||||
{
|
||||
model: db.file,
|
||||
model: db.files,
|
||||
as: 'profile_images',
|
||||
},
|
||||
|
||||
|
||||
@ -36,7 +36,7 @@ module.exports = class FileDBApi {
|
||||
);
|
||||
|
||||
for (const file of inexistentFiles) {
|
||||
await db.file.create(
|
||||
await db.files.create(
|
||||
{
|
||||
belongsTo: relation.belongsTo,
|
||||
belongsToColumn: relation.belongsToColumn,
|
||||
@ -62,7 +62,7 @@ module.exports = class FileDBApi {
|
||||
) {
|
||||
const transaction = (options && options.transaction) || undefined;
|
||||
|
||||
const filesToDelete = await db.file.findAll({
|
||||
const filesToDelete = await db.files.findAll({
|
||||
where: {
|
||||
belongsTo: relation.belongsTo,
|
||||
belongsToId: relation.belongsToId,
|
||||
|
||||
@ -456,12 +456,12 @@ module.exports = class Merchant_profilesDBApi {
|
||||
|
||||
|
||||
{
|
||||
model: db.file,
|
||||
model: db.files,
|
||||
as: 'logo_images',
|
||||
},
|
||||
|
||||
{
|
||||
model: db.file,
|
||||
model: db.files,
|
||||
as: 'banner_images',
|
||||
},
|
||||
|
||||
|
||||
@ -367,7 +367,7 @@ module.exports = class Product_categoriesDBApi {
|
||||
|
||||
|
||||
{
|
||||
model: db.file,
|
||||
model: db.files,
|
||||
as: 'category_images',
|
||||
},
|
||||
|
||||
|
||||
@ -460,7 +460,7 @@ module.exports = class ProductsDBApi {
|
||||
|
||||
|
||||
{
|
||||
model: db.file,
|
||||
model: db.files,
|
||||
as: 'product_images',
|
||||
},
|
||||
|
||||
|
||||
@ -498,7 +498,7 @@ module.exports = class StoresDBApi {
|
||||
|
||||
|
||||
{
|
||||
model: db.file,
|
||||
model: db.files,
|
||||
as: 'store_images',
|
||||
},
|
||||
|
||||
|
||||
@ -448,7 +448,7 @@ module.exports = class Support_ticketsDBApi {
|
||||
|
||||
|
||||
{
|
||||
model: db.file,
|
||||
model: db.files,
|
||||
as: 'attachment_files',
|
||||
},
|
||||
|
||||
|
||||
@ -577,7 +577,7 @@ module.exports = class UsersDBApi {
|
||||
|
||||
|
||||
{
|
||||
model: db.file,
|
||||
model: db.files,
|
||||
as: 'avatar',
|
||||
},
|
||||
|
||||
|
||||
@ -163,7 +163,7 @@ rating_count: {
|
||||
|
||||
|
||||
|
||||
db.courier_profiles.hasMany(db.file, {
|
||||
db.courier_profiles.hasMany(db.files, {
|
||||
as: 'verification_files',
|
||||
foreignKey: 'belongsToId',
|
||||
constraints: false,
|
||||
@ -173,7 +173,7 @@ rating_count: {
|
||||
},
|
||||
});
|
||||
|
||||
db.courier_profiles.hasMany(db.file, {
|
||||
db.courier_profiles.hasMany(db.files, {
|
||||
as: 'profile_images',
|
||||
foreignKey: 'belongsToId',
|
||||
constraints: false,
|
||||
|
||||
@ -1,6 +1,6 @@
|
||||
module.exports = function(sequelize, DataTypes) {
|
||||
const file = sequelize.define(
|
||||
'file',
|
||||
'files',
|
||||
{
|
||||
id: {
|
||||
type: DataTypes.UUID,
|
||||
@ -40,11 +40,11 @@ module.exports = function(sequelize, DataTypes) {
|
||||
);
|
||||
|
||||
file.associate = (db) => {
|
||||
db.file.belongsTo(db.users, {
|
||||
db.files.belongsTo(db.users, {
|
||||
as: 'createdBy',
|
||||
});
|
||||
|
||||
db.file.belongsTo(db.users, {
|
||||
db.files.belongsTo(db.users, {
|
||||
as: 'updatedBy',
|
||||
});
|
||||
};
|
||||
|
||||
@ -171,7 +171,7 @@ status: {
|
||||
|
||||
|
||||
|
||||
db.merchant_profiles.hasMany(db.file, {
|
||||
db.merchant_profiles.hasMany(db.files, {
|
||||
as: 'logo_images',
|
||||
foreignKey: 'belongsToId',
|
||||
constraints: false,
|
||||
@ -181,7 +181,7 @@ status: {
|
||||
},
|
||||
});
|
||||
|
||||
db.merchant_profiles.hasMany(db.file, {
|
||||
db.merchant_profiles.hasMany(db.files, {
|
||||
as: 'banner_images',
|
||||
foreignKey: 'belongsToId',
|
||||
constraints: false,
|
||||
|
||||
@ -119,7 +119,7 @@ is_active: {
|
||||
|
||||
|
||||
|
||||
db.product_categories.hasMany(db.file, {
|
||||
db.product_categories.hasMany(db.files, {
|
||||
as: 'category_images',
|
||||
foreignKey: 'belongsToId',
|
||||
constraints: false,
|
||||
|
||||
@ -166,7 +166,7 @@ is_featured: {
|
||||
|
||||
|
||||
|
||||
db.products.hasMany(db.file, {
|
||||
db.products.hasMany(db.files, {
|
||||
as: 'product_images',
|
||||
foreignKey: 'belongsToId',
|
||||
constraints: false,
|
||||
|
||||
@ -219,7 +219,7 @@ closed_at: {
|
||||
|
||||
|
||||
|
||||
db.stores.hasMany(db.file, {
|
||||
db.stores.hasMany(db.files, {
|
||||
as: 'store_images',
|
||||
foreignKey: 'belongsToId',
|
||||
constraints: false,
|
||||
|
||||
@ -195,7 +195,7 @@ resolved_at: {
|
||||
|
||||
|
||||
|
||||
db.support_tickets.hasMany(db.file, {
|
||||
db.support_tickets.hasMany(db.files, {
|
||||
as: 'attachment_files',
|
||||
foreignKey: 'belongsToId',
|
||||
constraints: false,
|
||||
|
||||
@ -276,7 +276,7 @@ provider: {
|
||||
|
||||
|
||||
|
||||
db.users.hasMany(db.file, {
|
||||
db.users.hasMany(db.files, {
|
||||
as: 'avatar',
|
||||
foreignKey: 'belongsToId',
|
||||
constraints: false,
|
||||
|
||||
@ -102,9 +102,8 @@ router.use(checkCrudPermissions('orders'));
|
||||
router.post('/', wrapAsync(async (req, res) => {
|
||||
const referer = req.headers.referer || `${req.protocol}://${req.hostname}${req.originalUrl}`;
|
||||
const link = new URL(referer);
|
||||
await OrdersService.create(req.body.data, req.currentUser, true, link.host);
|
||||
const payload = true;
|
||||
res.status(200).send(payload);
|
||||
const createdOrder = await OrdersService.create(req.body.data, req.currentUser, true, link.host);
|
||||
res.status(200).send(createdOrder);
|
||||
}));
|
||||
|
||||
/**
|
||||
|
||||
@ -15,7 +15,7 @@ module.exports = class OrdersService {
|
||||
static async create(data, currentUser) {
|
||||
const transaction = await db.sequelize.transaction();
|
||||
try {
|
||||
await OrdersDBApi.create(
|
||||
const createdOrder = await OrdersDBApi.create(
|
||||
data,
|
||||
{
|
||||
currentUser,
|
||||
@ -24,6 +24,7 @@ module.exports = class OrdersService {
|
||||
);
|
||||
|
||||
await transaction.commit();
|
||||
return createdOrder;
|
||||
} catch (error) {
|
||||
await transaction.rollback();
|
||||
throw error;
|
||||
@ -135,4 +136,3 @@ module.exports = class OrdersService {
|
||||
|
||||
};
|
||||
|
||||
|
||||
|
||||
2799
backend/yarn.lock
2799
backend/yarn.lock
File diff suppressed because it is too large
Load Diff
52
frontend/public/locales/en-US/common.json
Normal file
52
frontend/public/locales/en-US/common.json
Normal file
@ -0,0 +1,52 @@
|
||||
{
|
||||
"pages": {
|
||||
"dashboard": {
|
||||
"pageTitle": "Dashboard",
|
||||
"overview": "Overview",
|
||||
"loadingWidgets": "Loading widgets...",
|
||||
"loading": "Loading..."
|
||||
},
|
||||
"login": {
|
||||
"pageTitle": "Login",
|
||||
|
||||
"form": {
|
||||
"loginLabel": "Login",
|
||||
"loginHelp": "Please enter your login",
|
||||
"passwordLabel": "Password",
|
||||
"passwordHelp": "Please enter your password",
|
||||
"remember": "Remember",
|
||||
"forgotPassword": "Forgot password?",
|
||||
"loginButton": "Login",
|
||||
"loading": "Loading...",
|
||||
"noAccountYet": "Don’t have an account yet?",
|
||||
"newAccount": "New Account"
|
||||
},
|
||||
|
||||
"pexels": {
|
||||
"photoCredit": "Photo by {{photographer}} on Pexels",
|
||||
"videoCredit": "Video by {{name}} on Pexels",
|
||||
"videoUnsupported": "Your browser does not support the video tag."
|
||||
},
|
||||
|
||||
"footer": {
|
||||
"copyright": "© {{year}} {{title}}. All rights reserved",
|
||||
"privacy": "Privacy Policy"
|
||||
}
|
||||
}
|
||||
},
|
||||
"components": {
|
||||
"widgetCreator": {
|
||||
"title": "Create Chart or Widget",
|
||||
"helpText": "Describe your new widget or chart in natural language. For example: \"Number of admin users\" OR \"red chart with number of closed contracts grouped by month\"",
|
||||
"settingsTitle": "Widget Creator Settings",
|
||||
"settingsDescription": "What role are we showing and creating widgets for?",
|
||||
"doneButton": "Done",
|
||||
"loading": "Loading..."
|
||||
},
|
||||
"search": {
|
||||
"placeholder": "Search",
|
||||
"required": "Required",
|
||||
"minLength": "Minimum length: {{count}} characters"
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -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';
|
||||
|
||||
|
||||
@ -3,6 +3,11 @@ import Select, { components, SingleValueProps, OptionProps } from 'react-select'
|
||||
|
||||
type LanguageOption = { label: string; value: string };
|
||||
|
||||
type Props = {
|
||||
value?: string;
|
||||
onChange?: (value: string) => void;
|
||||
};
|
||||
|
||||
const LANGS: LanguageOption[] = [
|
||||
{ value: 'en', label: '🇬🇧 EN' },
|
||||
{ value: 'fr', label: '🇫🇷 FR' },
|
||||
@ -22,7 +27,7 @@ const SingleVal = (props: SingleValueProps<LanguageOption, false>) => (
|
||||
</components.SingleValue>
|
||||
);
|
||||
|
||||
const LanguageSwitcher: React.FC = () => {
|
||||
const LanguageSwitcher: React.FC<Props> = ({ value, onChange }) => {
|
||||
const [mounted, setMounted] = useState(false);
|
||||
const [selected, setSelected] = useState<LanguageOption>(LANGS[0]);
|
||||
|
||||
@ -30,9 +35,20 @@ const LanguageSwitcher: React.FC = () => {
|
||||
setMounted(true);
|
||||
}, []);
|
||||
|
||||
useEffect(() => {
|
||||
if (!value) return;
|
||||
const nextSelection = LANGS.find((option) => option.value === value);
|
||||
if (nextSelection) {
|
||||
setSelected(nextSelection);
|
||||
}
|
||||
}, [value]);
|
||||
|
||||
const handleChange = (opt: LanguageOption | null) => {
|
||||
if (!opt) return;
|
||||
setSelected(opt);
|
||||
if (onChange) {
|
||||
onChange(opt.value);
|
||||
}
|
||||
};
|
||||
|
||||
if (!mounted) return null;
|
||||
|
||||
@ -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'
|
||||
|
||||
@ -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'
|
||||
|
||||
@ -112,6 +112,14 @@ const menuAside: MenuAsideItem[] = [
|
||||
icon: 'mdiReceiptText' in icon ? icon['mdiReceiptText' as keyof typeof icon] : icon.mdiTable ?? icon.mdiTable,
|
||||
permissions: 'READ_ORDERS'
|
||||
},
|
||||
{
|
||||
href: '/marketplace/quick-order',
|
||||
label: 'Quick order',
|
||||
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
|
||||
// @ts-ignore
|
||||
icon: icon.mdiCart ?? icon.mdiTable,
|
||||
permissions: 'READ_ORDERS'
|
||||
},
|
||||
{
|
||||
href: '/order_items/order_items-list',
|
||||
label: 'Order items',
|
||||
|
||||
@ -1,166 +1,340 @@
|
||||
|
||||
import React, { useEffect, useState } from 'react';
|
||||
import React, { useMemo, 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 CardBox from '../components/CardBox';
|
||||
import LayoutGuest from '../layouts/Guest';
|
||||
import LanguageSwitcher from '../components/LanguageSwitcher';
|
||||
import { getPageTitle } from '../config';
|
||||
import { useAppSelector } from '../stores/hooks';
|
||||
import CardBoxComponentTitle from "../components/CardBoxComponentTitle";
|
||||
import { getPexelsImage, getPexelsVideo } from '../helpers/pexels';
|
||||
|
||||
const copyByLanguage = {
|
||||
en: {
|
||||
title: 'Community Logistics Marketplace',
|
||||
subtitle:
|
||||
'Connect neighborhood shops, grocery stores, and local couriers with customers who want fast, affordable delivery.',
|
||||
ctaPrimary: 'Start ordering',
|
||||
ctaSecondary: 'Admin console',
|
||||
highlight: 'Create local jobs with walking, bike, and car deliveries.',
|
||||
stats: [
|
||||
{ label: 'Local delivery types', value: 'Walk • Bike • Car' },
|
||||
{ label: 'Typical sales lift', value: '+35%' },
|
||||
{ label: 'Platform commission', value: 'Small per-order fee' },
|
||||
],
|
||||
steps: [
|
||||
{
|
||||
title: 'Shops list products',
|
||||
description:
|
||||
'Retailers and wholesalers publish their catalog and delivery windows.',
|
||||
},
|
||||
{
|
||||
title: 'Customers place orders',
|
||||
description:
|
||||
'Buyers choose a store, checkout quickly, and leave delivery notes.',
|
||||
},
|
||||
{
|
||||
title: 'Couriers deliver locally',
|
||||
description:
|
||||
'Delivery partners accept jobs and complete them within their community.',
|
||||
},
|
||||
],
|
||||
roles: [
|
||||
{
|
||||
title: 'Stores & Wholesalers',
|
||||
description:
|
||||
'Reach more customers, manage inventory, and grow local sales.',
|
||||
},
|
||||
{
|
||||
title: 'Delivery Partners',
|
||||
description:
|
||||
'Earn income with flexible walking, biking, or driving shifts.',
|
||||
},
|
||||
{
|
||||
title: 'Customers',
|
||||
description:
|
||||
'Get essentials quickly from trusted nearby businesses.',
|
||||
},
|
||||
],
|
||||
ctaTitle: 'Launch your community delivery network today.',
|
||||
ctaNote:
|
||||
'Use the admin console to onboard stores, manage couriers, and monitor orders.',
|
||||
},
|
||||
fr: {
|
||||
title: 'Marché logistique communautaire',
|
||||
subtitle:
|
||||
'Reliez les commerces locaux, épiceries et livreurs de proximité aux clients qui veulent une livraison rapide.',
|
||||
ctaPrimary: 'Commander maintenant',
|
||||
ctaSecondary: 'Console admin',
|
||||
highlight: 'Créez des emplois locaux avec livraison à pied, vélo ou voiture.',
|
||||
stats: [
|
||||
{ label: 'Types de livraison', value: 'À pied • Vélo • Voiture' },
|
||||
{ label: 'Hausse des ventes', value: '+35%' },
|
||||
{ label: 'Commission plateforme', value: 'Petite commission par commande' },
|
||||
],
|
||||
steps: [
|
||||
{
|
||||
title: 'Les magasins publient',
|
||||
description:
|
||||
'Les commerçants mettent en ligne leurs produits et créneaux.',
|
||||
},
|
||||
{
|
||||
title: 'Les clients commandent',
|
||||
description:
|
||||
'Les clients choisissent un magasin et ajoutent des notes de livraison.',
|
||||
},
|
||||
{
|
||||
title: 'Les livreurs livrent',
|
||||
description:
|
||||
'Les partenaires acceptent les courses et livrent localement.',
|
||||
},
|
||||
],
|
||||
roles: [
|
||||
{
|
||||
title: 'Commerces & Grossistes',
|
||||
description: 'Touchez plus de clients et augmentez vos ventes locales.',
|
||||
},
|
||||
{
|
||||
title: 'Livreurs',
|
||||
description: 'Gagnez un revenu flexible à pied, à vélo ou en voiture.',
|
||||
},
|
||||
{
|
||||
title: 'Clients',
|
||||
description: 'Recevez rapidement les essentiels près de chez vous.',
|
||||
},
|
||||
],
|
||||
ctaTitle: 'Lancez votre réseau de livraison locale dès maintenant.',
|
||||
ctaNote:
|
||||
'Utilisez la console admin pour gérer magasins, livreurs et commandes.',
|
||||
},
|
||||
es: {
|
||||
title: 'Mercado logístico comunitario',
|
||||
subtitle:
|
||||
'Conecta tiendas locales, supermercados y repartidores con clientes que quieren entregas rápidas.',
|
||||
ctaPrimary: 'Empezar a pedir',
|
||||
ctaSecondary: 'Consola admin',
|
||||
highlight: 'Crea empleos locales con entregas a pie, bici o auto.',
|
||||
stats: [
|
||||
{ label: 'Tipos de entrega', value: 'A pie • Bici • Auto' },
|
||||
{ label: 'Aumento de ventas', value: '+35%' },
|
||||
{ label: 'Comisión plataforma', value: 'Pequeña comisión por pedido' },
|
||||
],
|
||||
steps: [
|
||||
{
|
||||
title: 'Tiendas publican productos',
|
||||
description:
|
||||
'Los comercios cargan su catálogo y horarios de entrega.',
|
||||
},
|
||||
{
|
||||
title: 'Clientes hacen pedidos',
|
||||
description:
|
||||
'Los clientes eligen tienda, pagan rápido y dejan notas.',
|
||||
},
|
||||
{
|
||||
title: 'Repartidores entregan',
|
||||
description:
|
||||
'Los socios aceptan trabajos y entregan en su comunidad.',
|
||||
},
|
||||
],
|
||||
roles: [
|
||||
{
|
||||
title: 'Tiendas y Mayoristas',
|
||||
description:
|
||||
'Llega a más clientes y aumenta las ventas locales.',
|
||||
},
|
||||
{
|
||||
title: 'Repartidores',
|
||||
description: 'Gana ingresos con turnos flexibles.',
|
||||
},
|
||||
{
|
||||
title: 'Clientes',
|
||||
description:
|
||||
'Recibe productos esenciales de negocios cercanos.',
|
||||
},
|
||||
],
|
||||
ctaTitle: 'Lanza tu red de entregas comunitarias hoy.',
|
||||
ctaNote:
|
||||
'Gestiona tiendas, repartidores y pedidos desde la consola.',
|
||||
},
|
||||
de: {
|
||||
title: 'Community-Logistik-Marktplatz',
|
||||
subtitle:
|
||||
'Verbinde lokale Geschäfte, Lebensmittelmärkte und Kuriere mit Kund:innen für schnelle Lieferungen.',
|
||||
ctaPrimary: 'Jetzt bestellen',
|
||||
ctaSecondary: 'Admin-Konsole',
|
||||
highlight:
|
||||
'Schaffe lokale Jobs mit Lieferungen zu Fuß, per Rad oder Auto.',
|
||||
stats: [
|
||||
{ label: 'Lieferarten', value: 'Zu Fuß • Rad • Auto' },
|
||||
{ label: 'Umsatzsteigerung', value: '+35%' },
|
||||
{ label: 'Plattformgebühr', value: 'Kleine Gebühr pro Auftrag' },
|
||||
],
|
||||
steps: [
|
||||
{
|
||||
title: 'Shops listen Produkte',
|
||||
description:
|
||||
'Händler veröffentlichen ihren Katalog und Lieferfenster.',
|
||||
},
|
||||
{
|
||||
title: 'Kunden bestellen',
|
||||
description:
|
||||
'Kund:innen wählen einen Laden, zahlen schnell und hinterlassen Hinweise.',
|
||||
},
|
||||
{
|
||||
title: 'Kuriere liefern lokal',
|
||||
description:
|
||||
'Lieferpartner nehmen Aufträge an und liefern in der Community.',
|
||||
},
|
||||
],
|
||||
roles: [
|
||||
{
|
||||
title: 'Geschäfte & Großhändler',
|
||||
description:
|
||||
'Erreiche mehr Kund:innen und steigere lokale Umsätze.',
|
||||
},
|
||||
{
|
||||
title: 'Lieferpartner',
|
||||
description:
|
||||
'Verdiene flexibel mit Lauf-, Rad- oder Autofahrten.',
|
||||
},
|
||||
{
|
||||
title: 'Kund:innen',
|
||||
description:
|
||||
'Erhalte schnell Essentials von lokalen Betrieben.',
|
||||
},
|
||||
],
|
||||
ctaTitle: 'Starte dein lokales Liefernetzwerk jetzt.',
|
||||
ctaNote:
|
||||
'Nutze die Admin-Konsole, um Shops, Kuriere und Bestellungen zu steuern.',
|
||||
},
|
||||
};
|
||||
|
||||
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('right');
|
||||
const textColor = useAppSelector((state) => state.style.linkColor);
|
||||
|
||||
const title = 'Community Logistics Marketplace'
|
||||
|
||||
// Fetch Pexels image/video
|
||||
useEffect(() => {
|
||||
async function fetchData() {
|
||||
const image = await getPexelsImage();
|
||||
const video = await getPexelsVideo();
|
||||
setIllustrationImage(image);
|
||||
setIllustrationVideo(video);
|
||||
}
|
||||
fetchData();
|
||||
}, []);
|
||||
|
||||
const imageBlock = (image) => (
|
||||
<div
|
||||
className='hidden md:flex flex-col justify-end relative flex-grow-0 flex-shrink-0 w-1/3'
|
||||
style={{
|
||||
backgroundImage: `${
|
||||
image
|
||||
? `url(${image?.src?.original})`
|
||||
: 'linear-gradient(rgba(255, 255, 255, 0.5), rgba(255, 255, 255, 0.5))'
|
||||
}`,
|
||||
backgroundSize: 'cover',
|
||||
backgroundPosition: 'left center',
|
||||
backgroundRepeat: 'no-repeat',
|
||||
}}
|
||||
>
|
||||
<div className='flex justify-center w-full bg-blue-300/20'>
|
||||
<a
|
||||
className='text-[8px]'
|
||||
href={image?.photographer_url}
|
||||
target='_blank'
|
||||
rel='noreferrer'
|
||||
>
|
||||
Photo by {image?.photographer} on Pexels
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
|
||||
const videoBlock = (video) => {
|
||||
if (video?.video_files?.length > 0) {
|
||||
return (
|
||||
<div className='hidden md:flex flex-col justify-end relative flex-grow-0 flex-shrink-0 w-1/3'>
|
||||
<video
|
||||
className='absolute top-0 left-0 w-full h-full object-cover'
|
||||
autoPlay
|
||||
loop
|
||||
muted
|
||||
>
|
||||
<source src={video?.video_files[0]?.link} type='video/mp4'/>
|
||||
Your browser does not support the video tag.
|
||||
</video>
|
||||
<div className='flex justify-center w-full bg-blue-300/20 z-10'>
|
||||
<a
|
||||
className='text-[8px]'
|
||||
href={video?.user?.url}
|
||||
target='_blank'
|
||||
rel='noreferrer'
|
||||
>
|
||||
Video by {video.user.name} on Pexels
|
||||
</a>
|
||||
</div>
|
||||
</div>)
|
||||
}
|
||||
};
|
||||
export default function Home() {
|
||||
const [language, setLanguage] = useState('en');
|
||||
const copy = useMemo(() => copyByLanguage[language] || copyByLanguage.en, [language]);
|
||||
|
||||
return (
|
||||
<div
|
||||
style={
|
||||
contentPosition === 'background'
|
||||
? {
|
||||
backgroundImage: `${
|
||||
illustrationImage
|
||||
? `url(${illustrationImage.src?.original})`
|
||||
: 'linear-gradient(rgba(255, 255, 255, 0.5), rgba(255, 255, 255, 0.5))'
|
||||
}`,
|
||||
backgroundSize: 'cover',
|
||||
backgroundPosition: 'left center',
|
||||
backgroundRepeat: 'no-repeat',
|
||||
}
|
||||
: {}
|
||||
}
|
||||
>
|
||||
<>
|
||||
<Head>
|
||||
<title>{getPageTitle('Starter Page')}</title>
|
||||
<title>{getPageTitle(copy.title)}</title>
|
||||
</Head>
|
||||
|
||||
<SectionFullScreen bg='violet'>
|
||||
<div
|
||||
className={`flex ${
|
||||
contentPosition === 'right' ? 'flex-row-reverse' : 'flex-row'
|
||||
} min-h-screen w-full`}
|
||||
>
|
||||
{contentType === 'image' && contentPosition !== 'background'
|
||||
? imageBlock(illustrationImage)
|
||||
: null}
|
||||
{contentType === 'video' && contentPosition !== 'background'
|
||||
? videoBlock(illustrationVideo)
|
||||
: null}
|
||||
<div className='flex items-center justify-center flex-col space-y-4 w-full lg:w-full'>
|
||||
<CardBox className='w-full md:w-3/5 lg:w-2/3'>
|
||||
<CardBoxComponentTitle title="Welcome to your Community Logistics Marketplace app!"/>
|
||||
|
||||
<div className="space-y-3">
|
||||
<p className='text-center text-gray-500'>This is a React.js/Node.js app generated by the <a className={`${textColor}`} href="https://flatlogic.com/generator">Flatlogic Web App Generator</a></p>
|
||||
<p className='text-center text-gray-500'>For guides and documentation please check
|
||||
your local README.md and the <a className={`${textColor}`} href="https://flatlogic.com/documentation">Flatlogic documentation</a></p>
|
||||
<div className="min-h-screen bg-slate-950 text-white">
|
||||
<header className="border-b border-white/10 bg-slate-950/60 backdrop-blur">
|
||||
<div className="mx-auto flex w-full max-w-6xl items-center justify-between px-6 py-4">
|
||||
<div>
|
||||
<p className="text-sm uppercase tracking-[0.3em] text-emerald-300">LogiLocal</p>
|
||||
<h1 className="text-xl font-semibold">{copy.title}</h1>
|
||||
</div>
|
||||
|
||||
<BaseButtons>
|
||||
<BaseButton
|
||||
href='/login'
|
||||
label='Login'
|
||||
color='info'
|
||||
className='w-full'
|
||||
/>
|
||||
<div className="flex items-center gap-4">
|
||||
<LanguageSwitcher value={language} onChange={setLanguage} />
|
||||
<BaseButtons type="justify-end" className="hidden md:flex" mb="mb-0">
|
||||
<BaseButton href="/login" label="Login" color="white" outline />
|
||||
<BaseButton href="/login" label={copy.ctaSecondary} color="info" />
|
||||
</BaseButtons>
|
||||
</div>
|
||||
</div>
|
||||
</header>
|
||||
|
||||
</BaseButtons>
|
||||
</CardBox>
|
||||
</div>
|
||||
</div>
|
||||
</SectionFullScreen>
|
||||
<div className='bg-black text-white flex flex-col text-center justify-center md:flex-row'>
|
||||
<p className='py-6 text-sm'>© 2026 <span>{title}</span>. All rights reserved</p>
|
||||
<Link className='py-6 ml-4 text-sm' href='/privacy-policy/'>
|
||||
Privacy Policy
|
||||
</Link>
|
||||
</div>
|
||||
<main>
|
||||
<section className="mx-auto grid w-full max-w-6xl gap-10 px-6 py-16 lg:grid-cols-2 lg:items-center">
|
||||
<div className="space-y-6">
|
||||
<span className="inline-flex w-fit items-center rounded-full bg-emerald-400/15 px-4 py-1 text-sm text-emerald-200">
|
||||
{copy.highlight}
|
||||
</span>
|
||||
<h2 className="text-4xl font-semibold leading-tight lg:text-5xl">{copy.title}</h2>
|
||||
<p className="text-lg text-slate-200">{copy.subtitle}</p>
|
||||
<BaseButtons type="justify-start" mb="mb-0">
|
||||
<BaseButton href="/register" label={copy.ctaPrimary} color="success" />
|
||||
<BaseButton href="/login" label="Login" color="white" outline />
|
||||
</BaseButtons>
|
||||
<div className="grid gap-4 pt-6 md:grid-cols-3">
|
||||
{copy.stats.map((item) => (
|
||||
<div key={item.label} className="rounded-2xl border border-white/10 bg-white/5 px-4 py-3">
|
||||
<p className="text-sm text-slate-300">{item.label}</p>
|
||||
<p className="text-base font-semibold text-white">{item.value}</p>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
<CardBox className="border border-white/10 bg-white/5">
|
||||
<div className="space-y-5">
|
||||
<div>
|
||||
<p className="text-sm uppercase tracking-[0.2em] text-emerald-200">Today's flow</p>
|
||||
<h3 className="text-2xl font-semibold text-white">
|
||||
Fast ordering + local delivery
|
||||
</h3>
|
||||
<p className="text-sm text-slate-200">
|
||||
Add stores, list products, and dispatch community couriers in minutes.
|
||||
</p>
|
||||
</div>
|
||||
<div className="space-y-3">
|
||||
{copy.steps.map((step, index) => (
|
||||
<div key={step.title} className="rounded-xl border border-white/10 bg-slate-900/70 p-4">
|
||||
<p className="text-xs uppercase tracking-[0.25em] text-emerald-300">
|
||||
Step {index + 1}
|
||||
</p>
|
||||
<p className="text-base font-semibold text-white">{step.title}</p>
|
||||
<p className="text-sm text-slate-300">{step.description}</p>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
<BaseButtons type="justify-start" mb="mb-0">
|
||||
<BaseButton href="/login" label={copy.ctaSecondary} color="info" />
|
||||
<BaseButton href="/dashboard" label="Go to dashboard" color="white" outline />
|
||||
</BaseButtons>
|
||||
</div>
|
||||
</CardBox>
|
||||
</section>
|
||||
|
||||
<section className="mx-auto w-full max-w-6xl px-6 pb-12">
|
||||
<div className="grid gap-6 md:grid-cols-3">
|
||||
{copy.roles.map((role) => (
|
||||
<div key={role.title} className="rounded-3xl border border-white/10 bg-white/5 p-6">
|
||||
<h3 className="text-lg font-semibold text-white">{role.title}</h3>
|
||||
<p className="mt-3 text-sm text-slate-200">{role.description}</p>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section className="mx-auto w-full max-w-6xl px-6 pb-20">
|
||||
<div className="rounded-3xl border border-emerald-400/20 bg-gradient-to-r from-emerald-500/10 via-slate-900/70 to-blue-500/10 p-8">
|
||||
<div className="grid gap-6 lg:grid-cols-[1.6fr_1fr] lg:items-center">
|
||||
<div>
|
||||
<h3 className="text-2xl font-semibold text-white">{copy.ctaTitle}</h3>
|
||||
<p className="mt-2 text-sm text-slate-200">{copy.ctaNote}</p>
|
||||
</div>
|
||||
<BaseButtons type="justify-start" mb="mb-0">
|
||||
<BaseButton href="/register" label={copy.ctaPrimary} color="success" />
|
||||
<BaseButton href="/login" label={copy.ctaSecondary} color="white" outline />
|
||||
</BaseButtons>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
</main>
|
||||
|
||||
<footer className="border-t border-white/10 bg-slate-950/70">
|
||||
<div className="mx-auto flex w-full max-w-6xl flex-col items-center justify-between gap-4 px-6 py-6 text-sm text-slate-300 md:flex-row">
|
||||
<p>© 2026 LogiLocal. All rights reserved.</p>
|
||||
<div className="flex items-center gap-4">
|
||||
<Link className="hover:text-white" href="/privacy-policy/">
|
||||
Privacy Policy
|
||||
</Link>
|
||||
<Link className="hover:text-white" href="/terms-of-use/">
|
||||
Terms of Use
|
||||
</Link>
|
||||
<Link className="hover:text-white" href="/login">
|
||||
Admin login
|
||||
</Link>
|
||||
</div>
|
||||
</div>
|
||||
</footer>
|
||||
</div>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
Starter.getLayout = function getLayout(page: ReactElement) {
|
||||
Home.getLayout = function getLayout(page: ReactElement) {
|
||||
return <LayoutGuest>{page}</LayoutGuest>;
|
||||
};
|
||||
|
||||
|
||||
340
frontend/src/pages/marketplace/quick-order.tsx
Normal file
340
frontend/src/pages/marketplace/quick-order.tsx
Normal file
@ -0,0 +1,340 @@
|
||||
import React, { ReactElement, useEffect, useMemo, useState } from 'react';
|
||||
import Head from 'next/head';
|
||||
import axios from 'axios';
|
||||
import { mdiCart } from '@mdi/js';
|
||||
|
||||
import LayoutAuthenticated from '../../layouts/Authenticated';
|
||||
import SectionMain from '../../components/SectionMain';
|
||||
import SectionTitleLineWithButton from '../../components/SectionTitleLineWithButton';
|
||||
import CardBox from '../../components/CardBox';
|
||||
import FormField from '../../components/FormField';
|
||||
import BaseButton from '../../components/BaseButton';
|
||||
import BaseButtons from '../../components/BaseButtons';
|
||||
import BaseDivider from '../../components/BaseDivider';
|
||||
import { getPageTitle } from '../../config';
|
||||
import { useAppSelector } from '../../stores/hooks';
|
||||
|
||||
const formatCurrency = (value: number) =>
|
||||
new Intl.NumberFormat('en-US', { style: 'currency', currency: 'USD' }).format(value || 0);
|
||||
|
||||
const QuickOrder = () => {
|
||||
const { currentUser } = useAppSelector((state) => state.auth);
|
||||
const [stores, setStores] = useState<any[]>([]);
|
||||
const [products, setProducts] = useState<any[]>([]);
|
||||
const [recentOrders, setRecentOrders] = useState<any[]>([]);
|
||||
const [selectedStore, setSelectedStore] = useState('');
|
||||
const [selectedProduct, setSelectedProduct] = useState('');
|
||||
const [quantity, setQuantity] = useState(1);
|
||||
const [deliveryFee, setDeliveryFee] = useState(3);
|
||||
const [serviceFee, setServiceFee] = useState(1);
|
||||
const [customerNote, setCustomerNote] = useState('');
|
||||
const [loading, setLoading] = useState(true);
|
||||
const [submitting, setSubmitting] = useState(false);
|
||||
const [errorMessage, setErrorMessage] = useState('');
|
||||
const [successMessage, setSuccessMessage] = useState('');
|
||||
const [lastOrder, setLastOrder] = useState<any | null>(null);
|
||||
|
||||
const filteredProducts = useMemo(() => {
|
||||
if (!selectedStore) return products;
|
||||
return products.filter((product) => product?.store?.id === selectedStore);
|
||||
}, [products, selectedStore]);
|
||||
|
||||
const selectedProductData = useMemo(
|
||||
() => products.find((product) => product.id === selectedProduct),
|
||||
[products, selectedProduct]
|
||||
);
|
||||
|
||||
const unitPrice = Number(selectedProductData?.price || 0);
|
||||
const safeQuantity = Math.max(1, Number(quantity) || 1);
|
||||
const subtotal = unitPrice * safeQuantity;
|
||||
const taxAmount = 0;
|
||||
const totalAmount = subtotal + Number(deliveryFee || 0) + Number(serviceFee || 0) + taxAmount;
|
||||
|
||||
const fetchData = async () => {
|
||||
setLoading(true);
|
||||
setErrorMessage('');
|
||||
try {
|
||||
const [storesResponse, productsResponse, ordersResponse] = await Promise.all([
|
||||
axios.get('/stores'),
|
||||
axios.get('/products'),
|
||||
axios.get('/orders?limit=5&page=0'),
|
||||
]);
|
||||
setStores(storesResponse.data?.rows || []);
|
||||
setProducts(productsResponse.data?.rows || []);
|
||||
setRecentOrders(ordersResponse.data?.rows || []);
|
||||
} catch (error) {
|
||||
console.error('Quick order load error:', error);
|
||||
setErrorMessage('Unable to load marketplace data. Please try again.');
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
fetchData();
|
||||
}, []);
|
||||
|
||||
const handleSubmit = async () => {
|
||||
setErrorMessage('');
|
||||
setSuccessMessage('');
|
||||
|
||||
if (!currentUser?.id) {
|
||||
setErrorMessage('Please sign in to place an order.');
|
||||
return;
|
||||
}
|
||||
if (!selectedStore) {
|
||||
setErrorMessage('Select a store before placing the order.');
|
||||
return;
|
||||
}
|
||||
if (!selectedProduct) {
|
||||
setErrorMessage('Select a product to continue.');
|
||||
return;
|
||||
}
|
||||
|
||||
setSubmitting(true);
|
||||
|
||||
try {
|
||||
const orderPayload = {
|
||||
order_number: `ORD-${Date.now().toString().slice(-6)}`,
|
||||
order_status: 'confirmed',
|
||||
fulfillment_type: 'delivery',
|
||||
subtotal_amount: subtotal,
|
||||
delivery_fee: deliveryFee,
|
||||
service_fee: serviceFee,
|
||||
discount_amount: 0,
|
||||
tax_amount: taxAmount,
|
||||
total_amount: totalAmount,
|
||||
customer_note: customerNote || null,
|
||||
placed_at: new Date().toISOString(),
|
||||
customer: currentUser.id,
|
||||
store: selectedStore,
|
||||
};
|
||||
|
||||
const createdOrderResponse = await axios.post('/orders', { data: orderPayload });
|
||||
const createdOrder = createdOrderResponse.data;
|
||||
|
||||
await axios.post('/order_items', {
|
||||
data: {
|
||||
order: createdOrder?.id,
|
||||
product: selectedProductData?.id,
|
||||
product_name_snapshot: selectedProductData?.product_name,
|
||||
unit_price: unitPrice,
|
||||
quantity: safeQuantity,
|
||||
line_total: subtotal,
|
||||
item_note: customerNote || null,
|
||||
},
|
||||
});
|
||||
|
||||
setLastOrder(createdOrder);
|
||||
setSuccessMessage(`Order ${createdOrder?.order_number || ''} created successfully.`);
|
||||
await fetchData();
|
||||
} catch (error) {
|
||||
console.error('Quick order submit error:', error);
|
||||
setErrorMessage('Could not place the order. Please try again.');
|
||||
} finally {
|
||||
setSubmitting(false);
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<>
|
||||
<Head>
|
||||
<title>{getPageTitle('Quick order')}</title>
|
||||
</Head>
|
||||
<SectionMain>
|
||||
<SectionTitleLineWithButton icon={mdiCart} title="Quick order" main>
|
||||
<BaseButton href="/orders/orders-list" label="Orders list" color="info" />
|
||||
</SectionTitleLineWithButton>
|
||||
|
||||
<div className="grid grid-cols-1 gap-6 xl:grid-cols-3">
|
||||
<CardBox className="xl:col-span-2">
|
||||
<div className="space-y-4">
|
||||
<div>
|
||||
<h2 className="text-xl font-semibold">Create a delivery order</h2>
|
||||
<p className="text-sm text-gray-500">
|
||||
Select a store, add a product, and confirm fees to dispatch a delivery.
|
||||
</p>
|
||||
</div>
|
||||
|
||||
{errorMessage && (
|
||||
<div className="rounded-lg border border-red-200 bg-red-50 px-4 py-3 text-sm text-red-600">
|
||||
{errorMessage}
|
||||
</div>
|
||||
)}
|
||||
{successMessage && (
|
||||
<div className="rounded-lg border border-emerald-200 bg-emerald-50 px-4 py-3 text-sm text-emerald-700">
|
||||
{successMessage}
|
||||
{lastOrder?.id && (
|
||||
<LinkToOrder orderId={lastOrder.id} />
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
|
||||
<FormField label="Store">
|
||||
<select value={selectedStore} onChange={(event) => setSelectedStore(event.target.value)}>
|
||||
<option value="">Select a store</option>
|
||||
{stores.map((store) => (
|
||||
<option key={store.id} value={store.id}>
|
||||
{store.store_name || 'Unnamed store'}
|
||||
</option>
|
||||
))}
|
||||
</select>
|
||||
</FormField>
|
||||
|
||||
{stores.length === 0 && !loading && (
|
||||
<div className="rounded-lg border border-dashed border-gray-300 px-4 py-3 text-sm text-gray-500">
|
||||
No stores yet. Create your first store to enable ordering.
|
||||
<div className="mt-2">
|
||||
<BaseButton href="/stores/stores-new" label="Create store" color="info" />
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
<FormField label="Product">
|
||||
<select
|
||||
value={selectedProduct}
|
||||
onChange={(event) => setSelectedProduct(event.target.value)}
|
||||
disabled={!filteredProducts.length}
|
||||
>
|
||||
<option value="">Select a product</option>
|
||||
{filteredProducts.map((product) => (
|
||||
<option key={product.id} value={product.id}>
|
||||
{product.product_name || 'Unnamed product'}
|
||||
</option>
|
||||
))}
|
||||
</select>
|
||||
</FormField>
|
||||
|
||||
{!filteredProducts.length && !loading && (
|
||||
<div className="rounded-lg border border-dashed border-gray-300 px-4 py-3 text-sm text-gray-500">
|
||||
No products available for this store.
|
||||
<div className="mt-2">
|
||||
<BaseButton href="/products/products-new" label="Add product" color="info" />
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
<FormField label="Quantity">
|
||||
<input
|
||||
type="number"
|
||||
min="1"
|
||||
value={safeQuantity}
|
||||
onChange={(event) => setQuantity(Number(event.target.value))}
|
||||
/>
|
||||
</FormField>
|
||||
|
||||
<FormField label="Delivery fee">
|
||||
<input
|
||||
type="number"
|
||||
min="0"
|
||||
step="0.5"
|
||||
value={deliveryFee}
|
||||
onChange={(event) => setDeliveryFee(Number(event.target.value))}
|
||||
/>
|
||||
</FormField>
|
||||
|
||||
<FormField label="Service fee">
|
||||
<input
|
||||
type="number"
|
||||
min="0"
|
||||
step="0.5"
|
||||
value={serviceFee}
|
||||
onChange={(event) => setServiceFee(Number(event.target.value))}
|
||||
/>
|
||||
</FormField>
|
||||
|
||||
<FormField label="Customer note" hasTextareaHeight>
|
||||
<textarea
|
||||
value={customerNote}
|
||||
onChange={(event) => setCustomerNote(event.target.value)}
|
||||
placeholder="Delivery notes, landmarks, preferred time"
|
||||
/>
|
||||
</FormField>
|
||||
|
||||
<BaseDivider />
|
||||
|
||||
<BaseButtons type="justify-start" mb="mb-0">
|
||||
<BaseButton
|
||||
label={submitting ? 'Placing order...' : 'Place order'}
|
||||
color="success"
|
||||
onClick={handleSubmit}
|
||||
disabled={submitting || loading}
|
||||
/>
|
||||
<BaseButton href="/orders/orders-new" label="Advanced order form" color="white" outline />
|
||||
</BaseButtons>
|
||||
</div>
|
||||
</CardBox>
|
||||
|
||||
<div className="space-y-6">
|
||||
<CardBox>
|
||||
<div className="space-y-4">
|
||||
<h3 className="text-lg font-semibold">Order summary</h3>
|
||||
<div className="text-sm text-gray-500">
|
||||
<p>Product: {selectedProductData?.product_name || 'Select a product'}</p>
|
||||
<p>Store: {stores.find((store) => store.id === selectedStore)?.store_name || 'Select a store'}</p>
|
||||
</div>
|
||||
<div className="space-y-2 text-sm">
|
||||
<SummaryRow label="Unit price" value={formatCurrency(unitPrice)} />
|
||||
<SummaryRow label="Quantity" value={safeQuantity.toString()} />
|
||||
<SummaryRow label="Subtotal" value={formatCurrency(subtotal)} />
|
||||
<SummaryRow label="Delivery fee" value={formatCurrency(Number(deliveryFee || 0))} />
|
||||
<SummaryRow label="Service fee" value={formatCurrency(Number(serviceFee || 0))} />
|
||||
<SummaryRow label="Total" value={formatCurrency(totalAmount)} strong />
|
||||
</div>
|
||||
</div>
|
||||
</CardBox>
|
||||
|
||||
<CardBox>
|
||||
<div className="space-y-4">
|
||||
<div className="flex items-center justify-between">
|
||||
<h3 className="text-lg font-semibold">Recent orders</h3>
|
||||
<BaseButton href="/orders/orders-list" label="View all" color="white" outline small />
|
||||
</div>
|
||||
{recentOrders.length === 0 && !loading && (
|
||||
<p className="text-sm text-gray-500">No orders yet. Create the first one.</p>
|
||||
)}
|
||||
<div className="space-y-3">
|
||||
{recentOrders.map((order) => (
|
||||
<div key={order.id} className="rounded-lg border border-gray-200 px-3 py-2">
|
||||
<p className="text-sm font-semibold">{order.order_number || 'Order'}</p>
|
||||
<p className="text-xs text-gray-500">
|
||||
{order.store?.store_name || 'Store'} · {formatCurrency(Number(order.total_amount || 0))}
|
||||
</p>
|
||||
<div className="mt-2">
|
||||
<BaseButton
|
||||
href={`/orders/orders-view?id=${order.id}`}
|
||||
label="View"
|
||||
color="info"
|
||||
small
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
</CardBox>
|
||||
</div>
|
||||
</div>
|
||||
</SectionMain>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
const SummaryRow = ({ label, value, strong = false }: { label: string; value: string; strong?: boolean }) => (
|
||||
<div className="flex items-center justify-between">
|
||||
<span className="text-gray-500">{label}</span>
|
||||
<span className={strong ? 'font-semibold text-gray-900' : 'text-gray-700'}>{value}</span>
|
||||
</div>
|
||||
);
|
||||
|
||||
const LinkToOrder = ({ orderId }: { orderId: string }) => (
|
||||
<div className="mt-2">
|
||||
<BaseButton href={`/orders/orders-view?id=${orderId}`} label="View order" color="success" small />
|
||||
</div>
|
||||
);
|
||||
|
||||
QuickOrder.getLayout = function getLayout(page: ReactElement) {
|
||||
return <LayoutAuthenticated>{page}</LayoutAuthenticated>;
|
||||
};
|
||||
|
||||
export default QuickOrder;
|
||||
@ -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';
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user