DTv-1
This commit is contained in:
parent
d7b7b23e2a
commit
e426a53f7e
2
assets/pasted-20260331-220901-00af1a98.jpg
Normal file
2
assets/pasted-20260331-220901-00af1a98.jpg
Normal file
File diff suppressed because one or more lines are too long
|
After Width: | Height: | Size: 499 KiB |
@ -20,6 +20,7 @@ const pexelsRoutes = require('./routes/pexels');
|
|||||||
const organizationForAuthRoutes = require('./routes/organizationLogin');
|
const organizationForAuthRoutes = require('./routes/organizationLogin');
|
||||||
|
|
||||||
const openaiRoutes = require('./routes/openai');
|
const openaiRoutes = require('./routes/openai');
|
||||||
|
const salesHubRoutes = require('./routes/sales_hub');
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
@ -154,6 +155,7 @@ app.use('/api/leads', passport.authenticate('jwt', {session: false}), leadsRoute
|
|||||||
app.use('/api/deals', passport.authenticate('jwt', {session: false}), dealsRoutes);
|
app.use('/api/deals', passport.authenticate('jwt', {session: false}), dealsRoutes);
|
||||||
|
|
||||||
app.use('/api/activities', passport.authenticate('jwt', {session: false}), activitiesRoutes);
|
app.use('/api/activities', passport.authenticate('jwt', {session: false}), activitiesRoutes);
|
||||||
|
app.use('/api/sales-hub', passport.authenticate('jwt', {session: false}), salesHubRoutes);
|
||||||
|
|
||||||
app.use('/api/materials', passport.authenticate('jwt', {session: false}), materialsRoutes);
|
app.use('/api/materials', passport.authenticate('jwt', {session: false}), materialsRoutes);
|
||||||
|
|
||||||
|
|||||||
305
backend/src/routes/sales_hub.js
Normal file
305
backend/src/routes/sales_hub.js
Normal file
@ -0,0 +1,305 @@
|
|||||||
|
const express = require('express');
|
||||||
|
|
||||||
|
const db = require('../db/models');
|
||||||
|
const wrapAsync = require('../helpers').wrapAsync;
|
||||||
|
const ActivitiesDBApi = require('../db/api/activities');
|
||||||
|
|
||||||
|
const router = express.Router();
|
||||||
|
const { QueryTypes } = db.Sequelize;
|
||||||
|
|
||||||
|
const RELATION_MODELS = {
|
||||||
|
lead: db.leads,
|
||||||
|
deal: db.deals,
|
||||||
|
contact: db.contacts,
|
||||||
|
};
|
||||||
|
|
||||||
|
const RELATION_LABELS = {
|
||||||
|
lead: 'Potansiyel müşteri',
|
||||||
|
deal: 'Fırsat',
|
||||||
|
contact: 'Kişi',
|
||||||
|
};
|
||||||
|
|
||||||
|
const ACTIVITY_TYPES = new Set([
|
||||||
|
'call',
|
||||||
|
'email',
|
||||||
|
'meeting',
|
||||||
|
'task',
|
||||||
|
'note',
|
||||||
|
'demo',
|
||||||
|
'follow_up',
|
||||||
|
]);
|
||||||
|
|
||||||
|
function getOrganizationId(currentUser) {
|
||||||
|
return (
|
||||||
|
currentUser?.organizations?.id ||
|
||||||
|
currentUser?.organization?.id ||
|
||||||
|
currentUser?.organizationsId ||
|
||||||
|
currentUser?.organizationId ||
|
||||||
|
null
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
function buildScopedWhere(alias, globalAccess, organizationId, extraClauses = []) {
|
||||||
|
const clauses = [`${alias}."deletedAt" IS NULL`];
|
||||||
|
|
||||||
|
if (!globalAccess && organizationId) {
|
||||||
|
clauses.push(`${alias}."organizationsId" = :organizationId`);
|
||||||
|
}
|
||||||
|
|
||||||
|
return [...clauses, ...extraClauses].join(' AND ');
|
||||||
|
}
|
||||||
|
|
||||||
|
async function findScopedEntity(model, id, organizationId, globalAccess) {
|
||||||
|
const where = { id };
|
||||||
|
|
||||||
|
if (!globalAccess && organizationId) {
|
||||||
|
where.organizationsId = organizationId;
|
||||||
|
}
|
||||||
|
|
||||||
|
return model.findOne({ where });
|
||||||
|
}
|
||||||
|
|
||||||
|
router.get(
|
||||||
|
'/',
|
||||||
|
wrapAsync(async (req, res) => {
|
||||||
|
const { currentUser } = req;
|
||||||
|
const globalAccess = Boolean(currentUser?.app_role?.globalAccess);
|
||||||
|
const organizationId = getOrganizationId(currentUser);
|
||||||
|
const replacements = {
|
||||||
|
userId: currentUser.id,
|
||||||
|
};
|
||||||
|
|
||||||
|
if (!globalAccess && organizationId) {
|
||||||
|
replacements.organizationId = organizationId;
|
||||||
|
}
|
||||||
|
|
||||||
|
const leadScope = buildScopedWhere('l', globalAccess, organizationId, [
|
||||||
|
'l."ownerId" = :userId',
|
||||||
|
'COALESCE(l.status, \'new\') NOT IN (\'disqualified\', \'converted\')',
|
||||||
|
]);
|
||||||
|
const dealScope = buildScopedWhere('d', globalAccess, organizationId, [
|
||||||
|
'd."ownerId" = :userId',
|
||||||
|
'COALESCE(d.status, \'open\') = \'open\'',
|
||||||
|
]);
|
||||||
|
const activityOpenScope = buildScopedWhere('a', globalAccess, organizationId, [
|
||||||
|
'a."assigned_toId" = :userId',
|
||||||
|
'COALESCE(a.status, \'planned\') NOT IN (\'done\', \'canceled\')',
|
||||||
|
]);
|
||||||
|
const activityDoneScope = buildScopedWhere('a', globalAccess, organizationId, [
|
||||||
|
'a."assigned_toId" = :userId',
|
||||||
|
'COALESCE(a.status, \'planned\') = \'done\'',
|
||||||
|
]);
|
||||||
|
|
||||||
|
const [statsRows, followUps, recentLeads, stageSummary, quickOptions] =
|
||||||
|
await Promise.all([
|
||||||
|
db.sequelize.query(
|
||||||
|
`SELECT
|
||||||
|
(SELECT COUNT(*)::int FROM "leads" l WHERE ${leadScope}) AS "myOpenLeads",
|
||||||
|
(SELECT COUNT(*)::int FROM "deals" d WHERE ${dealScope}) AS "myActiveDeals",
|
||||||
|
(SELECT COUNT(*)::int FROM "activities" a WHERE ${activityOpenScope} AND a."due_at" IS NOT NULL AND DATE(a."due_at") = CURRENT_DATE) AS "dueToday",
|
||||||
|
(SELECT COUNT(*)::int FROM "activities" a WHERE ${activityOpenScope} AND a."due_at" IS NOT NULL AND a."due_at" < NOW()) AS "overdue",
|
||||||
|
(SELECT COUNT(*)::int FROM "activities" a WHERE ${activityDoneScope} AND a."completed_at" >= NOW() - INTERVAL '7 days') AS "completedThisWeek"`,
|
||||||
|
{
|
||||||
|
replacements,
|
||||||
|
type: QueryTypes.SELECT,
|
||||||
|
},
|
||||||
|
),
|
||||||
|
db.sequelize.query(
|
||||||
|
`SELECT
|
||||||
|
a.id,
|
||||||
|
a.subject,
|
||||||
|
a.status,
|
||||||
|
a."activity_type" AS "activityType",
|
||||||
|
a."due_at" AS "dueAt",
|
||||||
|
COALESCE(d.name, l.title, NULLIF(TRIM(CONCAT(c.first_name, ' ', c.last_name)), ''), c.email, 'Unlinked activity') AS "relatedName",
|
||||||
|
CASE
|
||||||
|
WHEN a."dealId" IS NOT NULL THEN 'deal'
|
||||||
|
WHEN a."leadId" IS NOT NULL THEN 'lead'
|
||||||
|
WHEN a."contactId" IS NOT NULL THEN 'contact'
|
||||||
|
ELSE 'activity'
|
||||||
|
END AS "relatedType",
|
||||||
|
COALESCE(a."dealId", a."leadId", a."contactId") AS "relatedId"
|
||||||
|
FROM "activities" a
|
||||||
|
LEFT JOIN "deals" d ON d.id = a."dealId" AND d."deletedAt" IS NULL
|
||||||
|
LEFT JOIN "leads" l ON l.id = a."leadId" AND l."deletedAt" IS NULL
|
||||||
|
LEFT JOIN "contacts" c ON c.id = a."contactId" AND c."deletedAt" IS NULL
|
||||||
|
WHERE ${activityOpenScope}
|
||||||
|
ORDER BY
|
||||||
|
CASE WHEN a."due_at" IS NULL THEN 1 ELSE 0 END,
|
||||||
|
a."due_at" ASC,
|
||||||
|
a."createdAt" DESC
|
||||||
|
LIMIT 8`,
|
||||||
|
{
|
||||||
|
replacements,
|
||||||
|
type: QueryTypes.SELECT,
|
||||||
|
},
|
||||||
|
),
|
||||||
|
db.sequelize.query(
|
||||||
|
`SELECT
|
||||||
|
l.id,
|
||||||
|
l.title,
|
||||||
|
l.status,
|
||||||
|
l."estimated_value" AS "estimatedValue",
|
||||||
|
l."next_follow_up_at" AS "nextFollowUpAt",
|
||||||
|
c.name AS "companyName",
|
||||||
|
ps.name AS "stageName"
|
||||||
|
FROM "leads" l
|
||||||
|
LEFT JOIN "companies" c ON c.id = l."companyId" AND c."deletedAt" IS NULL
|
||||||
|
LEFT JOIN "pipeline_stages" ps ON ps.id = l."stageId" AND ps."deletedAt" IS NULL
|
||||||
|
WHERE ${leadScope}
|
||||||
|
ORDER BY l."createdAt" DESC
|
||||||
|
LIMIT 6`,
|
||||||
|
{
|
||||||
|
replacements,
|
||||||
|
type: QueryTypes.SELECT,
|
||||||
|
},
|
||||||
|
),
|
||||||
|
db.sequelize.query(
|
||||||
|
`SELECT
|
||||||
|
COALESCE(ps.id::text, 'unassigned') AS id,
|
||||||
|
COALESCE(ps.name, 'Unassigned') AS name,
|
||||||
|
COUNT(d.id)::int AS "dealCount",
|
||||||
|
COALESCE(SUM(d.amount), 0)::float AS amount
|
||||||
|
FROM "deals" d
|
||||||
|
LEFT JOIN "pipeline_stages" ps ON ps.id = d."stageId" AND ps."deletedAt" IS NULL
|
||||||
|
WHERE ${dealScope}
|
||||||
|
GROUP BY COALESCE(ps.id::text, 'unassigned'), COALESCE(ps.name, 'Unassigned'), COALESCE(ps."sort_order", 999)
|
||||||
|
ORDER BY COALESCE(ps."sort_order", 999) ASC, COALESCE(ps.name, 'Unassigned') ASC`,
|
||||||
|
{
|
||||||
|
replacements,
|
||||||
|
type: QueryTypes.SELECT,
|
||||||
|
},
|
||||||
|
),
|
||||||
|
Promise.all([
|
||||||
|
db.sequelize.query(
|
||||||
|
`SELECT l.id, l.title AS label
|
||||||
|
FROM "leads" l
|
||||||
|
WHERE ${leadScope}
|
||||||
|
ORDER BY l."updatedAt" DESC
|
||||||
|
LIMIT 8`,
|
||||||
|
{
|
||||||
|
replacements,
|
||||||
|
type: QueryTypes.SELECT,
|
||||||
|
},
|
||||||
|
),
|
||||||
|
db.sequelize.query(
|
||||||
|
`SELECT d.id, d.name AS label
|
||||||
|
FROM "deals" d
|
||||||
|
WHERE ${dealScope}
|
||||||
|
ORDER BY d."updatedAt" DESC
|
||||||
|
LIMIT 8`,
|
||||||
|
{
|
||||||
|
replacements,
|
||||||
|
type: QueryTypes.SELECT,
|
||||||
|
},
|
||||||
|
),
|
||||||
|
db.sequelize.query(
|
||||||
|
`SELECT c.id, COALESCE(NULLIF(TRIM(CONCAT(c.first_name, ' ', c.last_name)), ''), c.email, 'Untitled contact') AS label
|
||||||
|
FROM "contacts" c
|
||||||
|
WHERE ${buildScopedWhere('c', globalAccess, organizationId, ['c."ownerId" = :userId'])}
|
||||||
|
ORDER BY c."updatedAt" DESC
|
||||||
|
LIMIT 8`,
|
||||||
|
{
|
||||||
|
replacements,
|
||||||
|
type: QueryTypes.SELECT,
|
||||||
|
},
|
||||||
|
),
|
||||||
|
]),
|
||||||
|
]);
|
||||||
|
|
||||||
|
return res.status(200).json({
|
||||||
|
stats: statsRows[0] || {
|
||||||
|
myOpenLeads: 0,
|
||||||
|
myActiveDeals: 0,
|
||||||
|
dueToday: 0,
|
||||||
|
overdue: 0,
|
||||||
|
completedThisWeek: 0,
|
||||||
|
},
|
||||||
|
followUps,
|
||||||
|
recentLeads,
|
||||||
|
stageSummary,
|
||||||
|
quickOptions: {
|
||||||
|
leads: quickOptions[0],
|
||||||
|
deals: quickOptions[1],
|
||||||
|
contacts: quickOptions[2],
|
||||||
|
},
|
||||||
|
});
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
|
||||||
|
router.post(
|
||||||
|
'/follow-ups',
|
||||||
|
wrapAsync(async (req, res) => {
|
||||||
|
const { currentUser } = req;
|
||||||
|
const globalAccess = Boolean(currentUser?.app_role?.globalAccess);
|
||||||
|
const organizationId = getOrganizationId(currentUser);
|
||||||
|
const data = req.body?.data || {};
|
||||||
|
const subject = typeof data.subject === 'string' ? data.subject.trim() : '';
|
||||||
|
const details = typeof data.details === 'string' ? data.details.trim() : '';
|
||||||
|
const relationType = data.relationType;
|
||||||
|
const relationId = data.relationId;
|
||||||
|
const activityType = ACTIVITY_TYPES.has(data.activity_type)
|
||||||
|
? data.activity_type
|
||||||
|
: 'follow_up';
|
||||||
|
|
||||||
|
if (!subject) {
|
||||||
|
return res.status(400).json({ message: 'Konu alanı zorunludur.' });
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!data.due_at) {
|
||||||
|
return res.status(400).json({ message: 'Termin tarihi zorunludur.' });
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!RELATION_MODELS[relationType] || !relationId) {
|
||||||
|
return res.status(400).json({
|
||||||
|
message: 'Takibin bağlanacağı bir potansiyel müşteri, fırsat veya kişi seçin.',
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
const relationEntity = await findScopedEntity(
|
||||||
|
RELATION_MODELS[relationType],
|
||||||
|
relationId,
|
||||||
|
organizationId,
|
||||||
|
globalAccess,
|
||||||
|
);
|
||||||
|
|
||||||
|
if (!relationEntity) {
|
||||||
|
return res.status(400).json({
|
||||||
|
message: `${RELATION_LABELS[relationType]} bulunamadı veya erişilemez durumda.`,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
const transaction = await db.sequelize.transaction();
|
||||||
|
|
||||||
|
try {
|
||||||
|
const activity = await ActivitiesDBApi.create(
|
||||||
|
{
|
||||||
|
activity_type: activityType,
|
||||||
|
status: 'planned',
|
||||||
|
subject,
|
||||||
|
details: details || null,
|
||||||
|
due_at: data.due_at,
|
||||||
|
assigned_to: currentUser.id,
|
||||||
|
organizations: organizationId,
|
||||||
|
[relationType]: relationId,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
currentUser,
|
||||||
|
transaction,
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
|
await transaction.commit();
|
||||||
|
|
||||||
|
return res.status(200).json({
|
||||||
|
id: activity.id,
|
||||||
|
subject: activity.subject,
|
||||||
|
});
|
||||||
|
} catch (error) {
|
||||||
|
await transaction.rollback();
|
||||||
|
throw error;
|
||||||
|
}
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
|
||||||
|
module.exports = router;
|
||||||
2
frontend/public/brand/dakiktabela-logo.svg
Normal file
2
frontend/public/brand/dakiktabela-logo.svg
Normal file
File diff suppressed because one or more lines are too long
|
After Width: | Height: | Size: 499 KiB |
52
frontend/public/locales/tr/common.json
Normal file
52
frontend/public/locales/tr/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"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -29,16 +29,30 @@ const AsideMenuItem = ({ item, isDropdownList = false }: Props) => {
|
|||||||
const { asPath, isReady } = useRouter()
|
const { asPath, isReady } = useRouter()
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (item.href && isReady) {
|
if (!isReady) {
|
||||||
const linkPathName = new URL(item.href, location.href).pathname + '/';
|
return
|
||||||
const activePathname = new URL(asPath, location.href).pathname
|
|
||||||
|
|
||||||
const activeView = activePathname.split('/')[1];
|
|
||||||
const linkPathNameView = linkPathName.split('/')[1];
|
|
||||||
|
|
||||||
setIsLinkActive(linkPathNameView === activeView);
|
|
||||||
}
|
}
|
||||||
}, [item.href, isReady, asPath])
|
|
||||||
|
const activePathname = new URL(asPath, location.href).pathname
|
||||||
|
|
||||||
|
if (item.href) {
|
||||||
|
const linkPathName = new URL(item.href, location.href).pathname + '/'
|
||||||
|
const activeView = activePathname.split('/')[1]
|
||||||
|
const linkPathNameView = linkPathName.split('/')[1]
|
||||||
|
setIsLinkActive(linkPathNameView === activeView)
|
||||||
|
}
|
||||||
|
|
||||||
|
if (item.menu?.length) {
|
||||||
|
const hasActiveChild = item.menu.some((child) => {
|
||||||
|
if (!child.href) return false
|
||||||
|
const childPath = new URL(child.href, location.href).pathname
|
||||||
|
return childPath.split('/')[1] === activePathname.split('/')[1]
|
||||||
|
})
|
||||||
|
|
||||||
|
setIsDropdownActive(hasActiveChild)
|
||||||
|
setIsLinkActive(hasActiveChild)
|
||||||
|
}
|
||||||
|
}, [item.href, item.menu, isReady, asPath])
|
||||||
|
|
||||||
const asideMenuItemInnerContents = (
|
const asideMenuItemInnerContents = (
|
||||||
<>
|
<>
|
||||||
|
|||||||
@ -1,15 +1,12 @@
|
|||||||
import React from 'react'
|
import React from 'react'
|
||||||
import { mdiLogout, mdiClose } from '@mdi/js'
|
import { mdiClose } from '@mdi/js'
|
||||||
import BaseIcon from './BaseIcon'
|
import BaseIcon from './BaseIcon'
|
||||||
import AsideMenuList from './AsideMenuList'
|
import AsideMenuList from './AsideMenuList'
|
||||||
import { MenuAsideItem } from '../interfaces'
|
import { MenuAsideItem } from '../interfaces'
|
||||||
import { useAppSelector } from '../stores/hooks'
|
import { useAppDispatch, useAppSelector } from '../stores/hooks'
|
||||||
import Link from 'next/link';
|
import { createAsyncThunk } from '@reduxjs/toolkit'
|
||||||
|
import axios from 'axios'
|
||||||
import { useAppDispatch } from '../stores/hooks';
|
import Logo from './Logo'
|
||||||
import { createAsyncThunk } from '@reduxjs/toolkit';
|
|
||||||
import axios from 'axios';
|
|
||||||
|
|
||||||
|
|
||||||
type Props = {
|
type Props = {
|
||||||
menu: MenuAsideItem[]
|
menu: MenuAsideItem[]
|
||||||
@ -18,7 +15,7 @@ type Props = {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export default function AsideMenuLayer({ menu, className = '', ...props }: Props) {
|
export default function AsideMenuLayer({ menu, className = '', ...props }: Props) {
|
||||||
const corners = useAppSelector((state) => state.style.corners);
|
const corners = useAppSelector((state) => state.style.corners)
|
||||||
const asideStyle = useAppSelector((state) => state.style.asideStyle)
|
const asideStyle = useAppSelector((state) => state.style.asideStyle)
|
||||||
const asideBrandStyle = useAppSelector((state) => state.style.asideBrandStyle)
|
const asideBrandStyle = useAppSelector((state) => state.style.asideBrandStyle)
|
||||||
const asideScrollbarsStyle = useAppSelector((state) => state.style.asideScrollbarsStyle)
|
const asideScrollbarsStyle = useAppSelector((state) => state.style.asideScrollbarsStyle)
|
||||||
@ -29,55 +26,49 @@ export default function AsideMenuLayer({ menu, className = '', ...props }: Props
|
|||||||
props.onAsideLgCloseClick()
|
props.onAsideLgCloseClick()
|
||||||
}
|
}
|
||||||
|
|
||||||
const dispatch = useAppDispatch();
|
const dispatch = useAppDispatch()
|
||||||
const { currentUser } = useAppSelector((state) => state.auth);
|
const { currentUser } = useAppSelector((state) => state.auth)
|
||||||
const organizationsId = currentUser?.organizations?.id;
|
const organizationsId = currentUser?.organizations?.id
|
||||||
const [organizations, setOrganizations] = React.useState(null);
|
const [organizations, setOrganizations] = React.useState<any[]>([])
|
||||||
|
|
||||||
const fetchOrganizations = createAsyncThunk('/org-for-auth', async () => {
|
const fetchOrganizations = createAsyncThunk('/org-for-auth', async () => {
|
||||||
try {
|
try {
|
||||||
const response = await axios.get('/org-for-auth');
|
const response = await axios.get('/org-for-auth')
|
||||||
setOrganizations(response.data);
|
setOrganizations(response.data)
|
||||||
return response.data;
|
return response.data
|
||||||
} catch (error) {
|
} catch (error: any) {
|
||||||
console.error(error.response);
|
console.error(error?.response || error)
|
||||||
throw error;
|
throw error
|
||||||
}
|
}
|
||||||
});
|
})
|
||||||
|
|
||||||
React.useEffect(() => {
|
React.useEffect(() => {
|
||||||
dispatch(fetchOrganizations());
|
dispatch(fetchOrganizations())
|
||||||
}, [dispatch]);
|
}, [dispatch])
|
||||||
|
|
||||||
let organizationName = organizations?.find(item => item.id === organizationsId)?.name;
|
let organizationName = organizations?.find((item) => item.id === organizationsId)?.name
|
||||||
if(organizationName?.length > 25){
|
if (organizationName?.length > 28) {
|
||||||
organizationName = organizationName?.substring(0, 25) + '...';
|
organizationName = organizationName.substring(0, 28) + '...'
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<aside
|
<aside
|
||||||
id='asideMenu'
|
id='asideMenu'
|
||||||
className={`${className} zzz lg:py-2 lg:pl-2 w-60 fixed flex z-40 top-0 h-screen transition-position overflow-hidden`}
|
className={`${className} zzz lg:py-2 lg:pl-2 w-60 fixed flex z-40 top-0 h-screen transition-position overflow-hidden`}
|
||||||
>
|
>
|
||||||
<div
|
<div className={`flex-1 flex flex-col overflow-hidden dark:bg-dark-900 ${asideStyle} ${corners}`}>
|
||||||
className={`flex-1 flex flex-col overflow-hidden dark:bg-dark-900 ${asideStyle} ${corners}`}
|
<div className={`flex min-h-14 items-center justify-between gap-3 px-4 py-3 ${asideBrandStyle}`}>
|
||||||
>
|
<div className='flex min-w-0 items-center gap-3'>
|
||||||
<div
|
<Logo className='h-10 w-auto shrink-0 rounded-xl bg-white p-1.5 shadow-sm' />
|
||||||
className={`flex flex-row h-14 items-center justify-between ${asideBrandStyle}`}
|
<div className='min-w-0'>
|
||||||
>
|
<p className='truncate text-sm font-semibold text-slate-900 dark:text-white'>DakikTabela</p>
|
||||||
<div className="text-center flex-1 lg:text-left lg:pl-6 xl:text-center xl:pl-0">
|
<p className='truncate text-xs text-slate-500 dark:text-slate-300'>Operasyon Platformu</p>
|
||||||
|
{organizationName && (
|
||||||
<b className="font-black">DakikTabela Ops Suite</b>
|
<p className='truncate text-[11px] text-slate-400 dark:text-slate-400'>{organizationName}</p>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
{organizationName && <p>{organizationName}</p>}
|
|
||||||
|
|
||||||
</div>
|
</div>
|
||||||
<button
|
<button className='hidden lg:inline-block xl:hidden p-3' onClick={handleAsideLgCloseClick}>
|
||||||
className="hidden lg:inline-block xl:hidden p-3"
|
|
||||||
onClick={handleAsideLgCloseClick}
|
|
||||||
>
|
|
||||||
<BaseIcon path={mdiClose} />
|
<BaseIcon path={mdiClose} />
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@ -10,25 +10,16 @@ export default function FooterBar({ children }: Props) {
|
|||||||
const year = new Date().getFullYear()
|
const year = new Date().getFullYear()
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<footer className={`py-2 px-6 ${containerMaxW}`}>
|
<footer className={`py-3 px-6 ${containerMaxW}`}>
|
||||||
<div className="block md:flex items-center justify-between">
|
<div className="block md:flex items-center justify-between gap-4">
|
||||||
<div className="text-center md:text-left mb-6 md:mb-0">
|
<div className="text-center md:text-left mb-4 md:mb-0 text-sm text-slate-600 dark:text-slate-300">
|
||||||
<b>
|
<b className='text-slate-900 dark:text-white'>© {year} DakikTabela.</b>{' '}
|
||||||
©{year},{` `}
|
{children || 'Satış, üretim ve etkinlik operasyonları tek platformda.'}
|
||||||
<a href="https://flatlogic.com/" rel="noreferrer" target="_blank">
|
|
||||||
Flatlogic
|
|
||||||
</a>
|
|
||||||
.
|
|
||||||
</b>
|
|
||||||
{` `}
|
|
||||||
{children}
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="flex item-center md:py-2 gap-4">
|
<div className="flex items-center justify-center md:justify-end gap-3">
|
||||||
<a href="https://flatlogic.com/" rel="noreferrer" target="_blank">
|
<Logo className="w-auto h-10 md:h-8" />
|
||||||
<Logo className="w-auto h-8 md:h-6 mx-auto" />
|
</div>
|
||||||
</a>
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
</footer>
|
</footer>
|
||||||
)
|
)
|
||||||
|
|||||||
@ -1,96 +1,102 @@
|
|||||||
import React, { useEffect, useState } from 'react';
|
import React, { useEffect, useState } from 'react'
|
||||||
import Select, { components, SingleValueProps, OptionProps } from 'react-select';
|
import Select, { components, SingleValueProps, OptionProps } from 'react-select'
|
||||||
|
import { useTranslation } from 'react-i18next'
|
||||||
|
|
||||||
type LanguageOption = { label: string; value: string };
|
type LanguageOption = { label: string; value: string }
|
||||||
|
|
||||||
const LANGS: LanguageOption[] = [
|
const LANGS: LanguageOption[] = [
|
||||||
{ value: 'en', label: '🇬🇧 EN' },
|
{ value: 'tr', label: '🇹🇷 TR' },
|
||||||
{ value: 'fr', label: '🇫🇷 FR' },
|
{ value: 'en', label: '🇬🇧 EN' },
|
||||||
{ value: 'es', label: '🇪🇸 ES' },
|
{ value: 'fr', label: '🇫🇷 FR' },
|
||||||
{ value: 'de', label: '🇩🇪 DE' },
|
{ value: 'es', label: '🇪🇸 ES' },
|
||||||
];
|
{ value: 'de', label: '🇩🇪 DE' },
|
||||||
|
]
|
||||||
|
|
||||||
const Option = (props: OptionProps<LanguageOption, false>) => (
|
const Option = (props: OptionProps<LanguageOption, false>) => (
|
||||||
<components.Option {...props}>
|
<components.Option {...props}>
|
||||||
<span className='flex items-center gap-1'>{props.data.label}</span>
|
<span className='flex items-center gap-1'>{props.data.label}</span>
|
||||||
</components.Option>
|
</components.Option>
|
||||||
);
|
)
|
||||||
|
|
||||||
const SingleVal = (props: SingleValueProps<LanguageOption, false>) => (
|
const SingleVal = (props: SingleValueProps<LanguageOption, false>) => (
|
||||||
<components.SingleValue {...props}>
|
<components.SingleValue {...props}>
|
||||||
<span className='flex items-center gap-1'>{props.data.label}</span>
|
<span className='flex items-center gap-1'>{props.data.label}</span>
|
||||||
</components.SingleValue>
|
</components.SingleValue>
|
||||||
);
|
)
|
||||||
|
|
||||||
const LanguageSwitcher: React.FC = () => {
|
const LanguageSwitcher: React.FC = () => {
|
||||||
const [mounted, setMounted] = useState(false);
|
const { i18n } = useTranslation()
|
||||||
const [selected, setSelected] = useState<LanguageOption>(LANGS[0]);
|
const [mounted, setMounted] = useState(false)
|
||||||
|
const [selected, setSelected] = useState<LanguageOption>(LANGS[0])
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
setMounted(true);
|
const active = LANGS.find((item) => item.value === i18n.language) || LANGS[0]
|
||||||
}, []);
|
setSelected(active)
|
||||||
|
setMounted(true)
|
||||||
|
}, [i18n.language])
|
||||||
|
|
||||||
const handleChange = (opt: LanguageOption | null) => {
|
const handleChange = (opt: LanguageOption | null) => {
|
||||||
if (!opt) return;
|
if (!opt) return
|
||||||
setSelected(opt);
|
setSelected(opt)
|
||||||
};
|
i18n.changeLanguage(opt.value)
|
||||||
|
}
|
||||||
|
|
||||||
if (!mounted) return null;
|
if (!mounted) return null
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div style={{ width: 88 }}>
|
<div style={{ width: 88 }}>
|
||||||
<Select
|
<Select
|
||||||
value={selected}
|
value={selected}
|
||||||
options={LANGS}
|
options={LANGS}
|
||||||
onChange={handleChange}
|
onChange={handleChange}
|
||||||
isSearchable={false}
|
isSearchable={false}
|
||||||
menuPlacement='top'
|
menuPlacement='top'
|
||||||
components={{
|
components={{
|
||||||
Option,
|
Option,
|
||||||
SingleValue: SingleVal,
|
SingleValue: SingleVal,
|
||||||
IndicatorSeparator: () => null,
|
IndicatorSeparator: () => null,
|
||||||
}}
|
}}
|
||||||
styles={{
|
styles={{
|
||||||
control: (base) => ({
|
control: (base) => ({
|
||||||
...base,
|
...base,
|
||||||
minHeight: 28,
|
minHeight: 28,
|
||||||
height: 28,
|
height: 28,
|
||||||
paddingTop: 0,
|
paddingTop: 0,
|
||||||
paddingBottom: 0,
|
paddingBottom: 0,
|
||||||
borderColor: '#d1d5db',
|
borderColor: '#d1d5db',
|
||||||
cursor: 'pointer',
|
cursor: 'pointer',
|
||||||
}),
|
}),
|
||||||
valueContainer: (base) => ({
|
valueContainer: (base) => ({
|
||||||
...base,
|
...base,
|
||||||
paddingTop: 0,
|
paddingTop: 0,
|
||||||
paddingBottom: 0,
|
paddingBottom: 0,
|
||||||
paddingLeft: 6,
|
paddingLeft: 6,
|
||||||
}),
|
}),
|
||||||
indicatorsContainer: (base) => ({
|
indicatorsContainer: (base) => ({
|
||||||
...base,
|
...base,
|
||||||
height: 24,
|
height: 24,
|
||||||
}),
|
}),
|
||||||
dropdownIndicator: (base) => ({
|
dropdownIndicator: (base) => ({
|
||||||
...base,
|
...base,
|
||||||
padding: 2,
|
padding: 2,
|
||||||
}),
|
}),
|
||||||
option: (base, state) => ({
|
option: (base, state) => ({
|
||||||
...base,
|
...base,
|
||||||
paddingTop: 4,
|
paddingTop: 4,
|
||||||
paddingBottom: 4,
|
paddingBottom: 4,
|
||||||
height: 26,
|
height: 26,
|
||||||
fontSize: '0.875rem',
|
fontSize: '0.875rem',
|
||||||
backgroundColor: state.isFocused ? '#f3f4f6' : 'white',
|
backgroundColor: state.isFocused ? '#f3f4f6' : 'white',
|
||||||
color: '#111827',
|
color: '#111827',
|
||||||
}),
|
}),
|
||||||
menu: (base) => ({
|
menu: (base) => ({
|
||||||
...base,
|
...base,
|
||||||
zIndex: 9999,
|
zIndex: 9999,
|
||||||
}),
|
}),
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
);
|
)
|
||||||
};
|
}
|
||||||
|
|
||||||
export default LanguageSwitcher;
|
export default LanguageSwitcher
|
||||||
|
|||||||
@ -7,9 +7,9 @@ type Props = {
|
|||||||
export default function Logo({ className = '' }: Props) {
|
export default function Logo({ className = '' }: Props) {
|
||||||
return (
|
return (
|
||||||
<img
|
<img
|
||||||
src={"https://flatlogic.com/logo.svg"}
|
src='/brand/dakiktabela-logo.svg'
|
||||||
className={className}
|
className={className}
|
||||||
alt={'Flatlogic logo'}>
|
alt='DakikTabela logosu'
|
||||||
</img>
|
/>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|||||||
@ -1,6 +1,5 @@
|
|||||||
import React, {useEffect, useRef} from 'react'
|
import React, { useEffect, useRef, useState } from 'react'
|
||||||
import Link from 'next/link'
|
import Link from 'next/link'
|
||||||
import { useState } from 'react'
|
|
||||||
import { mdiChevronUp, mdiChevronDown } from '@mdi/js'
|
import { mdiChevronUp, mdiChevronDown } from '@mdi/js'
|
||||||
import BaseDivider from './BaseDivider'
|
import BaseDivider from './BaseDivider'
|
||||||
import BaseIcon from './BaseIcon'
|
import BaseIcon from './BaseIcon'
|
||||||
@ -67,8 +66,10 @@ export default function NavBarItem({ item }: Props) {
|
|||||||
const getItemId = (label) => {
|
const getItemId = (label) => {
|
||||||
switch (label) {
|
switch (label) {
|
||||||
case 'Light/Dark':
|
case 'Light/Dark':
|
||||||
|
case 'Açık/Koyu Tema':
|
||||||
return 'themeToggle';
|
return 'themeToggle';
|
||||||
case 'Log out':
|
case 'Log out':
|
||||||
|
case 'Çıkış yap':
|
||||||
return 'logout';
|
return 'logout';
|
||||||
default:
|
default:
|
||||||
return undefined;
|
return undefined;
|
||||||
|
|||||||
@ -49,16 +49,16 @@ export default function PasswordSetOrReset() {
|
|||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<Head>
|
<Head>
|
||||||
{isInvitation && <title>{getPageTitle('Set Password')}</title>}
|
{isInvitation && <title>{getPageTitle('Şifre Belirle')}</title>}
|
||||||
{!isInvitation && <title>{getPageTitle('Reset Password')}</title>}
|
{!isInvitation && <title>{getPageTitle('Şifreyi Sıfırla')}</title>}
|
||||||
</Head>
|
</Head>
|
||||||
|
|
||||||
<SectionFullScreen bg='violet'>
|
<SectionFullScreen bg='violet'>
|
||||||
<div className='w-full flex flex-col items-center justify-center'>
|
<div className='w-full flex flex-col items-center justify-center'>
|
||||||
<CardBox className='w-11/12 md:w-7/12 lg:w-6/12 xl:w-4/12'>
|
<CardBox className='w-11/12 md:w-7/12 lg:w-6/12 xl:w-4/12'>
|
||||||
{isInvitation && <p className='text-xl mb-2'>Set Password</p>}
|
{isInvitation && <p className='text-xl mb-2'>Şifre Belirle</p>}
|
||||||
{!isInvitation && <p className='text-xl mb-2'>Reset Password</p>}
|
{!isInvitation && <p className='text-xl mb-2'>Şifreyi Sıfırla</p>}
|
||||||
<p className='text-base mb-4'>Enter your new password</p>
|
<p className='text-base mb-4'>Yeni şifrenizi girin</p>
|
||||||
|
|
||||||
<Formik
|
<Formik
|
||||||
initialValues={{
|
initialValues={{
|
||||||
@ -74,7 +74,7 @@ export default function PasswordSetOrReset() {
|
|||||||
<Field
|
<Field
|
||||||
type='password'
|
type='password'
|
||||||
name='password'
|
name='password'
|
||||||
placeholder='Password'
|
placeholder='Şifre'
|
||||||
/>
|
/>
|
||||||
</FormField>
|
</FormField>
|
||||||
<FormField
|
<FormField
|
||||||
@ -82,7 +82,7 @@ export default function PasswordSetOrReset() {
|
|||||||
<Field
|
<Field
|
||||||
type='password'
|
type='password'
|
||||||
name='confirm'
|
name='confirm'
|
||||||
placeholder='Confirm Password'
|
placeholder='Şifre Tekrarı'
|
||||||
/>
|
/>
|
||||||
</FormField>
|
</FormField>
|
||||||
|
|
||||||
@ -93,10 +93,10 @@ export default function PasswordSetOrReset() {
|
|||||||
disabled={loading}
|
disabled={loading}
|
||||||
label={
|
label={
|
||||||
loading
|
loading
|
||||||
? 'Loading...'
|
? 'Yükleniyor...'
|
||||||
: isInvitation
|
: isInvitation
|
||||||
? 'Set Password'
|
? 'Şifre Belirle'
|
||||||
: 'Reset Password'
|
: 'Şifreyi Sıfırla'
|
||||||
}
|
}
|
||||||
color='info'
|
color='info'
|
||||||
/>
|
/>
|
||||||
|
|||||||
@ -8,7 +8,7 @@ const SearchResults = ({ searchResults, searchQuery }) => {
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<p className={'block font-bold mb-2'}>Matches with: {searchQuery}</p>
|
<p className={'block font-bold mb-2'}>Aranan ifade: {searchQuery}</p>
|
||||||
{Object.keys(searchResults).map((tableName) => (
|
{Object.keys(searchResults).map((tableName) => (
|
||||||
<>
|
<>
|
||||||
<p className={'block font-bold mb-2'}>{humanize(tableName)}</p>
|
<p className={'block font-bold mb-2'}>{humanize(tableName)}</p>
|
||||||
@ -68,13 +68,13 @@ const SearchResults = ({ searchResults, searchQuery }) => {
|
|||||||
</table>
|
</table>
|
||||||
</div>
|
</div>
|
||||||
{!Object.keys(searchResults).length && (
|
{!Object.keys(searchResults).length && (
|
||||||
<div className={'text-center py-4'}>No data</div>
|
<div className={'text-center py-4'}>Veri bulunamadı</div>
|
||||||
)}
|
)}
|
||||||
</CardBox>
|
</CardBox>
|
||||||
</>
|
</>
|
||||||
))}
|
))}
|
||||||
{!Object.keys(searchResults).length && (
|
{!Object.keys(searchResults).length && (
|
||||||
<div className={'py-4'}>No matches</div>
|
<div className={'py-4'}>Eşleşme bulunamadı</div>
|
||||||
)}
|
)}
|
||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
|
|||||||
@ -75,7 +75,7 @@ export const SmartWidget = ({ widget, userId, admin, roleId }) => {
|
|||||||
)
|
)
|
||||||
) : (
|
) : (
|
||||||
<div className='text-center text-red-400'>
|
<div className='text-center text-red-400'>
|
||||||
Something went wrong, please try again or use a different query.
|
Bir sorun oluştu. Lütfen tekrar deneyin veya farklı bir sorgu kullanın.
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@ -67,7 +67,7 @@ export const WidgetCreator = ({
|
|||||||
const errorMessage =
|
const errorMessage =
|
||||||
responcePayload.data?.error?.message || error?.message;
|
responcePayload.data?.error?.message || error?.message;
|
||||||
await dispatch(
|
await dispatch(
|
||||||
setErrorNotification(errorMessage || 'Error with widget creation'),
|
setErrorNotification(errorMessage || 'Bileşen oluşturulurken bir hata oluştu'),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
@ -90,11 +90,11 @@ export const WidgetCreator = ({
|
|||||||
>
|
>
|
||||||
<Form>
|
<Form>
|
||||||
<FormField
|
<FormField
|
||||||
label='Create Chart or Widget'
|
label='Grafik veya bileşen oluştur'
|
||||||
help={
|
help={
|
||||||
isFetchingQuery ?
|
isFetchingQuery ?
|
||||||
'Loading...' :
|
'Yükleniyor...' :
|
||||||
'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"'
|
'Yeni bileşeni doğal dille tarif edin. Örnek: "Aylara göre kapanan fırsat sayısı" veya "Yönetici kullanıcı sayısı"'
|
||||||
}
|
}
|
||||||
>
|
>
|
||||||
<Field type='input' name='description' disabled={isFetchingQuery} />
|
<Field type='input' name='description' disabled={isFetchingQuery} />
|
||||||
@ -110,14 +110,14 @@ export const WidgetCreator = ({
|
|||||||
>
|
>
|
||||||
{({ submitForm }) => (
|
{({ submitForm }) => (
|
||||||
<CardBoxModal
|
<CardBoxModal
|
||||||
title='Widget Creator Settings'
|
title='Bileşen oluşturucu ayarları'
|
||||||
buttonColor='info'
|
buttonColor='info'
|
||||||
buttonLabel='Done'
|
buttonLabel='Tamam'
|
||||||
isActive={isModalOpen}
|
isActive={isModalOpen}
|
||||||
onConfirm={submitForm}
|
onConfirm={submitForm}
|
||||||
onCancel={() => setIsModalOpen(false)}
|
onCancel={() => setIsModalOpen(false)}
|
||||||
>
|
>
|
||||||
<p>What role are we showing and creating widgets for?</p>
|
<p>Bileşenleri hangi rol için göstereceğimizi ve oluşturacağımızı seçin.</p>
|
||||||
|
|
||||||
<Form>
|
<Form>
|
||||||
<FormField>
|
<FormField>
|
||||||
|
|||||||
@ -8,7 +8,7 @@ export const localStorageStyleKey = 'style'
|
|||||||
|
|
||||||
export const containerMaxW = 'xl:max-w-full xl:mx-auto 2xl:mx-20'
|
export const containerMaxW = 'xl:max-w-full xl:mx-auto 2xl:mx-20'
|
||||||
|
|
||||||
export const appTitle = 'created by Flatlogic generator!'
|
export const appTitle = 'DakikTabela Operasyon Platformu'
|
||||||
|
|
||||||
export const getPageTitle = (currentPageTitle: string) => `${currentPageTitle} — ${appTitle}`
|
export const getPageTitle = (currentPageTitle: string) => `${currentPageTitle} — ${appTitle}`
|
||||||
|
|
||||||
|
|||||||
@ -33,3 +33,8 @@
|
|||||||
.introjs-prevbutton{
|
.introjs-prevbutton{
|
||||||
@apply bg-transparent border border-blue-600 text-blue-600 !important;
|
@apply bg-transparent border border-blue-600 text-blue-600 !important;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
html, body {
|
||||||
|
font-family: 'Poppins', ui-sans-serif, system-ui, sans-serif;
|
||||||
|
}
|
||||||
|
|||||||
@ -8,7 +8,7 @@ i18n
|
|||||||
.use(LanguageDetector)
|
.use(LanguageDetector)
|
||||||
.use(initReactI18next)
|
.use(initReactI18next)
|
||||||
.init({
|
.init({
|
||||||
fallbackLng: 'en',
|
fallbackLng: 'tr',
|
||||||
detection: {
|
detection: {
|
||||||
order: ['localStorage', 'navigator'],
|
order: ['localStorage', 'navigator'],
|
||||||
lookupLocalStorage: 'app_lang_',
|
lookupLocalStorage: 'app_lang_',
|
||||||
|
|||||||
@ -1,5 +1,4 @@
|
|||||||
import React, { ReactNode, useEffect } from 'react'
|
import React, { ReactNode, useEffect, useState } from 'react'
|
||||||
import { useState } from 'react'
|
|
||||||
import jwt from 'jsonwebtoken';
|
import jwt from 'jsonwebtoken';
|
||||||
import { mdiForwardburger, mdiBackburger, mdiMenu } from '@mdi/js'
|
import { mdiForwardburger, mdiBackburger, mdiMenu } from '@mdi/js'
|
||||||
import menuAside from '../menuAside'
|
import menuAside from '../menuAside'
|
||||||
@ -122,7 +121,7 @@ export default function LayoutAuthenticated({
|
|||||||
onAsideLgClose={() => setIsAsideLgActive(false)}
|
onAsideLgClose={() => setIsAsideLgActive(false)}
|
||||||
/>
|
/>
|
||||||
{children}
|
{children}
|
||||||
<FooterBar>Hand-crafted & Made with ❤️</FooterBar>
|
<FooterBar>Satış, üretim ve etkinlik süreçleri tek ekranda yönetilir.</FooterBar>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
)
|
)
|
||||||
|
|||||||
@ -1,234 +1,212 @@
|
|||||||
import * as icon from '@mdi/js';
|
import * as icon from '@mdi/js'
|
||||||
import { MenuAsideItem } from './interfaces'
|
import { MenuAsideItem } from './interfaces'
|
||||||
|
|
||||||
|
const mdi = icon as Record<string, string>
|
||||||
|
const pickIcon = (name: string, fallback = icon.mdiTable) => mdi[name] || fallback
|
||||||
|
|
||||||
const menuAside: MenuAsideItem[] = [
|
const menuAside: MenuAsideItem[] = [
|
||||||
{
|
{
|
||||||
href: '/dashboard',
|
href: '/dashboard',
|
||||||
icon: icon.mdiViewDashboardOutline,
|
icon: icon.mdiViewDashboardOutline,
|
||||||
label: 'Dashboard',
|
label: 'Operasyon Özeti',
|
||||||
},
|
|
||||||
|
|
||||||
{
|
|
||||||
href: '/users/users-list',
|
|
||||||
label: 'Users',
|
|
||||||
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
|
|
||||||
// @ts-ignore
|
|
||||||
icon: icon.mdiAccountGroup ?? icon.mdiTable,
|
|
||||||
permissions: 'READ_USERS'
|
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
href: '/roles/roles-list',
|
href: '/sales-hub',
|
||||||
label: 'Roles',
|
icon: icon.mdiChartTimelineVariant,
|
||||||
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
|
label: 'Satış Merkezi',
|
||||||
// @ts-ignore
|
|
||||||
icon: icon.mdiShieldAccountVariantOutline ?? icon.mdiTable,
|
|
||||||
permissions: 'READ_ROLES'
|
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
href: '/permissions/permissions-list',
|
label: 'CRM ve Satış',
|
||||||
label: 'Permissions',
|
icon: pickIcon('mdiHandshake', icon.mdiTable),
|
||||||
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
|
withDevider: true,
|
||||||
// @ts-ignore
|
menu: [
|
||||||
icon: icon.mdiShieldAccountOutline ?? icon.mdiTable,
|
{
|
||||||
permissions: 'READ_PERMISSIONS'
|
href: '/companies/companies-list',
|
||||||
|
label: 'Şirketler',
|
||||||
|
icon: pickIcon('mdiDomain'),
|
||||||
|
permissions: 'READ_COMPANIES',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
href: '/contacts/contacts-list',
|
||||||
|
label: 'Kişiler',
|
||||||
|
icon: pickIcon('mdiCardAccountDetails'),
|
||||||
|
permissions: 'READ_CONTACTS',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
href: '/pipelines/pipelines-list',
|
||||||
|
label: 'Pipeline\'lar',
|
||||||
|
icon: pickIcon('mdiSourceBranch'),
|
||||||
|
permissions: 'READ_PIPELINES',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
href: '/pipeline_stages/pipeline_stages-list',
|
||||||
|
label: 'Aşamalar',
|
||||||
|
icon: pickIcon('mdiSwapHorizontal'),
|
||||||
|
permissions: 'READ_PIPELINE_STAGES',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
href: '/leads/leads-list',
|
||||||
|
label: 'Potansiyel Müşteriler',
|
||||||
|
icon: pickIcon('mdiAccountArrowRight'),
|
||||||
|
permissions: 'READ_LEADS',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
href: '/deals/deals-list',
|
||||||
|
label: 'Fırsatlar',
|
||||||
|
icon: pickIcon('mdiHandshake'),
|
||||||
|
permissions: 'READ_DEALS',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
href: '/activities/activities-list',
|
||||||
|
label: 'Aktiviteler',
|
||||||
|
icon: pickIcon('mdiCalendarCheck'),
|
||||||
|
permissions: 'READ_ACTIVITIES',
|
||||||
|
},
|
||||||
|
],
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
href: '/organizations/organizations-list',
|
label: 'Üretim ERP',
|
||||||
label: 'Organizations',
|
icon: pickIcon('mdiFactory', icon.mdiTable),
|
||||||
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
|
menu: [
|
||||||
// @ts-ignore
|
{
|
||||||
icon: icon.mdiTable ?? icon.mdiTable,
|
href: '/materials/materials-list',
|
||||||
permissions: 'READ_ORGANIZATIONS'
|
label: 'Malzemeler',
|
||||||
|
icon: pickIcon('mdiCubeOutline'),
|
||||||
|
permissions: 'READ_MATERIALS',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
href: '/warehouses/warehouses-list',
|
||||||
|
label: 'Depolar',
|
||||||
|
icon: pickIcon('mdiWarehouse'),
|
||||||
|
permissions: 'READ_WAREHOUSES',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
href: '/inventory_items/inventory_items-list',
|
||||||
|
label: 'Stok Kalemleri',
|
||||||
|
icon: pickIcon('mdiPackageVariantClosed'),
|
||||||
|
permissions: 'READ_INVENTORY_ITEMS',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
href: '/machines/machines-list',
|
||||||
|
label: 'Makineler',
|
||||||
|
icon: pickIcon('mdiRobotIndustrial'),
|
||||||
|
permissions: 'READ_MACHINES',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
href: '/production_orders/production_orders-list',
|
||||||
|
label: 'Üretim Emirleri',
|
||||||
|
icon: pickIcon('mdiFactory'),
|
||||||
|
permissions: 'READ_PRODUCTION_ORDERS',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
href: '/production_operations/production_operations-list',
|
||||||
|
label: 'Operasyonlar',
|
||||||
|
icon: pickIcon('mdiPlaylistPlay'),
|
||||||
|
permissions: 'READ_PRODUCTION_OPERATIONS',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
href: '/quality_inspections/quality_inspections-list',
|
||||||
|
label: 'Kalite Kontrolleri',
|
||||||
|
icon: pickIcon('mdiClipboardSearch'),
|
||||||
|
permissions: 'READ_QUALITY_INSPECTIONS',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
href: '/nonconformances/nonconformances-list',
|
||||||
|
label: 'Uygunsuzluklar',
|
||||||
|
icon: pickIcon('mdiAlertOctagonOutline'),
|
||||||
|
permissions: 'READ_NONCONFORMANCES',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
href: '/vendors/vendors-list',
|
||||||
|
label: 'Tedarikçiler',
|
||||||
|
icon: pickIcon('mdiTruckFast'),
|
||||||
|
permissions: 'READ_VENDORS',
|
||||||
|
},
|
||||||
|
],
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
href: '/companies/companies-list',
|
label: 'Etkinlik Yönetimi',
|
||||||
label: 'Companies',
|
icon: pickIcon('mdiCalendarStar', icon.mdiTable),
|
||||||
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
|
menu: [
|
||||||
// @ts-ignore
|
{
|
||||||
icon: 'mdiDomain' in icon ? icon['mdiDomain' as keyof typeof icon] : icon.mdiTable ?? icon.mdiTable,
|
href: '/venues/venues-list',
|
||||||
permissions: 'READ_COMPANIES'
|
label: 'Mekanlar',
|
||||||
|
icon: pickIcon('mdiMapMarkerRadius'),
|
||||||
|
permissions: 'READ_VENUES',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
href: '/events/events-list',
|
||||||
|
label: 'Etkinlikler',
|
||||||
|
icon: pickIcon('mdiCalendarStar'),
|
||||||
|
permissions: 'READ_EVENTS',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
href: '/event_tasks/event_tasks-list',
|
||||||
|
label: 'Etkinlik Görevleri',
|
||||||
|
icon: pickIcon('mdiChecklist'),
|
||||||
|
permissions: 'READ_EVENT_TASKS',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
href: '/event_guests/event_guests-list',
|
||||||
|
label: 'Davetliler',
|
||||||
|
icon: pickIcon('mdiAccountGroup'),
|
||||||
|
permissions: 'READ_EVENT_GUESTS',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
href: '/event_schedule_items/event_schedule_items-list',
|
||||||
|
label: 'Takvim Kalemleri',
|
||||||
|
icon: pickIcon('mdiTimelineText'),
|
||||||
|
permissions: 'READ_EVENT_SCHEDULE_ITEMS',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
href: '/budget_items/budget_items-list',
|
||||||
|
label: 'Bütçe Kalemleri',
|
||||||
|
icon: pickIcon('mdiCurrencyUsd'),
|
||||||
|
permissions: 'READ_BUDGET_ITEMS',
|
||||||
|
},
|
||||||
|
],
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
href: '/contacts/contacts-list',
|
label: 'Yönetim',
|
||||||
label: 'Contacts',
|
icon: icon.mdiShieldAccountVariantOutline,
|
||||||
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
|
menu: [
|
||||||
// @ts-ignore
|
{
|
||||||
icon: 'mdiCardAccountDetails' in icon ? icon['mdiCardAccountDetails' as keyof typeof icon] : icon.mdiTable ?? icon.mdiTable,
|
href: '/users/users-list',
|
||||||
permissions: 'READ_CONTACTS'
|
label: 'Kullanıcılar',
|
||||||
},
|
icon: icon.mdiAccountGroup,
|
||||||
{
|
permissions: 'READ_USERS',
|
||||||
href: '/pipelines/pipelines-list',
|
},
|
||||||
label: 'Pipelines',
|
{
|
||||||
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
|
href: '/roles/roles-list',
|
||||||
// @ts-ignore
|
label: 'Roller',
|
||||||
icon: 'mdiSourceBranch' in icon ? icon['mdiSourceBranch' as keyof typeof icon] : icon.mdiTable ?? icon.mdiTable,
|
icon: icon.mdiShieldAccountVariantOutline,
|
||||||
permissions: 'READ_PIPELINES'
|
permissions: 'READ_ROLES',
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
href: '/pipeline_stages/pipeline_stages-list',
|
href: '/permissions/permissions-list',
|
||||||
label: 'Pipeline stages',
|
label: 'Yetkiler',
|
||||||
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
|
icon: icon.mdiShieldAccountOutline,
|
||||||
// @ts-ignore
|
permissions: 'READ_PERMISSIONS',
|
||||||
icon: 'mdiSwapHorizontal' in icon ? icon['mdiSwapHorizontal' as keyof typeof icon] : icon.mdiTable ?? icon.mdiTable,
|
},
|
||||||
permissions: 'READ_PIPELINE_STAGES'
|
{
|
||||||
},
|
href: '/organizations/organizations-list',
|
||||||
{
|
label: 'Organizasyonlar',
|
||||||
href: '/leads/leads-list',
|
icon: icon.mdiTable,
|
||||||
label: 'Leads',
|
permissions: 'READ_ORGANIZATIONS',
|
||||||
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
|
},
|
||||||
// @ts-ignore
|
{
|
||||||
icon: 'mdiAccountArrowRight' in icon ? icon['mdiAccountArrowRight' as keyof typeof icon] : icon.mdiTable ?? icon.mdiTable,
|
href: '/profile',
|
||||||
permissions: 'READ_LEADS'
|
label: 'Profil',
|
||||||
},
|
icon: icon.mdiAccountCircle,
|
||||||
{
|
},
|
||||||
href: '/deals/deals-list',
|
{
|
||||||
label: 'Deals',
|
href: '/api-docs',
|
||||||
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
|
target: '_blank',
|
||||||
// @ts-ignore
|
label: 'Swagger API',
|
||||||
icon: 'mdiHandshake' in icon ? icon['mdiHandshake' as keyof typeof icon] : icon.mdiTable ?? icon.mdiTable,
|
icon: icon.mdiFileCode,
|
||||||
permissions: 'READ_DEALS'
|
permissions: 'READ_API_DOCS',
|
||||||
},
|
},
|
||||||
{
|
],
|
||||||
href: '/activities/activities-list',
|
|
||||||
label: 'Activities',
|
|
||||||
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
|
|
||||||
// @ts-ignore
|
|
||||||
icon: 'mdiCalendarCheck' in icon ? icon['mdiCalendarCheck' as keyof typeof icon] : icon.mdiTable ?? icon.mdiTable,
|
|
||||||
permissions: 'READ_ACTIVITIES'
|
|
||||||
},
|
|
||||||
{
|
|
||||||
href: '/materials/materials-list',
|
|
||||||
label: 'Materials',
|
|
||||||
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
|
|
||||||
// @ts-ignore
|
|
||||||
icon: 'mdiCubeOutline' in icon ? icon['mdiCubeOutline' as keyof typeof icon] : icon.mdiTable ?? icon.mdiTable,
|
|
||||||
permissions: 'READ_MATERIALS'
|
|
||||||
},
|
|
||||||
{
|
|
||||||
href: '/warehouses/warehouses-list',
|
|
||||||
label: 'Warehouses',
|
|
||||||
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
|
|
||||||
// @ts-ignore
|
|
||||||
icon: 'mdiWarehouse' in icon ? icon['mdiWarehouse' as keyof typeof icon] : icon.mdiTable ?? icon.mdiTable,
|
|
||||||
permissions: 'READ_WAREHOUSES'
|
|
||||||
},
|
|
||||||
{
|
|
||||||
href: '/inventory_items/inventory_items-list',
|
|
||||||
label: 'Inventory items',
|
|
||||||
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
|
|
||||||
// @ts-ignore
|
|
||||||
icon: 'mdiPackageVariantClosed' in icon ? icon['mdiPackageVariantClosed' as keyof typeof icon] : icon.mdiTable ?? icon.mdiTable,
|
|
||||||
permissions: 'READ_INVENTORY_ITEMS'
|
|
||||||
},
|
|
||||||
{
|
|
||||||
href: '/machines/machines-list',
|
|
||||||
label: 'Machines',
|
|
||||||
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
|
|
||||||
// @ts-ignore
|
|
||||||
icon: 'mdiRobotIndustrial' in icon ? icon['mdiRobotIndustrial' as keyof typeof icon] : icon.mdiTable ?? icon.mdiTable,
|
|
||||||
permissions: 'READ_MACHINES'
|
|
||||||
},
|
|
||||||
{
|
|
||||||
href: '/production_orders/production_orders-list',
|
|
||||||
label: 'Production orders',
|
|
||||||
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
|
|
||||||
// @ts-ignore
|
|
||||||
icon: 'mdiFactory' in icon ? icon['mdiFactory' as keyof typeof icon] : icon.mdiTable ?? icon.mdiTable,
|
|
||||||
permissions: 'READ_PRODUCTION_ORDERS'
|
|
||||||
},
|
|
||||||
{
|
|
||||||
href: '/production_operations/production_operations-list',
|
|
||||||
label: 'Production operations',
|
|
||||||
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
|
|
||||||
// @ts-ignore
|
|
||||||
icon: 'mdiPlaylistPlay' in icon ? icon['mdiPlaylistPlay' as keyof typeof icon] : icon.mdiTable ?? icon.mdiTable,
|
|
||||||
permissions: 'READ_PRODUCTION_OPERATIONS'
|
|
||||||
},
|
|
||||||
{
|
|
||||||
href: '/quality_inspections/quality_inspections-list',
|
|
||||||
label: 'Quality inspections',
|
|
||||||
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
|
|
||||||
// @ts-ignore
|
|
||||||
icon: 'mdiClipboardSearch' in icon ? icon['mdiClipboardSearch' as keyof typeof icon] : icon.mdiTable ?? icon.mdiTable,
|
|
||||||
permissions: 'READ_QUALITY_INSPECTIONS'
|
|
||||||
},
|
|
||||||
{
|
|
||||||
href: '/nonconformances/nonconformances-list',
|
|
||||||
label: 'Nonconformances',
|
|
||||||
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
|
|
||||||
// @ts-ignore
|
|
||||||
icon: 'mdiAlertOctagonOutline' in icon ? icon['mdiAlertOctagonOutline' as keyof typeof icon] : icon.mdiTable ?? icon.mdiTable,
|
|
||||||
permissions: 'READ_NONCONFORMANCES'
|
|
||||||
},
|
|
||||||
{
|
|
||||||
href: '/vendors/vendors-list',
|
|
||||||
label: 'Vendors',
|
|
||||||
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
|
|
||||||
// @ts-ignore
|
|
||||||
icon: 'mdiTruckFast' in icon ? icon['mdiTruckFast' as keyof typeof icon] : icon.mdiTable ?? icon.mdiTable,
|
|
||||||
permissions: 'READ_VENDORS'
|
|
||||||
},
|
|
||||||
{
|
|
||||||
href: '/venues/venues-list',
|
|
||||||
label: 'Venues',
|
|
||||||
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
|
|
||||||
// @ts-ignore
|
|
||||||
icon: 'mdiMapMarkerRadius' in icon ? icon['mdiMapMarkerRadius' as keyof typeof icon] : icon.mdiTable ?? icon.mdiTable,
|
|
||||||
permissions: 'READ_VENUES'
|
|
||||||
},
|
|
||||||
{
|
|
||||||
href: '/events/events-list',
|
|
||||||
label: 'Events',
|
|
||||||
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
|
|
||||||
// @ts-ignore
|
|
||||||
icon: 'mdiCalendarStar' in icon ? icon['mdiCalendarStar' as keyof typeof icon] : icon.mdiTable ?? icon.mdiTable,
|
|
||||||
permissions: 'READ_EVENTS'
|
|
||||||
},
|
|
||||||
{
|
|
||||||
href: '/event_tasks/event_tasks-list',
|
|
||||||
label: 'Event tasks',
|
|
||||||
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
|
|
||||||
// @ts-ignore
|
|
||||||
icon: 'mdiChecklist' in icon ? icon['mdiChecklist' as keyof typeof icon] : icon.mdiTable ?? icon.mdiTable,
|
|
||||||
permissions: 'READ_EVENT_TASKS'
|
|
||||||
},
|
|
||||||
{
|
|
||||||
href: '/event_guests/event_guests-list',
|
|
||||||
label: 'Event guests',
|
|
||||||
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
|
|
||||||
// @ts-ignore
|
|
||||||
icon: 'mdiAccountGroup' in icon ? icon['mdiAccountGroup' as keyof typeof icon] : icon.mdiTable ?? icon.mdiTable,
|
|
||||||
permissions: 'READ_EVENT_GUESTS'
|
|
||||||
},
|
|
||||||
{
|
|
||||||
href: '/event_schedule_items/event_schedule_items-list',
|
|
||||||
label: 'Event schedule items',
|
|
||||||
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
|
|
||||||
// @ts-ignore
|
|
||||||
icon: 'mdiTimelineText' in icon ? icon['mdiTimelineText' as keyof typeof icon] : icon.mdiTable ?? icon.mdiTable,
|
|
||||||
permissions: 'READ_EVENT_SCHEDULE_ITEMS'
|
|
||||||
},
|
|
||||||
{
|
|
||||||
href: '/budget_items/budget_items-list',
|
|
||||||
label: 'Budget items',
|
|
||||||
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
|
|
||||||
// @ts-ignore
|
|
||||||
icon: 'mdiCurrencyUsd' in icon ? icon['mdiCurrencyUsd' as keyof typeof icon] : icon.mdiTable ?? icon.mdiTable,
|
|
||||||
permissions: 'READ_BUDGET_ITEMS'
|
|
||||||
},
|
|
||||||
{
|
|
||||||
href: '/profile',
|
|
||||||
label: 'Profile',
|
|
||||||
icon: icon.mdiAccountCircle,
|
|
||||||
},
|
|
||||||
|
|
||||||
|
|
||||||
{
|
|
||||||
href: '/api-docs',
|
|
||||||
target: '_blank',
|
|
||||||
label: 'Swagger API',
|
|
||||||
icon: icon.mdiFileCode,
|
|
||||||
permissions: 'READ_API_DOCS'
|
|
||||||
},
|
},
|
||||||
]
|
]
|
||||||
|
|
||||||
|
|||||||
@ -1,15 +1,7 @@
|
|||||||
import {
|
import {
|
||||||
mdiMenu,
|
|
||||||
mdiClockOutline,
|
|
||||||
mdiCloud,
|
|
||||||
mdiCrop,
|
|
||||||
mdiAccount,
|
mdiAccount,
|
||||||
mdiCogOutline,
|
|
||||||
mdiEmail,
|
|
||||||
mdiLogout,
|
mdiLogout,
|
||||||
mdiThemeLightDark,
|
mdiThemeLightDark,
|
||||||
mdiGithub,
|
|
||||||
mdiVuejs,
|
|
||||||
} from '@mdi/js'
|
} from '@mdi/js'
|
||||||
import { MenuNavBarItem } from './interfaces'
|
import { MenuNavBarItem } from './interfaces'
|
||||||
|
|
||||||
@ -19,7 +11,7 @@ const menuNavBar: MenuNavBarItem[] = [
|
|||||||
menu: [
|
menu: [
|
||||||
{
|
{
|
||||||
icon: mdiAccount,
|
icon: mdiAccount,
|
||||||
label: 'My Profile',
|
label: 'Profilim',
|
||||||
href: '/profile',
|
href: '/profile',
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
@ -27,27 +19,25 @@ const menuNavBar: MenuNavBarItem[] = [
|
|||||||
},
|
},
|
||||||
{
|
{
|
||||||
icon: mdiLogout,
|
icon: mdiLogout,
|
||||||
label: 'Log Out',
|
label: 'Çıkış yap',
|
||||||
isLogout: true,
|
isLogout: true,
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
icon: mdiThemeLightDark,
|
icon: mdiThemeLightDark,
|
||||||
label: 'Light/Dark',
|
label: 'Açık/Koyu Tema',
|
||||||
isDesktopNoLabel: true,
|
isDesktopNoLabel: true,
|
||||||
isToggleLightDark: true,
|
isToggleLightDark: true,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
icon: mdiLogout,
|
icon: mdiLogout,
|
||||||
label: 'Log out',
|
label: 'Çıkış yap',
|
||||||
isDesktopNoLabel: true,
|
isDesktopNoLabel: true,
|
||||||
isLogout: true,
|
isLogout: true,
|
||||||
},
|
},
|
||||||
]
|
]
|
||||||
|
|
||||||
export const webPagesNavBar = [
|
export const webPagesNavBar = []
|
||||||
|
|
||||||
];
|
|
||||||
|
|
||||||
export default menuNavBar
|
export default menuNavBar
|
||||||
|
|||||||
@ -3,24 +3,30 @@ import type { AppProps } from 'next/app';
|
|||||||
import type { ReactElement, ReactNode } from 'react';
|
import type { ReactElement, ReactNode } from 'react';
|
||||||
import type { NextPage } from 'next';
|
import type { NextPage } from 'next';
|
||||||
import Head from 'next/head';
|
import Head from 'next/head';
|
||||||
|
import { Poppins } from 'next/font/google';
|
||||||
import { store } from '../stores/store';
|
import { store } from '../stores/store';
|
||||||
import { Provider } from 'react-redux';
|
import { Provider } from 'react-redux';
|
||||||
import '../css/main.css';
|
import '../css/main.css';
|
||||||
import axios from 'axios';
|
import axios from 'axios';
|
||||||
import { baseURLApi } from '../config';
|
import { baseURLApi } from '../config';
|
||||||
import { useRouter } from 'next/router';
|
import { useRouter } from 'next/router';
|
||||||
import ErrorBoundary from "../components/ErrorBoundary";
|
import ErrorBoundary from '../components/ErrorBoundary';
|
||||||
import DevModeBadge from '../components/DevModeBadge';
|
import DevModeBadge from '../components/DevModeBadge';
|
||||||
import 'intro.js/introjs.css';
|
import 'intro.js/introjs.css';
|
||||||
import { appWithTranslation } from 'next-i18next';
|
import { appWithTranslation } from 'next-i18next';
|
||||||
import '../i18n';
|
import '../i18n';
|
||||||
import IntroGuide from '../components/IntroGuide';
|
import IntroGuide from '../components/IntroGuide';
|
||||||
import { appSteps, loginSteps, usersSteps, rolesSteps } from '../stores/introSteps';
|
import { appSteps, loginSteps, usersSteps, rolesSteps } from '../stores/introSteps';
|
||||||
|
|
||||||
|
const poppins = Poppins({
|
||||||
|
subsets: ['latin'],
|
||||||
|
weight: ['400', '500', '600', '700'],
|
||||||
|
display: 'swap',
|
||||||
|
});
|
||||||
|
|
||||||
// Initialize axios
|
|
||||||
axios.defaults.baseURL = process.env.NEXT_PUBLIC_BACK_API
|
axios.defaults.baseURL = process.env.NEXT_PUBLIC_BACK_API
|
||||||
? process.env.NEXT_PUBLIC_BACK_API
|
? process.env.NEXT_PUBLIC_BACK_API
|
||||||
: baseURLApi;
|
: baseURLApi;
|
||||||
|
|
||||||
axios.defaults.headers.common['Content-Type'] = 'application/json';
|
axios.defaults.headers.common['Content-Type'] = 'application/json';
|
||||||
|
|
||||||
@ -33,95 +39,94 @@ type AppPropsWithLayout = AppProps & {
|
|||||||
}
|
}
|
||||||
|
|
||||||
function MyApp({ Component, pageProps }: AppPropsWithLayout) {
|
function MyApp({ Component, pageProps }: AppPropsWithLayout) {
|
||||||
// Use the layout defined at the page level, if available
|
|
||||||
const getLayout = Component.getLayout || ((page) => page);
|
const getLayout = Component.getLayout || ((page) => page);
|
||||||
const router = useRouter();
|
const router = useRouter();
|
||||||
const [stepsEnabled, setStepsEnabled] = React.useState(false);
|
const [stepsEnabled, setStepsEnabled] = React.useState(false);
|
||||||
const [stepName, setStepName] = React.useState('');
|
const [stepName, setStepName] = React.useState('');
|
||||||
const [steps, setSteps] = React.useState([]);
|
const [steps, setSteps] = React.useState([]);
|
||||||
|
|
||||||
axios.interceptors.request.use(
|
React.useEffect(() => {
|
||||||
config => {
|
document.documentElement.lang = 'tr';
|
||||||
const token = localStorage.getItem('token');
|
}, []);
|
||||||
|
|
||||||
if (token) {
|
axios.interceptors.request.use(
|
||||||
config.headers.Authorization = `Bearer ${token}`;
|
(config) => {
|
||||||
} else {
|
const token = localStorage.getItem('token');
|
||||||
delete config.headers.Authorization;
|
|
||||||
}
|
|
||||||
|
|
||||||
return config;
|
if (token) {
|
||||||
},
|
config.headers.Authorization = `Bearer ${token}`;
|
||||||
error => {
|
} else {
|
||||||
return Promise.reject(error);
|
delete config.headers.Authorization;
|
||||||
}
|
}
|
||||||
);
|
|
||||||
|
|
||||||
// TODO: Remove this code in future releases
|
return config;
|
||||||
React.useEffect(() => {
|
},
|
||||||
const allowedOrigin = (() => {
|
(error) => Promise.reject(error),
|
||||||
if (!document.referrer) {
|
);
|
||||||
return null;
|
|
||||||
}
|
React.useEffect(() => {
|
||||||
try {
|
const allowedOrigin = (() => {
|
||||||
return new URL(document.referrer).origin;
|
if (!document.referrer) {
|
||||||
} catch (error) {
|
return null;
|
||||||
console.warn('[postMessage] Failed to parse parent origin from referrer', error);
|
}
|
||||||
return null;
|
try {
|
||||||
}
|
return new URL(document.referrer).origin;
|
||||||
})();
|
} catch (error) {
|
||||||
|
console.warn('[postMessage] Failed to parse parent origin from referrer', error);
|
||||||
const handleMessage = async (event: MessageEvent) => {
|
return null;
|
||||||
if (event.data === 'getLocation') {
|
}
|
||||||
event.source?.postMessage(
|
})();
|
||||||
{ iframeLocation: window.location.pathname },
|
|
||||||
event.origin,
|
const handleMessage = async (event: MessageEvent) => {
|
||||||
);
|
if (event.data === 'getLocation') {
|
||||||
return;
|
event.source?.postMessage(
|
||||||
}
|
{ iframeLocation: window.location.pathname },
|
||||||
|
event.origin,
|
||||||
if (event.data === 'getAuthToken') {
|
);
|
||||||
if (allowedOrigin && event.origin !== allowedOrigin) {
|
return;
|
||||||
console.warn('[postMessage] Blocked getAuthToken from origin', event.origin);
|
}
|
||||||
return;
|
|
||||||
}
|
if (event.data === 'getAuthToken') {
|
||||||
const token = localStorage.getItem('token');
|
if (allowedOrigin && event.origin !== allowedOrigin) {
|
||||||
const user = localStorage.getItem('user');
|
console.warn('[postMessage] Blocked getAuthToken from origin', event.origin);
|
||||||
event.source?.postMessage(
|
return;
|
||||||
{ iframeAuthToken: token, iframeAuthUser: user },
|
}
|
||||||
event.origin,
|
const token = localStorage.getItem('token');
|
||||||
);
|
const user = localStorage.getItem('user');
|
||||||
return;
|
event.source?.postMessage(
|
||||||
}
|
{ iframeAuthToken: token, iframeAuthUser: user },
|
||||||
|
event.origin,
|
||||||
if (event.data === 'getScreenshot') {
|
);
|
||||||
try {
|
return;
|
||||||
const html2canvas = (await import('html2canvas')).default;
|
}
|
||||||
const canvas = await html2canvas(document.body, { useCORS: true });
|
|
||||||
const url = canvas.toDataURL('image/jpeg', 0.8);
|
if (event.data === 'getScreenshot') {
|
||||||
event.source?.postMessage({ iframeScreenshot: url }, event.origin);
|
try {
|
||||||
} catch (e) {
|
const html2canvas = (await import('html2canvas')).default;
|
||||||
console.error('html2canvas failed', e);
|
const canvas = await html2canvas(document.body, { useCORS: true });
|
||||||
event.source?.postMessage({ iframeScreenshot: null }, event.origin);
|
const url = canvas.toDataURL('image/jpeg', 0.8);
|
||||||
}
|
event.source?.postMessage({ iframeScreenshot: url }, event.origin);
|
||||||
}
|
} catch (e) {
|
||||||
};
|
console.error('html2canvas failed', e);
|
||||||
|
event.source?.postMessage({ iframeScreenshot: null }, event.origin);
|
||||||
window.addEventListener('message', handleMessage);
|
}
|
||||||
return () => window.removeEventListener('message', handleMessage);
|
}
|
||||||
}, []);
|
};
|
||||||
|
|
||||||
|
window.addEventListener('message', handleMessage);
|
||||||
|
return () => window.removeEventListener('message', handleMessage);
|
||||||
|
}, []);
|
||||||
|
|
||||||
React.useEffect(() => {
|
React.useEffect(() => {
|
||||||
// Tour is disabled by default in generated projects.
|
|
||||||
return;
|
return;
|
||||||
const isCompleted = (stepKey: string) => {
|
const isCompleted = (stepKey: string) => {
|
||||||
return localStorage.getItem(`completed_${stepKey}`) === 'true';
|
return localStorage.getItem(`completed_${stepKey}`) === 'true';
|
||||||
};
|
};
|
||||||
if (router.pathname === '/login' && !isCompleted('loginSteps')) {
|
if (router.pathname === '/login' && !isCompleted('loginSteps')) {
|
||||||
setSteps(loginSteps);
|
setSteps(loginSteps);
|
||||||
setStepName('loginSteps');
|
setStepName('loginSteps');
|
||||||
setStepsEnabled(true);
|
setStepsEnabled(true);
|
||||||
}else if (router.pathname === '/dashboard' && !isCompleted('appSteps')) {
|
} else if (router.pathname === '/dashboard' && !isCompleted('appSteps')) {
|
||||||
setTimeout(() => {
|
setTimeout(() => {
|
||||||
setSteps(appSteps);
|
setSteps(appSteps);
|
||||||
setStepName('appSteps');
|
setStepName('appSteps');
|
||||||
@ -149,51 +154,56 @@ function MyApp({ Component, pageProps }: AppPropsWithLayout) {
|
|||||||
setStepsEnabled(false);
|
setStepsEnabled(false);
|
||||||
};
|
};
|
||||||
|
|
||||||
const title = 'DakikTabela Ops Suite'
|
const title = 'DakikTabela Operasyon Platformu';
|
||||||
const description = "Unified ops suite for CRM pipelines, manufacturing ERP tracking, and event planning coordination with ownership and follow-ups."
|
const description =
|
||||||
const url = "https://flatlogic.com/"
|
'DakikTabela için CRM, üretim ERP ve etkinlik operasyonlarını tek merkezde yöneten Türkçe iş platformu.';
|
||||||
const image = "https://project-screens.s3.amazonaws.com/screenshots/39422/app-hero-20260331-214356.png"
|
const url = 'https://flatlogic.com/';
|
||||||
const imageWidth = '1920'
|
const image = 'https://project-screens.s3.amazonaws.com/screenshots/39422/app-hero-20260331-214356.png';
|
||||||
const imageHeight = '960'
|
const imageWidth = '1920';
|
||||||
|
const imageHeight = '960';
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Provider store={store}>
|
<Provider store={store}>
|
||||||
{getLayout(
|
<div className={poppins.className}>
|
||||||
<>
|
{getLayout(
|
||||||
<Head>
|
<>
|
||||||
<meta name="description" content={description} />
|
<Head>
|
||||||
|
<meta name='description' content={description} />
|
||||||
|
|
||||||
<meta property="og:url" content={url} />
|
<meta property='og:url' content={url} />
|
||||||
<meta property="og:site_name" content="https://flatlogic.com/" />
|
<meta property='og:site_name' content='https://flatlogic.com/' />
|
||||||
<meta property="og:title" content={title} />
|
<meta property='og:title' content={title} />
|
||||||
<meta property="og:description" content={description} />
|
<meta property='og:description' content={description} />
|
||||||
<meta property="og:image" content={image} />
|
<meta property='og:image' content={image} />
|
||||||
<meta property="og:image:type" content="image/png" />
|
<meta property='og:image:type' content='image/png' />
|
||||||
<meta property="og:image:width" content={imageWidth} />
|
<meta property='og:image:width' content={imageWidth} />
|
||||||
<meta property="og:image:height" content={imageHeight} />
|
<meta property='og:image:height' content={imageHeight} />
|
||||||
|
|
||||||
<meta property="twitter:card" content="summary_large_image" />
|
<meta property='twitter:card' content='summary_large_image' />
|
||||||
<meta property="twitter:title" content={title} />
|
<meta property='twitter:title' content={title} />
|
||||||
<meta property="twitter:description" content={description} />
|
<meta property='twitter:description' content={description} />
|
||||||
<meta property="twitter:image:src" content={image} />
|
<meta property='twitter:image:src' content={image} />
|
||||||
<meta property="twitter:image:width" content={imageWidth} />
|
<meta property='twitter:image:width' content={imageWidth} />
|
||||||
<meta property="twitter:image:height" content={imageHeight} />
|
<meta property='twitter:image:height' content={imageHeight} />
|
||||||
|
|
||||||
<link rel="icon" href="/favicon.svg" />
|
<link rel='icon' href='/favicon.svg' />
|
||||||
</Head>
|
</Head>
|
||||||
|
|
||||||
<ErrorBoundary>
|
<ErrorBoundary>
|
||||||
<Component {...pageProps} />
|
<Component {...pageProps} />
|
||||||
</ErrorBoundary>
|
</ErrorBoundary>
|
||||||
<IntroGuide
|
<IntroGuide
|
||||||
steps={steps}
|
steps={steps}
|
||||||
stepsName={stepName}
|
stepsName={stepName}
|
||||||
stepsEnabled={stepsEnabled}
|
stepsEnabled={stepsEnabled}
|
||||||
onExit={handleExit}
|
onExit={handleExit}
|
||||||
/>
|
/>
|
||||||
{(process.env.NODE_ENV === 'development' || process.env.NODE_ENV === 'dev_stage') && <DevModeBadge />}
|
{(process.env.NODE_ENV === 'development' || process.env.NODE_ENV === 'dev_stage') && (
|
||||||
</>
|
<DevModeBadge />
|
||||||
)}
|
)}
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
</Provider>
|
</Provider>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|||||||
File diff suppressed because it is too large
Load Diff
@ -38,7 +38,7 @@ export default function Forgot() {
|
|||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<Head>
|
<Head>
|
||||||
<title>{getPageTitle('Login')}</title>
|
<title>{getPageTitle('Şifremi Unuttum')}</title>
|
||||||
</Head>
|
</Head>
|
||||||
|
|
||||||
<SectionFullScreen bg='violet'>
|
<SectionFullScreen bg='violet'>
|
||||||
@ -50,7 +50,7 @@ export default function Forgot() {
|
|||||||
onSubmit={(values) => handleSubmit(values)}
|
onSubmit={(values) => handleSubmit(values)}
|
||||||
>
|
>
|
||||||
<Form>
|
<Form>
|
||||||
<FormField label='Email' help='Please enter your email'>
|
<FormField label='E-posta' help='E-posta adresinizi yazın'>
|
||||||
<Field name='email' />
|
<Field name='email' />
|
||||||
</FormField>
|
</FormField>
|
||||||
|
|
||||||
@ -59,12 +59,12 @@ export default function Forgot() {
|
|||||||
<BaseButtons>
|
<BaseButtons>
|
||||||
<BaseButton
|
<BaseButton
|
||||||
type='submit'
|
type='submit'
|
||||||
label={loading ? 'Loading...' : 'Submit' }
|
label={loading ? 'Yükleniyor...' : 'Gönder' }
|
||||||
color='info'
|
color='info'
|
||||||
/>
|
/>
|
||||||
<BaseButton
|
<BaseButton
|
||||||
href={'/login'}
|
href={'/login'}
|
||||||
label={'Login'}
|
label={'Giriş yap'}
|
||||||
color='info'
|
color='info'
|
||||||
/>
|
/>
|
||||||
</BaseButtons>
|
</BaseButtons>
|
||||||
|
|||||||
@ -1,166 +1,156 @@
|
|||||||
|
import { mdiArrowRight, mdiCheckCircleOutline, mdiFactory, mdiHandshake, mdiCalendarStar } from '@mdi/js'
|
||||||
|
import Head from 'next/head'
|
||||||
|
import Link from 'next/link'
|
||||||
|
import React, { ReactElement } from 'react'
|
||||||
|
import LayoutGuest from '../layouts/Guest'
|
||||||
|
import Logo from '../components/Logo'
|
||||||
|
import { getPageTitle } from '../config'
|
||||||
|
|
||||||
import React, { useEffect, useState } from 'react';
|
const platformCards = [
|
||||||
import type { ReactElement } from 'react';
|
{
|
||||||
import Head from 'next/head';
|
title: 'Satış CRM',
|
||||||
import Link from 'next/link';
|
description: 'Potansiyel müşteri, fırsat, kişi, aktivite ve takip adımlarını tek yerden yönetin.',
|
||||||
import BaseButton from '../components/BaseButton';
|
icon: mdiHandshake,
|
||||||
import CardBox from '../components/CardBox';
|
href: '/sales-hub',
|
||||||
import SectionFullScreen from '../components/SectionFullScreen';
|
cta: 'Satış merkezini aç',
|
||||||
import LayoutGuest from '../layouts/Guest';
|
},
|
||||||
import BaseDivider from '../components/BaseDivider';
|
{
|
||||||
import BaseButtons from '../components/BaseButtons';
|
title: 'Üretim ERP',
|
||||||
import { getPageTitle } from '../config';
|
description: 'Malzeme, depo, stok, makine, üretim emri ve kalite operasyonlarını görünür kılın.',
|
||||||
import { useAppSelector } from '../stores/hooks';
|
icon: mdiFactory,
|
||||||
import CardBoxComponentTitle from "../components/CardBoxComponentTitle";
|
href: '/dashboard',
|
||||||
import { getPexelsImage, getPexelsVideo } from '../helpers/pexels';
|
cta: 'ERP görünümünü aç',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: 'Etkinlik Yönetimi',
|
||||||
|
description: 'Mekan, tedarikçi, davetli, görev, takvim ve bütçe süreçlerini birlikte koordine edin.',
|
||||||
|
icon: mdiCalendarStar,
|
||||||
|
href: '/dashboard',
|
||||||
|
cta: 'Etkinlik modülünü aç',
|
||||||
|
},
|
||||||
|
]
|
||||||
|
|
||||||
|
const promises = [
|
||||||
|
'Tüm ekipler için Türkçe arayüz ve kurumsal görünüm',
|
||||||
|
'Yetki bazlı erişim, sahiplik ve takip disiplini',
|
||||||
|
'CRM, ERP ve etkinlik süreçleri arasında tek operasyon ekranı',
|
||||||
|
]
|
||||||
|
|
||||||
export default function Starter() {
|
export default function IndexPage() {
|
||||||
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('background');
|
|
||||||
const textColor = useAppSelector((state) => state.style.linkColor);
|
|
||||||
|
|
||||||
const title = 'DakikTabela Ops 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) => (
|
|
||||||
<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>)
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
return (
|
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>
|
<Head>
|
||||||
<title>{getPageTitle('Starter Page')}</title>
|
<title>{getPageTitle('DakikTabela')}</title>
|
||||||
</Head>
|
</Head>
|
||||||
|
|
||||||
<SectionFullScreen bg='violet'>
|
<main className='min-h-screen bg-[#f8fafc] text-slate-900'>
|
||||||
<div
|
<section className='relative overflow-hidden border-b border-slate-200 bg-[radial-gradient(circle_at_top_left,_rgba(20,184,166,0.16),_transparent_28%),radial-gradient(circle_at_top_right,_rgba(29,78,216,0.18),_transparent_32%),linear-gradient(180deg,_#ffffff,_#eef4ff)]'>
|
||||||
className={`flex ${
|
<div className='mx-auto flex min-h-screen w-full max-w-7xl flex-col px-6 py-8 lg:px-10'>
|
||||||
contentPosition === 'right' ? 'flex-row-reverse' : 'flex-row'
|
<header className='flex flex-col gap-4 border-b border-slate-200/80 pb-6 md:flex-row md:items-center md:justify-between'>
|
||||||
} min-h-screen w-full`}
|
<div className='flex items-center gap-4'>
|
||||||
>
|
<Logo className='h-14 w-auto rounded-2xl bg-white p-2 shadow-sm' />
|
||||||
{contentType === 'image' && contentPosition !== 'background'
|
<div>
|
||||||
? imageBlock(illustrationImage)
|
<p className='text-xs font-semibold uppercase tracking-[0.24em] text-cyan-700'>dakiktabela.com</p>
|
||||||
: null}
|
<h1 className='mt-1 text-xl font-semibold tracking-tight'>DakikTabela Operasyon Platformu</h1>
|
||||||
{contentType === 'video' && contentPosition !== 'background'
|
</div>
|
||||||
? videoBlock(illustrationVideo)
|
</div>
|
||||||
: null}
|
<nav className='flex flex-wrap items-center gap-3'>
|
||||||
<div className='flex items-center justify-center flex-col space-y-4 w-full lg:w-full'>
|
<Link
|
||||||
<CardBox className='w-full md:w-3/5 lg:w-2/3'>
|
href='/login'
|
||||||
<CardBoxComponentTitle title="Welcome to your DakikTabela Ops Suite app!"/>
|
className='inline-flex items-center justify-center rounded-full border border-slate-300 bg-white px-5 py-2.5 text-sm font-semibold text-slate-900 transition hover:border-cyan-400 hover:text-cyan-700'
|
||||||
|
>
|
||||||
|
Giriş yap
|
||||||
|
</Link>
|
||||||
|
<Link
|
||||||
|
href='/dashboard'
|
||||||
|
className='inline-flex items-center justify-center rounded-full bg-slate-900 px-5 py-2.5 text-sm font-semibold text-white transition hover:bg-cyan-600'
|
||||||
|
>
|
||||||
|
Yönetim panelini aç
|
||||||
|
</Link>
|
||||||
|
</nav>
|
||||||
|
</header>
|
||||||
|
|
||||||
<div className="space-y-3">
|
<div className='grid flex-1 items-center gap-14 py-14 lg:grid-cols-[1.1fr_0.9fr] lg:py-20'>
|
||||||
<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>
|
<div className='space-y-8'>
|
||||||
<p className='text-center text-gray-500'>For guides and documentation please check
|
<div className='inline-flex items-center rounded-full border border-cyan-200 bg-white/90 px-4 py-1.5 text-xs font-semibold uppercase tracking-[0.18em] text-cyan-800 shadow-sm'>
|
||||||
your local README.md and the <a className={`${textColor}`} href="https://flatlogic.com/documentation">Flatlogic documentation</a></p>
|
Kurumsal, Türkçe ve operasyon odaklı
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<BaseButtons>
|
<div className='space-y-5'>
|
||||||
<BaseButton
|
<h2 className='max-w-4xl text-4xl font-semibold tracking-tight text-slate-950 md:text-6xl'>
|
||||||
|
DakikTabela için satıştan üretime, üretimden etkinlik yönetimine uzanan tek iş platformu.
|
||||||
|
</h2>
|
||||||
|
<p className='max-w-2xl text-lg leading-8 text-slate-600'>
|
||||||
|
Bu yapı; fırsat sahipliği, takip disiplini, üretim görünürlüğü ve etkinlik koordinasyonunu
|
||||||
|
aynı ekranda toplar. Ekipler hangi kaydın kime ait olduğunu, sıradaki işi ve kritik gecikmeleri
|
||||||
|
anında görebilir.
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className='flex flex-wrap gap-4'>
|
||||||
|
<Link
|
||||||
|
href='/sales-hub'
|
||||||
|
className='inline-flex items-center gap-2 rounded-full bg-cyan-500 px-6 py-3 text-sm font-semibold text-slate-950 transition hover:bg-cyan-400'
|
||||||
|
>
|
||||||
|
Satış merkezine git
|
||||||
|
<svg viewBox='0 0 24 24' className='h-4 w-4 fill-current'>
|
||||||
|
<path d={mdiArrowRight} />
|
||||||
|
</svg>
|
||||||
|
</Link>
|
||||||
|
<Link
|
||||||
href='/login'
|
href='/login'
|
||||||
label='Login'
|
className='inline-flex items-center gap-2 rounded-full border border-slate-300 px-6 py-3 text-sm font-semibold text-slate-900 transition hover:border-cyan-400 hover:text-cyan-700'
|
||||||
color='info'
|
>
|
||||||
className='w-full'
|
Sisteme giriş yap
|
||||||
/>
|
</Link>
|
||||||
|
</div>
|
||||||
|
|
||||||
</BaseButtons>
|
<div className='grid gap-3'>
|
||||||
</CardBox>
|
{promises.map((promise) => (
|
||||||
</div>
|
<div key={promise} className='flex items-start gap-3 rounded-2xl bg-white/85 p-4 shadow-sm'>
|
||||||
</div>
|
<svg viewBox='0 0 24 24' className='mt-0.5 h-5 w-5 shrink-0 fill-cyan-600'>
|
||||||
</SectionFullScreen>
|
<path d={mdiCheckCircleOutline} />
|
||||||
<div className='bg-black text-white flex flex-col text-center justify-center md:flex-row'>
|
</svg>
|
||||||
<p className='py-6 text-sm'>© 2026 <span>{title}</span>. All rights reserved</p>
|
<p className='text-sm leading-6 text-slate-700'>{promise}</p>
|
||||||
<Link className='py-6 ml-4 text-sm' href='/privacy-policy/'>
|
</div>
|
||||||
Privacy Policy
|
))}
|
||||||
</Link>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
</div>
|
<div className='grid gap-4'>
|
||||||
);
|
{platformCards.map((card) => (
|
||||||
|
<div
|
||||||
|
key={card.title}
|
||||||
|
className='rounded-[28px] border border-white/80 bg-white/90 p-6 shadow-[0_24px_80px_rgba(15,23,42,0.08)] backdrop-blur'
|
||||||
|
>
|
||||||
|
<div className='flex items-start justify-between gap-4'>
|
||||||
|
<div>
|
||||||
|
<h3 className='text-xl font-semibold text-slate-950'>{card.title}</h3>
|
||||||
|
<p className='mt-3 text-sm leading-6 text-slate-600'>{card.description}</p>
|
||||||
|
</div>
|
||||||
|
<div className='rounded-2xl bg-slate-900 p-3 text-white'>
|
||||||
|
<svg viewBox='0 0 24 24' className='h-5 w-5 fill-current'>
|
||||||
|
<path d={card.icon} />
|
||||||
|
</svg>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<Link href={card.href} className='mt-5 inline-flex items-center gap-2 text-sm font-semibold text-cyan-700'>
|
||||||
|
{card.cta}
|
||||||
|
<svg viewBox='0 0 24 24' className='h-4 w-4 fill-current'>
|
||||||
|
<path d={mdiArrowRight} />
|
||||||
|
</svg>
|
||||||
|
</Link>
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
</main>
|
||||||
|
</>
|
||||||
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
Starter.getLayout = function getLayout(page: ReactElement) {
|
IndexPage.getLayout = function getLayout(page: ReactElement) {
|
||||||
return <LayoutGuest>{page}</LayoutGuest>;
|
return <LayoutGuest>{page}</LayoutGuest>
|
||||||
};
|
}
|
||||||
|
|
||||||
|
|||||||
@ -154,7 +154,7 @@ export default function Login() {
|
|||||||
backgroundRepeat: 'no-repeat',
|
backgroundRepeat: 'no-repeat',
|
||||||
} : {}}>
|
} : {}}>
|
||||||
<Head>
|
<Head>
|
||||||
<title>{getPageTitle('Login')}</title>
|
<title>{getPageTitle('Giriş')}</title>
|
||||||
</Head>
|
</Head>
|
||||||
|
|
||||||
<SectionFullScreen bg='violet'>
|
<SectionFullScreen bg='violet'>
|
||||||
@ -170,25 +170,25 @@ export default function Login() {
|
|||||||
<div className='flex flex-row text-gray-500 justify-between'>
|
<div className='flex flex-row text-gray-500 justify-between'>
|
||||||
<div>
|
<div>
|
||||||
|
|
||||||
<p className='mb-2'>Use{' '}
|
<p className='mb-2'>Kullan{' '}
|
||||||
<code className={`cursor-pointer ${textColor} `}
|
<code className={`cursor-pointer ${textColor} `}
|
||||||
data-password="9c0c2fc3"
|
data-password="9c0c2fc3"
|
||||||
onClick={(e) => setLogin(e.target)}>super_admin@flatlogic.com</code>{' / '}
|
onClick={(e) => setLogin(e.target)}>super_admin@flatlogic.com</code>{' / '}
|
||||||
<code className={`${textColor}`}>9c0c2fc3</code>{' / '}
|
<code className={`${textColor}`}>9c0c2fc3</code>{' / '}
|
||||||
to login as Super Admin</p>
|
ile Süper Admin olarak giriş yapın</p>
|
||||||
|
|
||||||
<p className='mb-2'>Use{' '}
|
<p className='mb-2'>Kullan{' '}
|
||||||
<code className={`cursor-pointer ${textColor} `}
|
<code className={`cursor-pointer ${textColor} `}
|
||||||
data-password="9c0c2fc3"
|
data-password="9c0c2fc3"
|
||||||
onClick={(e) => setLogin(e.target)}>admin@flatlogic.com</code>{' / '}
|
onClick={(e) => setLogin(e.target)}>admin@flatlogic.com</code>{' / '}
|
||||||
<code className={`${textColor}`}>9c0c2fc3</code>{' / '}
|
<code className={`${textColor}`}>9c0c2fc3</code>{' / '}
|
||||||
to login as Admin</p>
|
ile Admin olarak giriş yapın</p>
|
||||||
<p>Use <code
|
<p>Use <code
|
||||||
className={`cursor-pointer ${textColor} `}
|
className={`cursor-pointer ${textColor} `}
|
||||||
data-password="0d5e2b23b648"
|
data-password="0d5e2b23b648"
|
||||||
onClick={(e) => setLogin(e.target)}>client@hello.com</code>{' / '}
|
onClick={(e) => setLogin(e.target)}>client@hello.com</code>{' / '}
|
||||||
<code className={`${textColor}`}>0d5e2b23b648</code>{' / '}
|
<code className={`${textColor}`}>0d5e2b23b648</code>{' / '}
|
||||||
to login as User</p>
|
ile Kullanıcı olarak giriş yapın</p>
|
||||||
</div>
|
</div>
|
||||||
<div>
|
<div>
|
||||||
<BaseIcon
|
<BaseIcon
|
||||||
@ -210,15 +210,15 @@ export default function Login() {
|
|||||||
>
|
>
|
||||||
<Form>
|
<Form>
|
||||||
<FormField
|
<FormField
|
||||||
label='Login'
|
label='Giriş'
|
||||||
help='Please enter your login'>
|
help='Giriş e-posta adresinizi yazın'>
|
||||||
<Field name='email' />
|
<Field name='email' />
|
||||||
</FormField>
|
</FormField>
|
||||||
|
|
||||||
<div className='relative'>
|
<div className='relative'>
|
||||||
<FormField
|
<FormField
|
||||||
label='Password'
|
label='Şifre'
|
||||||
help='Please enter your password'>
|
help='Şifrenizi yazın'>
|
||||||
<Field name='password' type={showPassword ? 'text' : 'password'} />
|
<Field name='password' type={showPassword ? 'text' : 'password'} />
|
||||||
</FormField>
|
</FormField>
|
||||||
<div
|
<div
|
||||||
@ -234,12 +234,12 @@ export default function Login() {
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className={'flex justify-between'}>
|
<div className={'flex justify-between'}>
|
||||||
<FormCheckRadio type='checkbox' label='Remember'>
|
<FormCheckRadio type='checkbox' label='Beni hatırla'>
|
||||||
<Field type='checkbox' name='remember' />
|
<Field type='checkbox' name='remember' />
|
||||||
</FormCheckRadio>
|
</FormCheckRadio>
|
||||||
|
|
||||||
<Link className={`${textColor} text-blue-600`} href={'/forgot'}>
|
<Link className={`${textColor} text-blue-600`} href={'/forgot'}>
|
||||||
Forgot password?
|
Şifremi unuttum
|
||||||
</Link>
|
</Link>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@ -249,16 +249,16 @@ export default function Login() {
|
|||||||
<BaseButton
|
<BaseButton
|
||||||
className={'w-full'}
|
className={'w-full'}
|
||||||
type='submit'
|
type='submit'
|
||||||
label={isFetching ? 'Loading...' : 'Login'}
|
label={isFetching ? 'Yükleniyor...' : 'Giriş yap'}
|
||||||
color='info'
|
color='info'
|
||||||
disabled={isFetching}
|
disabled={isFetching}
|
||||||
/>
|
/>
|
||||||
</BaseButtons>
|
</BaseButtons>
|
||||||
<br />
|
<br />
|
||||||
<p className={'text-center'}>
|
<p className={'text-center'}>
|
||||||
Don’t have an account yet?{' '}
|
Henüz hesabınız yok mu?{' '}
|
||||||
<Link className={`${textColor}`} href={'/register'}>
|
<Link className={`${textColor}`} href={'/register'}>
|
||||||
New Account
|
Yeni hesap oluştur
|
||||||
</Link>
|
</Link>
|
||||||
</p>
|
</p>
|
||||||
</Form>
|
</Form>
|
||||||
|
|||||||
@ -65,18 +65,18 @@ const EditUsers = () => {
|
|||||||
await dispatch(update({ id: currentUser.id, data }));
|
await dispatch(update({ id: currentUser.id, data }));
|
||||||
await dispatch(findMe());
|
await dispatch(findMe());
|
||||||
await router.push('/users/users-list');
|
await router.push('/users/users-list');
|
||||||
notify('success', 'Profile was updated!');
|
notify('success', 'Profil güncellendi.');
|
||||||
};
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<Head>
|
<Head>
|
||||||
<title>{getPageTitle('Edit profile')}</title>
|
<title>{getPageTitle('Profilimi Düzenle')}</title>
|
||||||
</Head>
|
</Head>
|
||||||
<SectionMain>
|
<SectionMain>
|
||||||
<SectionTitleLineWithButton
|
<SectionTitleLineWithButton
|
||||||
icon={mdiChartTimelineVariant}
|
icon={mdiChartTimelineVariant}
|
||||||
title='Edit profile'
|
title='Profilimi Düzenle'
|
||||||
main
|
main
|
||||||
>
|
>
|
||||||
{''}
|
{''}
|
||||||
@ -84,7 +84,7 @@ const EditUsers = () => {
|
|||||||
<CardBox>
|
<CardBox>
|
||||||
{currentUser?.avatar[0]?.publicUrl && <div className={'grid grid-cols-6 gap-4 mb-4'}>
|
{currentUser?.avatar[0]?.publicUrl && <div className={'grid grid-cols-6 gap-4 mb-4'}>
|
||||||
<div className="col-span-1 w-80 h-80 overflow-hidden border-2 rounded-full inline-flex items-center justify-center mb-8">
|
<div className="col-span-1 w-80 h-80 overflow-hidden border-2 rounded-full inline-flex items-center justify-center mb-8">
|
||||||
<img className="w-80 h-80 max-w-full max-h-full object-cover object-center" src={`${currentUser?.avatar[0]?.publicUrl}`} alt="Avatar" />
|
<img className="w-80 h-80 max-w-full max-h-full object-cover object-center" src={`${currentUser?.avatar[0]?.publicUrl}`} alt="Profil görseli" />
|
||||||
</div>
|
</div>
|
||||||
</div>}
|
</div>}
|
||||||
<Formik
|
<Formik
|
||||||
@ -95,7 +95,7 @@ const EditUsers = () => {
|
|||||||
<Form>
|
<Form>
|
||||||
<FormField>
|
<FormField>
|
||||||
<Field
|
<Field
|
||||||
label='Avatar'
|
label='Profil görseli'
|
||||||
color='info'
|
color='info'
|
||||||
icon={mdiUpload}
|
icon={mdiUpload}
|
||||||
path={'users/avatar'}
|
path={'users/avatar'}
|
||||||
@ -108,23 +108,23 @@ const EditUsers = () => {
|
|||||||
component={FormImagePicker}
|
component={FormImagePicker}
|
||||||
></Field>
|
></Field>
|
||||||
</FormField>
|
</FormField>
|
||||||
<FormField label='First Name'>
|
<FormField label='Ad'>
|
||||||
<Field name='firstName' placeholder='First Name' />
|
<Field name='firstName' placeholder='Ad' />
|
||||||
</FormField>
|
</FormField>
|
||||||
|
|
||||||
<FormField label='Last Name'>
|
<FormField label='Soyad'>
|
||||||
<Field name='lastName' placeholder='Last Name' />
|
<Field name='lastName' placeholder='Soyad' />
|
||||||
</FormField>
|
</FormField>
|
||||||
|
|
||||||
<FormField label='Phone Number'>
|
<FormField label='Telefon'>
|
||||||
<Field name='phoneNumber' placeholder='Phone Number' />
|
<Field name='phoneNumber' placeholder='Telefon numarası' />
|
||||||
</FormField>
|
</FormField>
|
||||||
|
|
||||||
<FormField label='E-Mail'>
|
<FormField label='E-Mail'>
|
||||||
<Field name='email' placeholder='E-Mail' disabled />
|
<Field name='email' placeholder='E-Mail' disabled />
|
||||||
</FormField>
|
</FormField>
|
||||||
|
|
||||||
<FormField label='App Role' labelFor='app_role'>
|
<FormField label='Uygulama rolü' labelFor='app_role'>
|
||||||
<Field
|
<Field
|
||||||
name='app_role'
|
name='app_role'
|
||||||
id='app_role'
|
id='app_role'
|
||||||
@ -135,7 +135,7 @@ const EditUsers = () => {
|
|||||||
></Field>
|
></Field>
|
||||||
</FormField>
|
</FormField>
|
||||||
|
|
||||||
<FormField label='Disabled' labelFor='disabled'>
|
<FormField label='Pasif' labelFor='disabled'>
|
||||||
<Field
|
<Field
|
||||||
name='disabled'
|
name='disabled'
|
||||||
id='disabled'
|
id='disabled'
|
||||||
@ -144,24 +144,24 @@ const EditUsers = () => {
|
|||||||
</FormField>
|
</FormField>
|
||||||
|
|
||||||
<FormField
|
<FormField
|
||||||
label="Password"
|
label="Şifre"
|
||||||
>
|
>
|
||||||
<Field
|
<Field
|
||||||
name="password"
|
name="password"
|
||||||
placeholder="password"
|
placeholder="Yeni şifre"
|
||||||
/>
|
/>
|
||||||
</FormField>
|
</FormField>
|
||||||
|
|
||||||
<BaseDivider />
|
<BaseDivider />
|
||||||
|
|
||||||
<BaseButtons>
|
<BaseButtons>
|
||||||
<BaseButton type='submit' color='info' label='Submit' />
|
<BaseButton type='submit' color='info' label='Kaydet' />
|
||||||
<BaseButton type='reset' color='info' outline label='Reset' />
|
<BaseButton type='reset' color='info' outline label='Formu sıfırla' />
|
||||||
<BaseButton
|
<BaseButton
|
||||||
type='reset'
|
type='reset'
|
||||||
color='danger'
|
color='danger'
|
||||||
outline
|
outline
|
||||||
label='Cancel'
|
label='İptal'
|
||||||
onClick={() => router.push('/users/users-list')}
|
onClick={() => router.push('/users/users-list')}
|
||||||
/>
|
/>
|
||||||
</BaseButtons>
|
</BaseButtons>
|
||||||
|
|||||||
@ -69,7 +69,7 @@ export default function Register() {
|
|||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<Head>
|
<Head>
|
||||||
<title>{getPageTitle('Login')}</title>
|
<title>{getPageTitle('Kayıt Ol')}</title>
|
||||||
</Head>
|
</Head>
|
||||||
|
|
||||||
<SectionFullScreen bg='violet'>
|
<SectionFullScreen bg='violet'>
|
||||||
@ -96,13 +96,13 @@ export default function Register() {
|
|||||||
placeholder="Select organization..."
|
placeholder="Select organization..."
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<FormField label='Email' help='Please enter your email'>
|
<FormField label='E-posta' help='E-posta adresinizi yazın'>
|
||||||
<Field type='email' name='email' />
|
<Field type='email' name='email' />
|
||||||
</FormField>
|
</FormField>
|
||||||
<FormField label='Password' help='Please enter your password'>
|
<FormField label='Şifre' help='Şifrenizi yazın'>
|
||||||
<Field type='password' name='password' />
|
<Field type='password' name='password' />
|
||||||
</FormField>
|
</FormField>
|
||||||
<FormField label='Confirm Password' help='Please confirm your password'>
|
<FormField label='Şifre Tekrarı' help='Şifrenizi tekrar yazın'>
|
||||||
<Field type='password' name='confirm' />
|
<Field type='password' name='confirm' />
|
||||||
</FormField>
|
</FormField>
|
||||||
|
|
||||||
@ -111,12 +111,12 @@ export default function Register() {
|
|||||||
<BaseButtons>
|
<BaseButtons>
|
||||||
<BaseButton
|
<BaseButton
|
||||||
type='submit'
|
type='submit'
|
||||||
label={loading ? 'Loading...' : 'Register' }
|
label={loading ? 'Yükleniyor...' : 'Kayıt ol' }
|
||||||
color='info'
|
color='info'
|
||||||
/>
|
/>
|
||||||
<BaseButton
|
<BaseButton
|
||||||
href={'/login'}
|
href={'/login'}
|
||||||
label={'Login'}
|
label={'Giriş yap'}
|
||||||
color='info'
|
color='info'
|
||||||
/>
|
/>
|
||||||
</BaseButtons>
|
</BaseButtons>
|
||||||
|
|||||||
744
frontend/src/pages/sales-hub.tsx
Normal file
744
frontend/src/pages/sales-hub.tsx
Normal file
@ -0,0 +1,744 @@
|
|||||||
|
import {
|
||||||
|
mdiArrowRight,
|
||||||
|
mdiCalendarClock,
|
||||||
|
mdiChartTimelineVariant,
|
||||||
|
mdiCheckCircleOutline,
|
||||||
|
mdiClockAlertOutline,
|
||||||
|
mdiCurrencyUsd,
|
||||||
|
mdiOpenInNew,
|
||||||
|
mdiTarget,
|
||||||
|
} from '@mdi/js'
|
||||||
|
import axios from 'axios'
|
||||||
|
import Head from 'next/head'
|
||||||
|
import Link from 'next/link'
|
||||||
|
import React, { ReactElement, useEffect, useMemo, useState } from 'react'
|
||||||
|
import BaseButton from '../components/BaseButton'
|
||||||
|
import CardBox from '../components/CardBox'
|
||||||
|
import FormField from '../components/FormField'
|
||||||
|
import LayoutAuthenticated from '../layouts/Authenticated'
|
||||||
|
import NotificationBar from '../components/NotificationBar'
|
||||||
|
import SectionMain from '../components/SectionMain'
|
||||||
|
import SectionTitleLineWithButton from '../components/SectionTitleLineWithButton'
|
||||||
|
import { getPageTitle } from '../config'
|
||||||
|
import { useAppSelector } from '../stores/hooks'
|
||||||
|
|
||||||
|
type Stats = {
|
||||||
|
myOpenLeads: number
|
||||||
|
myActiveDeals: number
|
||||||
|
dueToday: number
|
||||||
|
overdue: number
|
||||||
|
completedThisWeek: number
|
||||||
|
}
|
||||||
|
|
||||||
|
type FollowUpItem = {
|
||||||
|
id: string
|
||||||
|
subject: string
|
||||||
|
status: string
|
||||||
|
activityType: string
|
||||||
|
dueAt: string | null
|
||||||
|
relatedName: string
|
||||||
|
relatedType: 'lead' | 'deal' | 'contact' | 'activity'
|
||||||
|
relatedId: string | null
|
||||||
|
}
|
||||||
|
|
||||||
|
type RecentLead = {
|
||||||
|
id: string
|
||||||
|
title: string
|
||||||
|
status: string
|
||||||
|
estimatedValue: number | null
|
||||||
|
nextFollowUpAt: string | null
|
||||||
|
companyName: string | null
|
||||||
|
stageName: string | null
|
||||||
|
}
|
||||||
|
|
||||||
|
type StageSummary = {
|
||||||
|
id: string
|
||||||
|
name: string
|
||||||
|
dealCount: number
|
||||||
|
amount: number
|
||||||
|
}
|
||||||
|
|
||||||
|
type QuickOption = {
|
||||||
|
id: string
|
||||||
|
label: string
|
||||||
|
}
|
||||||
|
|
||||||
|
type SalesHubResponse = {
|
||||||
|
stats: Stats
|
||||||
|
followUps: FollowUpItem[]
|
||||||
|
recentLeads: RecentLead[]
|
||||||
|
stageSummary: StageSummary[]
|
||||||
|
quickOptions: {
|
||||||
|
leads: QuickOption[]
|
||||||
|
deals: QuickOption[]
|
||||||
|
contacts: QuickOption[]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
type RelationType = 'lead' | 'deal' | 'contact'
|
||||||
|
|
||||||
|
type FollowUpForm = {
|
||||||
|
subject: string
|
||||||
|
activityType: string
|
||||||
|
dueAt: string
|
||||||
|
relationType: RelationType
|
||||||
|
relationId: string
|
||||||
|
details: string
|
||||||
|
}
|
||||||
|
|
||||||
|
const emptyHubData: SalesHubResponse = {
|
||||||
|
stats: {
|
||||||
|
myOpenLeads: 0,
|
||||||
|
myActiveDeals: 0,
|
||||||
|
dueToday: 0,
|
||||||
|
overdue: 0,
|
||||||
|
completedThisWeek: 0,
|
||||||
|
},
|
||||||
|
followUps: [],
|
||||||
|
recentLeads: [],
|
||||||
|
stageSummary: [],
|
||||||
|
quickOptions: {
|
||||||
|
leads: [],
|
||||||
|
deals: [],
|
||||||
|
contacts: [],
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
const defaultForm: FollowUpForm = {
|
||||||
|
subject: '',
|
||||||
|
activityType: 'follow_up',
|
||||||
|
dueAt: '',
|
||||||
|
relationType: 'lead',
|
||||||
|
relationId: '',
|
||||||
|
details: '',
|
||||||
|
}
|
||||||
|
|
||||||
|
const relationLabels: Record<RelationType, string> = {
|
||||||
|
lead: 'potansiyel müşteri',
|
||||||
|
deal: 'fırsat',
|
||||||
|
contact: 'kişi',
|
||||||
|
}
|
||||||
|
|
||||||
|
const relationPaths: Record<'lead' | 'deal' | 'contact', string> = {
|
||||||
|
lead: '/leads',
|
||||||
|
deal: '/deals',
|
||||||
|
contact: '/contacts',
|
||||||
|
}
|
||||||
|
|
||||||
|
const activityTypeLabels: Record<string, string> = {
|
||||||
|
call: 'Arama',
|
||||||
|
email: 'E-posta',
|
||||||
|
meeting: 'Toplantı',
|
||||||
|
task: 'Görev',
|
||||||
|
note: 'Not',
|
||||||
|
demo: 'Demo',
|
||||||
|
follow_up: 'Takip',
|
||||||
|
}
|
||||||
|
|
||||||
|
const statusLabels: Record<string, string> = {
|
||||||
|
planned: 'Planlandı',
|
||||||
|
done: 'Tamamlandı',
|
||||||
|
canceled: 'İptal edildi',
|
||||||
|
in_progress: 'Devam ediyor',
|
||||||
|
}
|
||||||
|
|
||||||
|
const statCards = [
|
||||||
|
{
|
||||||
|
key: 'myOpenLeads',
|
||||||
|
label: 'Açık potansiyel müşteri',
|
||||||
|
helper: 'Henüz nitelendirilmemiş veya sıradaki temas bekleyen kayıtlar.',
|
||||||
|
icon: mdiTarget,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
key: 'myActiveDeals',
|
||||||
|
label: 'Aktif fırsat',
|
||||||
|
helper: 'Şu anda sizin sorumluluğunuzdaki fırsatlar.',
|
||||||
|
icon: mdiCurrencyUsd,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
key: 'dueToday',
|
||||||
|
label: 'Bugün aksiyon bekleyen',
|
||||||
|
helper: 'Gün bitmeden dokunulması gereken takipler.',
|
||||||
|
icon: mdiCalendarClock,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
key: 'overdue',
|
||||||
|
label: 'Geciken',
|
||||||
|
helper: 'Tarihi geçmiş ve momentum kaybetme riski taşıyan işler.',
|
||||||
|
icon: mdiClockAlertOutline,
|
||||||
|
},
|
||||||
|
] as const
|
||||||
|
|
||||||
|
const quickActionLinks = [
|
||||||
|
{
|
||||||
|
href: '/leads/leads-new',
|
||||||
|
label: 'Yeni potansiyel müşteri',
|
||||||
|
description: 'Sahibi belli yeni bir fırsatı ilk günden sisteme alın.',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
href: '/deals/deals-new',
|
||||||
|
label: 'Yeni fırsat aç',
|
||||||
|
description: 'Nitelenen işleri aktif pipeline içine taşıyın.',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
href: '/contacts/contacts-new',
|
||||||
|
label: 'Yeni kişi ekle',
|
||||||
|
description: 'Her fırsatın arkasındaki karar vericileri görünür tutun.',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
href: '/activities/activities-list',
|
||||||
|
label: 'Tüm aktiviteler',
|
||||||
|
description: 'Görev, toplantı, e-posta ve takiplerin tamamını inceleyin.',
|
||||||
|
},
|
||||||
|
]
|
||||||
|
|
||||||
|
const formatCurrency = (value?: number | null) => {
|
||||||
|
const amount = Number(value || 0)
|
||||||
|
|
||||||
|
return new Intl.NumberFormat('tr-TR', {
|
||||||
|
style: 'currency',
|
||||||
|
currency: 'TRY',
|
||||||
|
maximumFractionDigits: 0,
|
||||||
|
}).format(amount)
|
||||||
|
}
|
||||||
|
|
||||||
|
const formatDateTime = (value?: string | null) => {
|
||||||
|
if (!value) {
|
||||||
|
return 'Termin yok'
|
||||||
|
}
|
||||||
|
|
||||||
|
return new Intl.DateTimeFormat('tr-TR', {
|
||||||
|
day: '2-digit',
|
||||||
|
month: 'short',
|
||||||
|
hour: '2-digit',
|
||||||
|
minute: '2-digit',
|
||||||
|
}).format(new Date(value))
|
||||||
|
}
|
||||||
|
|
||||||
|
const getActivityTone = (dueAt?: string | null) => {
|
||||||
|
if (!dueAt) {
|
||||||
|
return 'bg-slate-100 text-slate-700 dark:bg-slate-900/70 dark:text-slate-300'
|
||||||
|
}
|
||||||
|
|
||||||
|
const dueDate = new Date(dueAt).getTime()
|
||||||
|
const today = new Date()
|
||||||
|
const startOfTomorrow = new Date(today)
|
||||||
|
startOfTomorrow.setHours(24, 0, 0, 0)
|
||||||
|
|
||||||
|
if (dueDate < Date.now()) {
|
||||||
|
return 'bg-rose-100 text-rose-700 dark:bg-rose-950/50 dark:text-rose-300'
|
||||||
|
}
|
||||||
|
|
||||||
|
if (dueDate < startOfTomorrow.getTime()) {
|
||||||
|
return 'bg-amber-100 text-amber-700 dark:bg-amber-950/50 dark:text-amber-300'
|
||||||
|
}
|
||||||
|
|
||||||
|
return 'bg-emerald-100 text-emerald-700 dark:bg-emerald-950/40 dark:text-emerald-300'
|
||||||
|
}
|
||||||
|
|
||||||
|
const SalesHub = () => {
|
||||||
|
const { currentUser } = useAppSelector((state) => state.auth)
|
||||||
|
const [hubData, setHubData] = useState<SalesHubResponse>(emptyHubData)
|
||||||
|
const [loading, setLoading] = useState(true)
|
||||||
|
const [loadingError, setLoadingError] = useState('')
|
||||||
|
const [form, setForm] = useState<FollowUpForm>(defaultForm)
|
||||||
|
const [formError, setFormError] = useState('')
|
||||||
|
const [successState, setSuccessState] = useState<{ id: string; subject: string } | null>(null)
|
||||||
|
const [isSubmitting, setIsSubmitting] = useState(false)
|
||||||
|
|
||||||
|
const loadHub = async () => {
|
||||||
|
try {
|
||||||
|
setLoading(true)
|
||||||
|
setLoadingError('')
|
||||||
|
const { data } = await axios.get<SalesHubResponse>('/sales-hub')
|
||||||
|
setHubData(data)
|
||||||
|
} catch (error: any) {
|
||||||
|
setLoadingError(
|
||||||
|
error?.response?.data?.message || 'Satış merkezi yüklenemedi. Lütfen tekrar deneyin.',
|
||||||
|
)
|
||||||
|
} finally {
|
||||||
|
setLoading(false)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (currentUser?.id) {
|
||||||
|
loadHub()
|
||||||
|
}
|
||||||
|
}, [currentUser?.id])
|
||||||
|
|
||||||
|
const relationOptions = useMemo(() => {
|
||||||
|
return hubData.quickOptions[`${form.relationType}s` as keyof SalesHubResponse['quickOptions']] || []
|
||||||
|
}, [form.relationType, hubData.quickOptions])
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (!relationOptions.length) {
|
||||||
|
setForm((currentForm) => ({
|
||||||
|
...currentForm,
|
||||||
|
relationId: '',
|
||||||
|
}))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!relationOptions.find((option) => option.id === form.relationId)) {
|
||||||
|
setForm((currentForm) => ({
|
||||||
|
...currentForm,
|
||||||
|
relationId: relationOptions[0]?.id || '',
|
||||||
|
}))
|
||||||
|
}
|
||||||
|
}, [relationOptions, form.relationId])
|
||||||
|
|
||||||
|
const handleFormChange =
|
||||||
|
(field: keyof FollowUpForm) =>
|
||||||
|
(event: React.ChangeEvent<HTMLInputElement | HTMLTextAreaElement | HTMLSelectElement>) => {
|
||||||
|
const value = event.target.value
|
||||||
|
setFormError('')
|
||||||
|
setSuccessState(null)
|
||||||
|
setForm((currentForm) => ({
|
||||||
|
...currentForm,
|
||||||
|
[field]: value,
|
||||||
|
...(field === 'relationType' ? { relationId: '' } : {}),
|
||||||
|
}))
|
||||||
|
}
|
||||||
|
|
||||||
|
const handleSubmit = async (event: React.FormEvent<HTMLFormElement>) => {
|
||||||
|
event.preventDefault()
|
||||||
|
setFormError('')
|
||||||
|
setSuccessState(null)
|
||||||
|
|
||||||
|
if (!form.subject.trim()) {
|
||||||
|
setFormError('Takibin ne için açıldığını açıklayan kısa bir konu yazın.')
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!form.dueAt) {
|
||||||
|
setFormError('Takip için bir tarih ve saat seçin.')
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!form.relationId) {
|
||||||
|
setFormError(`Takibin bağlanacağı bir ${relationLabels[form.relationType]} seçin.`)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
setIsSubmitting(true)
|
||||||
|
const { data } = await axios.post('/sales-hub/follow-ups', {
|
||||||
|
data: {
|
||||||
|
subject: form.subject.trim(),
|
||||||
|
activity_type: form.activityType,
|
||||||
|
due_at: new Date(form.dueAt).toISOString(),
|
||||||
|
relationType: form.relationType,
|
||||||
|
relationId: form.relationId,
|
||||||
|
details: form.details.trim(),
|
||||||
|
},
|
||||||
|
})
|
||||||
|
|
||||||
|
setSuccessState(data)
|
||||||
|
setForm((currentForm) => ({
|
||||||
|
...defaultForm,
|
||||||
|
relationType: currentForm.relationType,
|
||||||
|
}))
|
||||||
|
await loadHub()
|
||||||
|
} catch (error: any) {
|
||||||
|
setFormError(
|
||||||
|
error?.response?.data?.message || 'Takip planlanamadı. Lütfen formu kontrol edip tekrar deneyin.',
|
||||||
|
)
|
||||||
|
} finally {
|
||||||
|
setIsSubmitting(false)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<Head>
|
||||||
|
<title>{getPageTitle('Satış Merkezi')}</title>
|
||||||
|
</Head>
|
||||||
|
<SectionMain>
|
||||||
|
<SectionTitleLineWithButton icon={mdiChartTimelineVariant} title='Satış Merkezi' main>
|
||||||
|
{''}
|
||||||
|
</SectionTitleLineWithButton>
|
||||||
|
|
||||||
|
<div className='grid gap-6'>
|
||||||
|
<CardBox className='overflow-hidden border-slate-900 bg-gradient-to-br from-[#0b1020] via-[#13213d] to-[#1d4ed8] text-white shadow-2xl shadow-slate-950/20'>
|
||||||
|
<div className='grid gap-8 lg:grid-cols-[1.4fr_0.9fr]'>
|
||||||
|
<div className='space-y-5'>
|
||||||
|
<div className='inline-flex items-center rounded-full border border-white/20 bg-white/10 px-3 py-1 text-xs font-semibold uppercase tracking-[0.2em] text-cyan-100'>
|
||||||
|
DakikTabela · CRM operasyon odağı
|
||||||
|
</div>
|
||||||
|
<div className='space-y-3'>
|
||||||
|
<h2 className='text-3xl font-semibold tracking-tight md:text-4xl'>
|
||||||
|
Pipeline sorumluluğunu görün, sıradaki işi kaçırmayın, takip disiplinini koruyun.
|
||||||
|
</h2>
|
||||||
|
<p className='max-w-2xl text-sm leading-6 text-slate-200 md:text-base'>
|
||||||
|
Bu ekran günlük satış ritmini tek yerde toplar: size ait fırsatları inceleyin, geciken işleri
|
||||||
|
görün, yeni takip planlayın ve doğrudan ilgili kayda geçin.
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
<div className='grid gap-3 sm:grid-cols-2 xl:grid-cols-4'>
|
||||||
|
{quickActionLinks.map((action) => (
|
||||||
|
<Link
|
||||||
|
key={action.href}
|
||||||
|
href={action.href}
|
||||||
|
className='group rounded-2xl border border-white/10 bg-white/10 p-4 transition hover:border-cyan-300/50 hover:bg-white/15'
|
||||||
|
>
|
||||||
|
<div className='flex items-center justify-between gap-3'>
|
||||||
|
<span className='font-semibold text-white'>{action.label}</span>
|
||||||
|
<span className='rounded-full bg-white/10 p-2 transition group-hover:bg-cyan-400/20'>
|
||||||
|
<svg viewBox='0 0 24 24' className='h-4 w-4 fill-current'>
|
||||||
|
<path d={mdiArrowRight} />
|
||||||
|
</svg>
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
<p className='mt-3 text-sm leading-6 text-slate-200'>{action.description}</p>
|
||||||
|
</Link>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className='grid gap-3 rounded-3xl border border-white/10 bg-slate-950/25 p-5 backdrop-blur-sm'>
|
||||||
|
<p className='text-sm font-semibold text-cyan-100'>Bugünün ritmi</p>
|
||||||
|
<div className='space-y-4'>
|
||||||
|
<div>
|
||||||
|
<p className='text-4xl font-semibold'>{hubData.stats.completedThisWeek}</p>
|
||||||
|
<p className='mt-1 text-sm text-slate-200'>Bu hafta tamamlanan takip</p>
|
||||||
|
</div>
|
||||||
|
<div className='grid gap-3 sm:grid-cols-2 lg:grid-cols-1'>
|
||||||
|
<div className='rounded-2xl border border-white/10 bg-white/10 p-4'>
|
||||||
|
<p className='text-xs uppercase tracking-[0.2em] text-slate-300'>Önerilen aksiyon</p>
|
||||||
|
<p className='mt-2 font-medium'>Her lead ve fırsat için tarihli bir sonraki adım belirleyin.</p>
|
||||||
|
</div>
|
||||||
|
<div className='rounded-2xl border border-white/10 bg-white/10 p-4'>
|
||||||
|
<p className='text-xs uppercase tracking-[0.2em] text-slate-300'>İyi uygulama</p>
|
||||||
|
<p className='mt-2 font-medium'>Her takipte tek sahip, net konu ve kesin termin olsun.</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</CardBox>
|
||||||
|
|
||||||
|
{loadingError && (
|
||||||
|
<NotificationBar color='danger'>
|
||||||
|
<span>{loadingError}</span>
|
||||||
|
</NotificationBar>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{successState && (
|
||||||
|
<NotificationBar
|
||||||
|
color='success'
|
||||||
|
button={
|
||||||
|
<BaseButton
|
||||||
|
color='success'
|
||||||
|
href={`/activities/${successState.id}`}
|
||||||
|
label='Aktiviteyi aç'
|
||||||
|
icon={mdiOpenInNew}
|
||||||
|
small
|
||||||
|
/>
|
||||||
|
}
|
||||||
|
>
|
||||||
|
<span>
|
||||||
|
Takip planlandı: <strong>{successState.subject}</strong>
|
||||||
|
</span>
|
||||||
|
</NotificationBar>
|
||||||
|
)}
|
||||||
|
|
||||||
|
<div className='grid gap-6 xl:grid-cols-[1.45fr_0.95fr]'>
|
||||||
|
<div className='space-y-6'>
|
||||||
|
<div className='grid gap-4 md:grid-cols-2 xl:grid-cols-4'>
|
||||||
|
{statCards.map((card) => (
|
||||||
|
<CardBox key={card.key} className='border-slate-200/70 bg-white/90 shadow-sm dark:border-dark-700'>
|
||||||
|
<div className='flex items-start justify-between gap-4'>
|
||||||
|
<div>
|
||||||
|
<p className='text-sm font-semibold text-slate-500 dark:text-slate-300'>{card.label}</p>
|
||||||
|
<p className='mt-3 text-4xl font-semibold text-slate-900 dark:text-white'>
|
||||||
|
{hubData.stats[card.key]}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
<div className='rounded-2xl bg-slate-100 p-3 text-slate-700 dark:bg-slate-800 dark:text-slate-100'>
|
||||||
|
<svg viewBox='0 0 24 24' className='h-5 w-5 fill-current'>
|
||||||
|
<path d={card.icon} />
|
||||||
|
</svg>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<p className='mt-4 text-sm leading-6 text-slate-500 dark:text-slate-300'>{card.helper}</p>
|
||||||
|
</CardBox>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<CardBox className='border-slate-200/70 bg-white/95 shadow-sm dark:border-dark-700'>
|
||||||
|
<div className='flex flex-col gap-3 border-b border-slate-200 pb-4 dark:border-dark-700 md:flex-row md:items-end md:justify-between'>
|
||||||
|
<div>
|
||||||
|
<p className='text-xs font-semibold uppercase tracking-[0.18em] text-cyan-600 dark:text-cyan-300'>
|
||||||
|
Öncelikli işler
|
||||||
|
</p>
|
||||||
|
<h3 className='mt-1 text-2xl font-semibold text-slate-900 dark:text-white'>Takip kuyruğum</h3>
|
||||||
|
<p className='mt-2 text-sm leading-6 text-slate-500 dark:text-slate-300'>
|
||||||
|
Size atanan sıradaki aktiviteler burada listelenir. Aktiviteyi veya bağlı kaydı tek tıkla açabilirsiniz.
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
<BaseButton color='info' href='/activities/activities-list' label='Tüm aktiviteler' small />
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{loading ? (
|
||||||
|
<div className='grid gap-3 py-6'>
|
||||||
|
{Array.from({ length: 4 }).map((_, index) => (
|
||||||
|
<div key={index} className='h-24 animate-pulse rounded-2xl bg-slate-100 dark:bg-slate-800' />
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
) : hubData.followUps.length ? (
|
||||||
|
<div className='grid gap-3 pt-6'>
|
||||||
|
{hubData.followUps.map((item) => {
|
||||||
|
const relatedPath =
|
||||||
|
item.relatedType !== 'activity' && item.relatedId
|
||||||
|
? `${relationPaths[item.relatedType]}/${item.relatedId}`
|
||||||
|
: null
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div
|
||||||
|
key={item.id}
|
||||||
|
className='grid gap-3 rounded-2xl border border-slate-200/80 bg-slate-50/70 p-4 dark:border-dark-700 dark:bg-dark-900/50 lg:grid-cols-[1.2fr_0.9fr_auto] lg:items-center'
|
||||||
|
>
|
||||||
|
<div>
|
||||||
|
<div className='flex flex-wrap items-center gap-2'>
|
||||||
|
<span className={`rounded-full px-2.5 py-1 text-xs font-semibold ${getActivityTone(item.dueAt)}`}>
|
||||||
|
{formatDateTime(item.dueAt)}
|
||||||
|
</span>
|
||||||
|
<span className='rounded-full bg-slate-900 px-2.5 py-1 text-xs font-semibold uppercase tracking-wide text-white dark:bg-slate-100 dark:text-slate-900'>
|
||||||
|
{activityTypeLabels[item.activityType] || item.activityType}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
<h4 className='mt-3 text-lg font-semibold text-slate-900 dark:text-white'>{item.subject}</h4>
|
||||||
|
<p className='mt-1 text-sm text-slate-500 dark:text-slate-300'>Bağlı kayıt: {item.relatedName}</p>
|
||||||
|
</div>
|
||||||
|
<div className='text-sm text-slate-500 dark:text-slate-300'>
|
||||||
|
<p className='font-medium text-slate-900 dark:text-white'>
|
||||||
|
Durum: {statusLabels[item.status] || item.status.replace('_', ' ')}
|
||||||
|
</p>
|
||||||
|
<p className='mt-1'>Termin kaymadan önce sıradaki teması tamamlayın.</p>
|
||||||
|
</div>
|
||||||
|
<div className='flex flex-wrap gap-2 lg:justify-end'>
|
||||||
|
<BaseButton color='info' href={`/activities/${item.id}`} label='Aktivite detayı' small />
|
||||||
|
{relatedPath && (
|
||||||
|
<BaseButton color='white' outline href={relatedPath} label='Kaydı aç' small />
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
})}
|
||||||
|
</div>
|
||||||
|
) : (
|
||||||
|
<div className='rounded-2xl border border-dashed border-slate-300 bg-slate-50/70 px-6 py-8 text-center dark:border-dark-600 dark:bg-dark-900/50'>
|
||||||
|
<p className='text-lg font-semibold text-slate-900 dark:text-white'>Size atanmış bekleyen takip yok.</p>
|
||||||
|
<p className='mt-2 text-sm leading-6 text-slate-500 dark:text-slate-300'>
|
||||||
|
Yeni bir takip planlayabilir veya aktivite listesinden tamamlanan işleri gözden geçirebilirsiniz.
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</CardBox>
|
||||||
|
|
||||||
|
<CardBox className='border-slate-200/70 bg-white/95 shadow-sm dark:border-dark-700'>
|
||||||
|
<div className='flex flex-col gap-3 border-b border-slate-200 pb-4 dark:border-dark-700 md:flex-row md:items-end md:justify-between'>
|
||||||
|
<div>
|
||||||
|
<p className='text-xs font-semibold uppercase tracking-[0.18em] text-cyan-600 dark:text-cyan-300'>
|
||||||
|
Yeni akış
|
||||||
|
</p>
|
||||||
|
<h3 className='mt-1 text-2xl font-semibold text-slate-900 dark:text-white'>Son potansiyel müşteriler</h3>
|
||||||
|
</div>
|
||||||
|
<BaseButton color='info' href='/leads/leads-list' label='Lead listesini aç' small />
|
||||||
|
</div>
|
||||||
|
{loading ? (
|
||||||
|
<div className='grid gap-3 py-6'>
|
||||||
|
{Array.from({ length: 3 }).map((_, index) => (
|
||||||
|
<div key={index} className='h-20 animate-pulse rounded-2xl bg-slate-100 dark:bg-slate-800' />
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
) : hubData.recentLeads.length ? (
|
||||||
|
<div className='grid gap-3 pt-6'>
|
||||||
|
{hubData.recentLeads.map((lead) => (
|
||||||
|
<Link
|
||||||
|
key={lead.id}
|
||||||
|
href={`/leads/${lead.id}`}
|
||||||
|
className='grid gap-3 rounded-2xl border border-slate-200/80 bg-slate-50/70 p-4 transition hover:border-cyan-300 hover:bg-white dark:border-dark-700 dark:bg-dark-900/50 dark:hover:border-cyan-500 lg:grid-cols-[1.1fr_0.9fr_auto] lg:items-center'
|
||||||
|
>
|
||||||
|
<div>
|
||||||
|
<p className='text-lg font-semibold text-slate-900 dark:text-white'>{lead.title}</p>
|
||||||
|
<p className='mt-1 text-sm text-slate-500 dark:text-slate-300'>
|
||||||
|
{lead.companyName || 'Şirket bağlı değil'} · {lead.stageName || 'Aşama seçilmedi'}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
<div className='text-sm text-slate-500 dark:text-slate-300'>
|
||||||
|
<p className='font-medium text-slate-900 dark:text-white'>{formatCurrency(lead.estimatedValue)}</p>
|
||||||
|
<p className='mt-1'>Sonraki temas: {formatDateTime(lead.nextFollowUpAt)}</p>
|
||||||
|
</div>
|
||||||
|
<div className='justify-self-start rounded-full bg-slate-900 px-3 py-1 text-xs font-semibold uppercase tracking-wide text-white dark:bg-slate-100 dark:text-slate-900'>
|
||||||
|
{lead.status}
|
||||||
|
</div>
|
||||||
|
</Link>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
) : (
|
||||||
|
<div className='rounded-2xl border border-dashed border-slate-300 bg-slate-50/70 px-6 py-8 text-center dark:border-dark-600 dark:bg-dark-900/50'>
|
||||||
|
<p className='text-lg font-semibold text-slate-900 dark:text-white'>Henüz aktif potansiyel müşteri yok.</p>
|
||||||
|
<p className='mt-2 text-sm leading-6 text-slate-500 dark:text-slate-300'>
|
||||||
|
İlk lead kaydınızı oluşturduğunuzda bu alanda yeni akış görünmeye başlar.
|
||||||
|
</p>
|
||||||
|
<div className='mt-5 flex justify-center'>
|
||||||
|
<BaseButton color='info' href='/leads/leads-new' label='İlk lead kaydını oluştur' />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</CardBox>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className='space-y-6'>
|
||||||
|
<CardBox className='border-slate-200/70 bg-white/95 shadow-sm dark:border-dark-700'>
|
||||||
|
<div className='border-b border-slate-200 pb-4 dark:border-dark-700'>
|
||||||
|
<p className='text-xs font-semibold uppercase tracking-[0.18em] text-cyan-600 dark:text-cyan-300'>
|
||||||
|
Planlı aksiyon
|
||||||
|
</p>
|
||||||
|
<h3 className='mt-1 text-2xl font-semibold text-slate-900 dark:text-white'>Yeni takip planla</h3>
|
||||||
|
<p className='mt-2 text-sm leading-6 text-slate-500 dark:text-slate-300'>
|
||||||
|
Takibi bir lead, fırsat veya kişiye bağlayın; konu, tarih ve not ile ekipteki sorumluluğu netleştirin.
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<form className='pt-6' onSubmit={handleSubmit}>
|
||||||
|
<FormField label='Konu' help='Takibin neden açıldığını kısa ve net yazın.'>
|
||||||
|
<input value={form.subject} onChange={handleFormChange('subject')} placeholder='Örn. teklif sonrası arama' />
|
||||||
|
</FormField>
|
||||||
|
|
||||||
|
<div className='grid gap-4 md:grid-cols-2'>
|
||||||
|
<FormField label='Takip tipi' help='Bu kaydın nasıl ilerletileceğini seçin.'>
|
||||||
|
<select value={form.activityType} onChange={handleFormChange('activityType')}>
|
||||||
|
{Object.entries(activityTypeLabels).map(([value, label]) => (
|
||||||
|
<option key={value} value={value}>
|
||||||
|
{label}
|
||||||
|
</option>
|
||||||
|
))}
|
||||||
|
</select>
|
||||||
|
</FormField>
|
||||||
|
|
||||||
|
<FormField label='Termin' help='Takibin tamamlanması gereken tarih ve saat.'>
|
||||||
|
<input type='datetime-local' value={form.dueAt} onChange={handleFormChange('dueAt')} />
|
||||||
|
</FormField>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<FormField label='Bağlı kayıt tipi' help='Takibin bağlanacağı kayıt türünü seçin.'>
|
||||||
|
<select value={form.relationType} onChange={handleFormChange('relationType')}>
|
||||||
|
<option value='lead'>Potansiyel müşteri</option>
|
||||||
|
<option value='deal'>Fırsat</option>
|
||||||
|
<option value='contact'>Kişi</option>
|
||||||
|
</select>
|
||||||
|
</FormField>
|
||||||
|
|
||||||
|
<FormField
|
||||||
|
label='Bağlı kayıt'
|
||||||
|
help={`Sadece size açık olan ${relationLabels[form.relationType]} kayıtları burada görünür.`}
|
||||||
|
>
|
||||||
|
<select value={form.relationId} onChange={handleFormChange('relationId')}>
|
||||||
|
<option value=''>Kayıt seçin</option>
|
||||||
|
{relationOptions.map((option) => (
|
||||||
|
<option key={option.id} value={option.id}>
|
||||||
|
{option.label}
|
||||||
|
</option>
|
||||||
|
))}
|
||||||
|
</select>
|
||||||
|
</FormField>
|
||||||
|
|
||||||
|
{!relationOptions.length && (
|
||||||
|
<div className='mb-6 rounded-2xl border border-dashed border-slate-300 bg-slate-50 px-4 py-4 text-sm text-slate-600 dark:border-dark-600 dark:bg-dark-900/50 dark:text-slate-300'>
|
||||||
|
Önce bir {relationLabels[form.relationType]} oluşturun veya size atanmış kayıtları güncelleyin; ardından bu alanda seçim yapabilirsiniz.
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
<FormField label='Notlar' help='Bir sonraki temas için gerekli bağlam, risk veya beklentiyi yazın.'>
|
||||||
|
<textarea
|
||||||
|
value={form.details}
|
||||||
|
onChange={handleFormChange('details')}
|
||||||
|
placeholder='Örn. fiyat itirazı var, revize teklif gönderildikten sonra aranacak.'
|
||||||
|
/>
|
||||||
|
</FormField>
|
||||||
|
|
||||||
|
{formError && (
|
||||||
|
<NotificationBar color='danger'>
|
||||||
|
<span>{formError}</span>
|
||||||
|
</NotificationBar>
|
||||||
|
)}
|
||||||
|
|
||||||
|
<div className='flex flex-wrap gap-3'>
|
||||||
|
<BaseButton
|
||||||
|
type='submit'
|
||||||
|
color='info'
|
||||||
|
label={isSubmitting ? 'Planlanıyor…' : 'Takibi planla'}
|
||||||
|
icon={mdiCheckCircleOutline}
|
||||||
|
disabled={isSubmitting}
|
||||||
|
/>
|
||||||
|
<BaseButton color='white' outline label='Aktivite listesine git' href='/activities/activities-list' />
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
</CardBox>
|
||||||
|
|
||||||
|
<CardBox className='border-slate-200/70 bg-white/95 shadow-sm dark:border-dark-700'>
|
||||||
|
<div className='border-b border-slate-200 pb-4 dark:border-dark-700'>
|
||||||
|
<p className='text-xs font-semibold uppercase tracking-[0.18em] text-cyan-600 dark:text-cyan-300'>Özet görünüm</p>
|
||||||
|
<h3 className='mt-1 text-2xl font-semibold text-slate-900 dark:text-white'>Aşamaya göre fırsatlar</h3>
|
||||||
|
<p className='mt-2 text-sm leading-6 text-slate-500 dark:text-slate-300'>
|
||||||
|
Aktif pipeline'ı aşama ve toplam değer bazında görün; satış ritmini günlük ekranınızdan takip edin.
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
{loading ? (
|
||||||
|
<div className='grid gap-3 py-6'>
|
||||||
|
{Array.from({ length: 4 }).map((_, index) => (
|
||||||
|
<div key={index} className='h-16 animate-pulse rounded-2xl bg-slate-100 dark:bg-slate-800' />
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
) : hubData.stageSummary.length ? (
|
||||||
|
<div className='grid gap-3 pt-6'>
|
||||||
|
{hubData.stageSummary.map((stage) => (
|
||||||
|
<div
|
||||||
|
key={stage.id}
|
||||||
|
className='rounded-2xl border border-slate-200/80 bg-slate-50/70 p-4 dark:border-dark-700 dark:bg-dark-900/50'
|
||||||
|
>
|
||||||
|
<div className='flex items-center justify-between gap-4'>
|
||||||
|
<div>
|
||||||
|
<p className='font-semibold text-slate-900 dark:text-white'>{stage.name}</p>
|
||||||
|
<p className='mt-1 text-sm text-slate-500 dark:text-slate-300'>
|
||||||
|
{stage.dealCount} aktif {stage.dealCount === 1 ? 'fırsat' : 'fırsat'}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
<div className='text-right'>
|
||||||
|
<p className='font-semibold text-slate-900 dark:text-white'>{formatCurrency(stage.amount)}</p>
|
||||||
|
<p className='mt-1 text-xs uppercase tracking-wide text-slate-500 dark:text-slate-400'>
|
||||||
|
toplam pipeline
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
) : (
|
||||||
|
<div className='rounded-2xl border border-dashed border-slate-300 bg-slate-50/70 px-6 py-8 text-center dark:border-dark-600 dark:bg-dark-900/50'>
|
||||||
|
<p className='text-lg font-semibold text-slate-900 dark:text-white'>Henüz açık fırsat yok.</p>
|
||||||
|
<p className='mt-2 text-sm leading-6 text-slate-500 dark:text-slate-300'>
|
||||||
|
Fırsatlar aktif hale geldiğinde bu alan aşama dağılımını ve toplam değeri gösterecek.
|
||||||
|
</p>
|
||||||
|
<div className='mt-5 flex justify-center'>
|
||||||
|
<BaseButton color='info' href='/deals/deals-new' label='İlk fırsatı oluştur' />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</CardBox>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</SectionMain>
|
||||||
|
</>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
SalesHub.getLayout = function getLayout(page: ReactElement) {
|
||||||
|
return <LayoutAuthenticated>{page}</LayoutAuthenticated>
|
||||||
|
}
|
||||||
|
|
||||||
|
export default SalesHub
|
||||||
@ -1,9 +1,7 @@
|
|||||||
import React, { ReactElement, useEffect, useState } from 'react';
|
import React, { ReactElement, useEffect, useState } from 'react';
|
||||||
import Head from 'next/head';
|
import Head from 'next/head';
|
||||||
import 'react-datepicker/dist/react-datepicker.css';
|
import 'react-datepicker/dist/react-datepicker.css';
|
||||||
import { useAppDispatch } from '../stores/hooks';
|
import { useAppDispatch, useAppSelector } from '../stores/hooks';
|
||||||
|
|
||||||
import { useAppSelector } from '../stores/hooks';
|
|
||||||
|
|
||||||
import { useRouter } from 'next/router';
|
import { useRouter } from 'next/router';
|
||||||
import LayoutAuthenticated from '../layouts/Authenticated';
|
import LayoutAuthenticated from '../layouts/Authenticated';
|
||||||
@ -57,12 +55,12 @@ const SearchView = () => {
|
|||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<Head>
|
<Head>
|
||||||
<title>Search Result</title>
|
<title>Arama Sonuçları</title>
|
||||||
</Head>
|
</Head>
|
||||||
<SectionMain>
|
<SectionMain>
|
||||||
<SectionTitleLineWithButton
|
<SectionTitleLineWithButton
|
||||||
icon={mdiChartTimelineVariant}
|
icon={mdiChartTimelineVariant}
|
||||||
title={'Search Result'}
|
title={'Arama Sonuçları'}
|
||||||
main
|
main
|
||||||
>
|
>
|
||||||
{''}
|
{''}
|
||||||
@ -75,7 +73,7 @@ const SearchView = () => {
|
|||||||
<BaseDivider />
|
<BaseDivider />
|
||||||
<BaseButton
|
<BaseButton
|
||||||
color='info'
|
color='info'
|
||||||
label='Back'
|
label='Geri dön'
|
||||||
onClick={() => router.push('/dashboard')}
|
onClick={() => router.push('/dashboard')}
|
||||||
/>
|
/>
|
||||||
</CardBox>
|
</CardBox>
|
||||||
|
|||||||
@ -44,11 +44,11 @@ export default function Verify() {
|
|||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<Head>
|
<Head>
|
||||||
<title>{getPageTitle('Verify Email')}</title>
|
<title>{getPageTitle('E-posta Doğrulama')}</title>
|
||||||
</Head>
|
</Head>
|
||||||
<SectionFullScreen bg='violet'>
|
<SectionFullScreen bg='violet'>
|
||||||
<CardBox className='w-11/12 md:w-7/12 lg:w-6/12 xl:w-4/12'>
|
<CardBox className='w-11/12 md:w-7/12 lg:w-6/12 xl:w-4/12'>
|
||||||
<p>{loading ? 'Loading...' : ''}</p>
|
<p>{loading ? 'Doğrulanıyor...' : ''}</p>
|
||||||
</CardBox>
|
</CardBox>
|
||||||
</SectionFullScreen>
|
</SectionFullScreen>
|
||||||
|
|
||||||
|
|||||||
@ -41,6 +41,9 @@ module.exports = {
|
|||||||
'fade-out': 'fade-out 250ms ease-in-out',
|
'fade-out': 'fade-out 250ms ease-in-out',
|
||||||
'fade-in': 'fade-in 250ms ease-in-out'
|
'fade-in': 'fade-in 250ms ease-in-out'
|
||||||
},
|
},
|
||||||
|
fontFamily: {
|
||||||
|
sans: ['Poppins', 'ui-sans-serif', 'system-ui', 'sans-serif'],
|
||||||
|
},
|
||||||
colors: {
|
colors: {
|
||||||
dark: {
|
dark: {
|
||||||
900: '#131618',
|
900: '#131618',
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user