This commit is contained in:
Flatlogic Bot 2026-04-01 00:45:15 +00:00
parent d7b7b23e2a
commit e426a53f7e
32 changed files with 2300 additions and 1601 deletions

File diff suppressed because one or more lines are too long

After

Width:  |  Height:  |  Size: 499 KiB

View File

@ -20,6 +20,7 @@ const pexelsRoutes = require('./routes/pexels');
const organizationForAuthRoutes = require('./routes/organizationLogin');
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/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);

View 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;

File diff suppressed because one or more lines are too long

After

Width:  |  Height:  |  Size: 499 KiB

View 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": "Dont 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"
}
}
}

View File

@ -29,16 +29,30 @@ const AsideMenuItem = ({ item, isDropdownList = false }: Props) => {
const { asPath, isReady } = useRouter()
useEffect(() => {
if (item.href && isReady) {
const linkPathName = new URL(item.href, location.href).pathname + '/';
const activePathname = new URL(asPath, location.href).pathname
const activeView = activePathname.split('/')[1];
const linkPathNameView = linkPathName.split('/')[1];
setIsLinkActive(linkPathNameView === activeView);
if (!isReady) {
return
}
}, [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 = (
<>

View File

@ -1,15 +1,12 @@
import React from 'react'
import { mdiLogout, mdiClose } from '@mdi/js'
import { mdiClose } from '@mdi/js'
import BaseIcon from './BaseIcon'
import AsideMenuList from './AsideMenuList'
import { MenuAsideItem } from '../interfaces'
import { useAppSelector } from '../stores/hooks'
import Link from 'next/link';
import { useAppDispatch } from '../stores/hooks';
import { createAsyncThunk } from '@reduxjs/toolkit';
import axios from 'axios';
import { useAppDispatch, useAppSelector } from '../stores/hooks'
import { createAsyncThunk } from '@reduxjs/toolkit'
import axios from 'axios'
import Logo from './Logo'
type Props = {
menu: MenuAsideItem[]
@ -18,7 +15,7 @@ type 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 asideBrandStyle = useAppSelector((state) => state.style.asideBrandStyle)
const asideScrollbarsStyle = useAppSelector((state) => state.style.asideScrollbarsStyle)
@ -29,55 +26,49 @@ export default function AsideMenuLayer({ menu, className = '', ...props }: Props
props.onAsideLgCloseClick()
}
const dispatch = useAppDispatch();
const { currentUser } = useAppSelector((state) => state.auth);
const organizationsId = currentUser?.organizations?.id;
const [organizations, setOrganizations] = React.useState(null);
const dispatch = useAppDispatch()
const { currentUser } = useAppSelector((state) => state.auth)
const organizationsId = currentUser?.organizations?.id
const [organizations, setOrganizations] = React.useState<any[]>([])
const fetchOrganizations = createAsyncThunk('/org-for-auth', async () => {
try {
const response = await axios.get('/org-for-auth');
setOrganizations(response.data);
return response.data;
} catch (error) {
console.error(error.response);
throw error;
const response = await axios.get('/org-for-auth')
setOrganizations(response.data)
return response.data
} catch (error: any) {
console.error(error?.response || error)
throw error
}
});
})
React.useEffect(() => {
dispatch(fetchOrganizations());
}, [dispatch]);
dispatch(fetchOrganizations())
}, [dispatch])
let organizationName = organizations?.find(item => item.id === organizationsId)?.name;
if(organizationName?.length > 25){
organizationName = organizationName?.substring(0, 25) + '...';
let organizationName = organizations?.find((item) => item.id === organizationsId)?.name
if (organizationName?.length > 28) {
organizationName = organizationName.substring(0, 28) + '...'
}
return (
<aside
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`}
>
<div
className={`flex-1 flex flex-col overflow-hidden dark:bg-dark-900 ${asideStyle} ${corners}`}
>
<div
className={`flex flex-row h-14 items-center justify-between ${asideBrandStyle}`}
>
<div className="text-center flex-1 lg:text-left lg:pl-6 xl:text-center xl:pl-0">
<b className="font-black">DakikTabela Ops Suite</b>
{organizationName && <p>{organizationName}</p>}
<div 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'>
<Logo className='h-10 w-auto shrink-0 rounded-xl bg-white p-1.5 shadow-sm' />
<div className='min-w-0'>
<p className='truncate text-sm font-semibold text-slate-900 dark:text-white'>DakikTabela</p>
<p className='truncate text-xs text-slate-500 dark:text-slate-300'>Operasyon Platformu</p>
{organizationName && (
<p className='truncate text-[11px] text-slate-400 dark:text-slate-400'>{organizationName}</p>
)}
</div>
</div>
<button
className="hidden lg:inline-block xl:hidden p-3"
onClick={handleAsideLgCloseClick}
>
<button className='hidden lg:inline-block xl:hidden p-3' onClick={handleAsideLgCloseClick}>
<BaseIcon path={mdiClose} />
</button>
</div>

View File

@ -10,25 +10,16 @@ export default function FooterBar({ children }: Props) {
const year = new Date().getFullYear()
return (
<footer className={`py-2 px-6 ${containerMaxW}`}>
<div className="block md:flex items-center justify-between">
<div className="text-center md:text-left mb-6 md:mb-0">
<b>
&copy;{year},{` `}
<a href="https://flatlogic.com/" rel="noreferrer" target="_blank">
Flatlogic
</a>
.
</b>
{` `}
{children}
<footer className={`py-3 px-6 ${containerMaxW}`}>
<div className="block md:flex items-center justify-between gap-4">
<div className="text-center md:text-left mb-4 md:mb-0 text-sm text-slate-600 dark:text-slate-300">
<b className='text-slate-900 dark:text-white'>&copy; {year} DakikTabela.</b>{' '}
{children || 'Satış, üretim ve etkinlik operasyonları tek platformda.'}
</div>
<div className="flex item-center md:py-2 gap-4">
<a href="https://flatlogic.com/" rel="noreferrer" target="_blank">
<Logo className="w-auto h-8 md:h-6 mx-auto" />
</a>
</div>
<div className="flex items-center justify-center md:justify-end gap-3">
<Logo className="w-auto h-10 md:h-8" />
</div>
</div>
</footer>
)

View File

@ -1,96 +1,102 @@
import React, { useEffect, useState } from 'react';
import Select, { components, SingleValueProps, OptionProps } from 'react-select';
import React, { useEffect, useState } from 'react'
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[] = [
{ value: 'en', label: '🇬🇧 EN' },
{ value: 'fr', label: '🇫🇷 FR' },
{ value: 'es', label: '🇪🇸 ES' },
{ value: 'de', label: '🇩🇪 DE' },
];
{ value: 'tr', label: '🇹🇷 TR' },
{ value: 'en', label: '🇬🇧 EN' },
{ value: 'fr', label: '🇫🇷 FR' },
{ value: 'es', label: '🇪🇸 ES' },
{ value: 'de', label: '🇩🇪 DE' },
]
const Option = (props: OptionProps<LanguageOption, false>) => (
<components.Option {...props}>
<span className='flex items-center gap-1'>{props.data.label}</span>
</components.Option>
);
<components.Option {...props}>
<span className='flex items-center gap-1'>{props.data.label}</span>
</components.Option>
)
const SingleVal = (props: SingleValueProps<LanguageOption, false>) => (
<components.SingleValue {...props}>
<span className='flex items-center gap-1'>{props.data.label}</span>
</components.SingleValue>
);
<components.SingleValue {...props}>
<span className='flex items-center gap-1'>{props.data.label}</span>
</components.SingleValue>
)
const LanguageSwitcher: React.FC = () => {
const [mounted, setMounted] = useState(false);
const [selected, setSelected] = useState<LanguageOption>(LANGS[0]);
const { i18n } = useTranslation()
const [mounted, setMounted] = useState(false)
const [selected, setSelected] = useState<LanguageOption>(LANGS[0])
useEffect(() => {
setMounted(true);
}, []);
useEffect(() => {
const active = LANGS.find((item) => item.value === i18n.language) || LANGS[0]
setSelected(active)
setMounted(true)
}, [i18n.language])
const handleChange = (opt: LanguageOption | null) => {
if (!opt) return;
setSelected(opt);
};
const handleChange = (opt: LanguageOption | null) => {
if (!opt) return
setSelected(opt)
i18n.changeLanguage(opt.value)
}
if (!mounted) return null;
if (!mounted) return null
return (
<div style={{ width: 88 }}>
<Select
value={selected}
options={LANGS}
onChange={handleChange}
isSearchable={false}
menuPlacement='top'
components={{
Option,
SingleValue: SingleVal,
IndicatorSeparator: () => null,
}}
styles={{
control: (base) => ({
...base,
minHeight: 28,
height: 28,
paddingTop: 0,
paddingBottom: 0,
borderColor: '#d1d5db',
cursor: 'pointer',
}),
valueContainer: (base) => ({
...base,
paddingTop: 0,
paddingBottom: 0,
paddingLeft: 6,
}),
indicatorsContainer: (base) => ({
...base,
height: 24,
}),
dropdownIndicator: (base) => ({
...base,
padding: 2,
}),
option: (base, state) => ({
...base,
paddingTop: 4,
paddingBottom: 4,
height: 26,
fontSize: '0.875rem',
backgroundColor: state.isFocused ? '#f3f4f6' : 'white',
color: '#111827',
}),
menu: (base) => ({
...base,
zIndex: 9999,
}),
}}
/>
</div>
);
};
return (
<div style={{ width: 88 }}>
<Select
value={selected}
options={LANGS}
onChange={handleChange}
isSearchable={false}
menuPlacement='top'
components={{
Option,
SingleValue: SingleVal,
IndicatorSeparator: () => null,
}}
styles={{
control: (base) => ({
...base,
minHeight: 28,
height: 28,
paddingTop: 0,
paddingBottom: 0,
borderColor: '#d1d5db',
cursor: 'pointer',
}),
valueContainer: (base) => ({
...base,
paddingTop: 0,
paddingBottom: 0,
paddingLeft: 6,
}),
indicatorsContainer: (base) => ({
...base,
height: 24,
}),
dropdownIndicator: (base) => ({
...base,
padding: 2,
}),
option: (base, state) => ({
...base,
paddingTop: 4,
paddingBottom: 4,
height: 26,
fontSize: '0.875rem',
backgroundColor: state.isFocused ? '#f3f4f6' : 'white',
color: '#111827',
}),
menu: (base) => ({
...base,
zIndex: 9999,
}),
}}
/>
</div>
)
}
export default LanguageSwitcher;
export default LanguageSwitcher

View File

@ -7,9 +7,9 @@ type Props = {
export default function Logo({ className = '' }: Props) {
return (
<img
src={"https://flatlogic.com/logo.svg"}
src='/brand/dakiktabela-logo.svg'
className={className}
alt={'Flatlogic logo'}>
</img>
alt='DakikTabela logosu'
/>
)
}

View File

@ -1,6 +1,5 @@
import React, {useEffect, useRef} from 'react'
import React, { useEffect, useRef, useState } from 'react'
import Link from 'next/link'
import { useState } from 'react'
import { mdiChevronUp, mdiChevronDown } from '@mdi/js'
import BaseDivider from './BaseDivider'
import BaseIcon from './BaseIcon'
@ -67,8 +66,10 @@ export default function NavBarItem({ item }: Props) {
const getItemId = (label) => {
switch (label) {
case 'Light/Dark':
case 'Açık/Koyu Tema':
return 'themeToggle';
case 'Log out':
case ıkış yap':
return 'logout';
default:
return undefined;

View File

@ -49,16 +49,16 @@ export default function PasswordSetOrReset() {
return (
<>
<Head>
{isInvitation && <title>{getPageTitle('Set Password')}</title>}
{!isInvitation && <title>{getPageTitle('Reset Password')}</title>}
{isInvitation && <title>{getPageTitle('Şifre Belirle')}</title>}
{!isInvitation && <title>{getPageTitle('Şifreyi Sıfırla')}</title>}
</Head>
<SectionFullScreen bg='violet'>
<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'>
{isInvitation && <p className='text-xl mb-2'>Set Password</p>}
{!isInvitation && <p className='text-xl mb-2'>Reset Password</p>}
<p className='text-base mb-4'>Enter your new password</p>
{isInvitation && <p className='text-xl mb-2'>Şifre Belirle</p>}
{!isInvitation && <p className='text-xl mb-2'>Şifreyi Sıfırla</p>}
<p className='text-base mb-4'>Yeni şifrenizi girin</p>
<Formik
initialValues={{
@ -74,7 +74,7 @@ export default function PasswordSetOrReset() {
<Field
type='password'
name='password'
placeholder='Password'
placeholder='Şifre'
/>
</FormField>
<FormField
@ -82,7 +82,7 @@ export default function PasswordSetOrReset() {
<Field
type='password'
name='confirm'
placeholder='Confirm Password'
placeholder='Şifre Tekrarı'
/>
</FormField>
@ -93,10 +93,10 @@ export default function PasswordSetOrReset() {
disabled={loading}
label={
loading
? 'Loading...'
? 'Yükleniyor...'
: isInvitation
? 'Set Password'
: 'Reset Password'
? 'Şifre Belirle'
: 'Şifreyi Sıfırla'
}
color='info'
/>

View File

@ -8,7 +8,7 @@ const SearchResults = ({ searchResults, searchQuery }) => {
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) => (
<>
<p className={'block font-bold mb-2'}>{humanize(tableName)}</p>
@ -68,13 +68,13 @@ const SearchResults = ({ searchResults, searchQuery }) => {
</table>
</div>
{!Object.keys(searchResults).length && (
<div className={'text-center py-4'}>No data</div>
<div className={'text-center py-4'}>Veri bulunamadı</div>
)}
</CardBox>
</>
))}
{!Object.keys(searchResults).length && (
<div className={'py-4'}>No matches</div>
<div className={'py-4'}>Eşleşme bulunamadı</div>
)}
</>
);

View File

@ -75,7 +75,7 @@ export const SmartWidget = ({ widget, userId, admin, roleId }) => {
)
) : (
<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>

View File

@ -67,7 +67,7 @@ export const WidgetCreator = ({
const errorMessage =
responcePayload.data?.error?.message || error?.message;
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>
<FormField
label='Create Chart or Widget'
label='Grafik veya bileşen oluştur'
help={
isFetchingQuery ?
'Loading...' :
'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"'
'Yükleniyor...' :
'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} />
@ -110,14 +110,14 @@ export const WidgetCreator = ({
>
{({ submitForm }) => (
<CardBoxModal
title='Widget Creator Settings'
title='Bileşen oluşturucu ayarları'
buttonColor='info'
buttonLabel='Done'
buttonLabel='Tamam'
isActive={isModalOpen}
onConfirm={submitForm}
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>
<FormField>

View File

@ -8,7 +8,7 @@ export const localStorageStyleKey = 'style'
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}`

View File

@ -33,3 +33,8 @@
.introjs-prevbutton{
@apply bg-transparent border border-blue-600 text-blue-600 !important;
}
html, body {
font-family: 'Poppins', ui-sans-serif, system-ui, sans-serif;
}

View File

@ -8,7 +8,7 @@ i18n
.use(LanguageDetector)
.use(initReactI18next)
.init({
fallbackLng: 'en',
fallbackLng: 'tr',
detection: {
order: ['localStorage', 'navigator'],
lookupLocalStorage: 'app_lang_',

View File

@ -1,5 +1,4 @@
import React, { ReactNode, useEffect } from 'react'
import { useState } from 'react'
import React, { ReactNode, useEffect, useState } from 'react'
import jwt from 'jsonwebtoken';
import { mdiForwardburger, mdiBackburger, mdiMenu } from '@mdi/js'
import menuAside from '../menuAside'
@ -122,7 +121,7 @@ export default function LayoutAuthenticated({
onAsideLgClose={() => setIsAsideLgActive(false)}
/>
{children}
<FooterBar>Hand-crafted & Made with </FooterBar>
<FooterBar>Satış, üretim ve etkinlik süreçleri tek ekranda yönetilir.</FooterBar>
</div>
</div>
)

View File

@ -1,234 +1,212 @@
import * as icon from '@mdi/js';
import * as icon from '@mdi/js'
import { MenuAsideItem } from './interfaces'
const mdi = icon as Record<string, string>
const pickIcon = (name: string, fallback = icon.mdiTable) => mdi[name] || fallback
const menuAside: MenuAsideItem[] = [
{
href: '/dashboard',
icon: icon.mdiViewDashboardOutline,
label: 'Dashboard',
},
{
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'
label: 'Operasyon Özeti',
},
{
href: '/roles/roles-list',
label: 'Roles',
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
// @ts-ignore
icon: icon.mdiShieldAccountVariantOutline ?? icon.mdiTable,
permissions: 'READ_ROLES'
href: '/sales-hub',
icon: icon.mdiChartTimelineVariant,
label: 'Satış Merkezi',
},
{
href: '/permissions/permissions-list',
label: 'Permissions',
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
// @ts-ignore
icon: icon.mdiShieldAccountOutline ?? icon.mdiTable,
permissions: 'READ_PERMISSIONS'
label: 'CRM ve Satış',
icon: pickIcon('mdiHandshake', icon.mdiTable),
withDevider: true,
menu: [
{
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: 'Organizations',
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
// @ts-ignore
icon: icon.mdiTable ?? icon.mdiTable,
permissions: 'READ_ORGANIZATIONS'
label: 'Üretim ERP',
icon: pickIcon('mdiFactory', icon.mdiTable),
menu: [
{
href: '/materials/materials-list',
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: 'Companies',
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
// @ts-ignore
icon: 'mdiDomain' in icon ? icon['mdiDomain' as keyof typeof icon] : icon.mdiTable ?? icon.mdiTable,
permissions: 'READ_COMPANIES'
label: 'Etkinlik Yönetimi',
icon: pickIcon('mdiCalendarStar', icon.mdiTable),
menu: [
{
href: '/venues/venues-list',
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: 'Contacts',
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
// @ts-ignore
icon: 'mdiCardAccountDetails' in icon ? icon['mdiCardAccountDetails' as keyof typeof icon] : icon.mdiTable ?? icon.mdiTable,
permissions: 'READ_CONTACTS'
},
{
href: '/pipelines/pipelines-list',
label: 'Pipelines',
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
// @ts-ignore
icon: 'mdiSourceBranch' in icon ? icon['mdiSourceBranch' as keyof typeof icon] : icon.mdiTable ?? icon.mdiTable,
permissions: 'READ_PIPELINES'
},
{
href: '/pipeline_stages/pipeline_stages-list',
label: 'Pipeline stages',
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
// @ts-ignore
icon: 'mdiSwapHorizontal' in icon ? icon['mdiSwapHorizontal' as keyof typeof icon] : icon.mdiTable ?? icon.mdiTable,
permissions: 'READ_PIPELINE_STAGES'
},
{
href: '/leads/leads-list',
label: 'Leads',
// 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,
permissions: 'READ_LEADS'
},
{
href: '/deals/deals-list',
label: 'Deals',
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
// @ts-ignore
icon: 'mdiHandshake' in icon ? icon['mdiHandshake' as keyof typeof icon] : icon.mdiTable ?? icon.mdiTable,
permissions: 'READ_DEALS'
},
{
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'
label: 'Yönetim',
icon: icon.mdiShieldAccountVariantOutline,
menu: [
{
href: '/users/users-list',
label: 'Kullanıcılar',
icon: icon.mdiAccountGroup,
permissions: 'READ_USERS',
},
{
href: '/roles/roles-list',
label: 'Roller',
icon: icon.mdiShieldAccountVariantOutline,
permissions: 'READ_ROLES',
},
{
href: '/permissions/permissions-list',
label: 'Yetkiler',
icon: icon.mdiShieldAccountOutline,
permissions: 'READ_PERMISSIONS',
},
{
href: '/organizations/organizations-list',
label: 'Organizasyonlar',
icon: icon.mdiTable,
permissions: 'READ_ORGANIZATIONS',
},
{
href: '/profile',
label: 'Profil',
icon: icon.mdiAccountCircle,
},
{
href: '/api-docs',
target: '_blank',
label: 'Swagger API',
icon: icon.mdiFileCode,
permissions: 'READ_API_DOCS',
},
],
},
]

View File

@ -1,15 +1,7 @@
import {
mdiMenu,
mdiClockOutline,
mdiCloud,
mdiCrop,
mdiAccount,
mdiCogOutline,
mdiEmail,
mdiLogout,
mdiThemeLightDark,
mdiGithub,
mdiVuejs,
} from '@mdi/js'
import { MenuNavBarItem } from './interfaces'
@ -19,7 +11,7 @@ const menuNavBar: MenuNavBarItem[] = [
menu: [
{
icon: mdiAccount,
label: 'My Profile',
label: 'Profilim',
href: '/profile',
},
{
@ -27,27 +19,25 @@ const menuNavBar: MenuNavBarItem[] = [
},
{
icon: mdiLogout,
label: 'Log Out',
label: 'Çıkış yap',
isLogout: true,
},
],
},
{
icon: mdiThemeLightDark,
label: 'Light/Dark',
label: 'ık/Koyu Tema',
isDesktopNoLabel: true,
isToggleLightDark: true,
},
{
icon: mdiLogout,
label: 'Log out',
label: 'Çıkış yap',
isDesktopNoLabel: true,
isLogout: true,
},
]
export const webPagesNavBar = [
];
export const webPagesNavBar = []
export default menuNavBar

View File

@ -3,24 +3,30 @@ import type { AppProps } from 'next/app';
import type { ReactElement, ReactNode } from 'react';
import type { NextPage } from 'next';
import Head from 'next/head';
import { Poppins } from 'next/font/google';
import { store } from '../stores/store';
import { Provider } from 'react-redux';
import '../css/main.css';
import axios from 'axios';
import { baseURLApi } from '../config';
import { useRouter } from 'next/router';
import ErrorBoundary from "../components/ErrorBoundary";
import ErrorBoundary from '../components/ErrorBoundary';
import DevModeBadge from '../components/DevModeBadge';
import 'intro.js/introjs.css';
import { appWithTranslation } from 'next-i18next';
import '../i18n';
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
? process.env.NEXT_PUBLIC_BACK_API
: baseURLApi;
? process.env.NEXT_PUBLIC_BACK_API
: baseURLApi;
axios.defaults.headers.common['Content-Type'] = 'application/json';
@ -33,115 +39,114 @@ type AppPropsWithLayout = AppProps & {
}
function MyApp({ Component, pageProps }: AppPropsWithLayout) {
// Use the layout defined at the page level, if available
const getLayout = Component.getLayout || ((page) => page);
const router = useRouter();
const [stepsEnabled, setStepsEnabled] = React.useState(false);
const [stepName, setStepName] = React.useState('');
const [steps, setSteps] = React.useState([]);
axios.interceptors.request.use(
config => {
const token = localStorage.getItem('token');
React.useEffect(() => {
document.documentElement.lang = 'tr';
}, []);
if (token) {
config.headers.Authorization = `Bearer ${token}`;
} else {
delete config.headers.Authorization;
}
axios.interceptors.request.use(
(config) => {
const token = localStorage.getItem('token');
return config;
},
error => {
return Promise.reject(error);
}
);
if (token) {
config.headers.Authorization = `Bearer ${token}`;
} else {
delete config.headers.Authorization;
}
// TODO: Remove this code in future releases
React.useEffect(() => {
const allowedOrigin = (() => {
if (!document.referrer) {
return null;
}
try {
return new URL(document.referrer).origin;
} catch (error) {
console.warn('[postMessage] Failed to parse parent origin from referrer', error);
return null;
}
})();
const handleMessage = async (event: MessageEvent) => {
if (event.data === 'getLocation') {
event.source?.postMessage(
{ iframeLocation: window.location.pathname },
event.origin,
);
return;
}
if (event.data === 'getAuthToken') {
if (allowedOrigin && event.origin !== allowedOrigin) {
console.warn('[postMessage] Blocked getAuthToken from origin', event.origin);
return;
}
const token = localStorage.getItem('token');
const user = localStorage.getItem('user');
event.source?.postMessage(
{ iframeAuthToken: token, iframeAuthUser: user },
event.origin,
);
return;
}
if (event.data === 'getScreenshot') {
try {
const html2canvas = (await import('html2canvas')).default;
const canvas = await html2canvas(document.body, { useCORS: true });
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);
}, []);
return config;
},
(error) => Promise.reject(error),
);
React.useEffect(() => {
const allowedOrigin = (() => {
if (!document.referrer) {
return null;
}
try {
return new URL(document.referrer).origin;
} catch (error) {
console.warn('[postMessage] Failed to parse parent origin from referrer', error);
return null;
}
})();
const handleMessage = async (event: MessageEvent) => {
if (event.data === 'getLocation') {
event.source?.postMessage(
{ iframeLocation: window.location.pathname },
event.origin,
);
return;
}
if (event.data === 'getAuthToken') {
if (allowedOrigin && event.origin !== allowedOrigin) {
console.warn('[postMessage] Blocked getAuthToken from origin', event.origin);
return;
}
const token = localStorage.getItem('token');
const user = localStorage.getItem('user');
event.source?.postMessage(
{ iframeAuthToken: token, iframeAuthUser: user },
event.origin,
);
return;
}
if (event.data === 'getScreenshot') {
try {
const html2canvas = (await import('html2canvas')).default;
const canvas = await html2canvas(document.body, { useCORS: true });
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);
}, []);
React.useEffect(() => {
// Tour is disabled by default in generated projects.
return;
const isCompleted = (stepKey: string) => {
return localStorage.getItem(`completed_${stepKey}`) === 'true';
};
if (router.pathname === '/login' && !isCompleted('loginSteps')) {
if (router.pathname === '/login' && !isCompleted('loginSteps')) {
setSteps(loginSteps);
setStepName('loginSteps');
setStepsEnabled(true);
}else if (router.pathname === '/dashboard' && !isCompleted('appSteps')) {
setStepsEnabled(true);
} else if (router.pathname === '/dashboard' && !isCompleted('appSteps')) {
setTimeout(() => {
setSteps(appSteps);
setStepName('appSteps');
setStepsEnabled(true);
setStepsEnabled(true);
}, 1000);
} else if (router.pathname === '/users/users-list' && !isCompleted('usersSteps')) {
setTimeout(() => {
setSteps(usersSteps);
setStepName('usersSteps');
setStepsEnabled(true);
setStepsEnabled(true);
}, 1000);
} else if (router.pathname === '/roles/roles-list' && !isCompleted('rolesSteps')) {
setTimeout(() => {
setSteps(rolesSteps);
setStepName('rolesSteps');
setStepsEnabled(true);
setStepsEnabled(true);
}, 1000);
} else {
setSteps([]);
setStepsEnabled(false);
setStepsEnabled(false);
}
}, [router.pathname]);
@ -149,51 +154,56 @@ function MyApp({ Component, pageProps }: AppPropsWithLayout) {
setStepsEnabled(false);
};
const title = 'DakikTabela Ops Suite'
const description = "Unified ops suite for CRM pipelines, manufacturing ERP tracking, and event planning coordination with ownership and follow-ups."
const url = "https://flatlogic.com/"
const image = "https://project-screens.s3.amazonaws.com/screenshots/39422/app-hero-20260331-214356.png"
const imageWidth = '1920'
const imageHeight = '960'
const title = 'DakikTabela Operasyon Platformu';
const description =
'DakikTabela için CRM, üretim ERP ve etkinlik operasyonlarını tek merkezde yöneten Türkçe iş platformu.';
const url = 'https://flatlogic.com/';
const image = 'https://project-screens.s3.amazonaws.com/screenshots/39422/app-hero-20260331-214356.png';
const imageWidth = '1920';
const imageHeight = '960';
return (
<Provider store={store}>
{getLayout(
<>
<Head>
<meta name="description" content={description} />
<div className={poppins.className}>
{getLayout(
<>
<Head>
<meta name='description' content={description} />
<meta property="og:url" content={url} />
<meta property="og:site_name" content="https://flatlogic.com/" />
<meta property="og:title" content={title} />
<meta property="og:description" content={description} />
<meta property="og:image" content={image} />
<meta property="og:image:type" content="image/png" />
<meta property="og:image:width" content={imageWidth} />
<meta property="og:image:height" content={imageHeight} />
<meta property='og:url' content={url} />
<meta property='og:site_name' content='https://flatlogic.com/' />
<meta property='og:title' content={title} />
<meta property='og:description' content={description} />
<meta property='og:image' content={image} />
<meta property='og:image:type' content='image/png' />
<meta property='og:image:width' content={imageWidth} />
<meta property='og:image:height' content={imageHeight} />
<meta property="twitter:card" content="summary_large_image" />
<meta property="twitter:title" content={title} />
<meta property="twitter:description" content={description} />
<meta property="twitter:image:src" content={image} />
<meta property="twitter:image:width" content={imageWidth} />
<meta property="twitter:image:height" content={imageHeight} />
<meta property='twitter:card' content='summary_large_image' />
<meta property='twitter:title' content={title} />
<meta property='twitter:description' content={description} />
<meta property='twitter:image:src' content={image} />
<meta property='twitter:image:width' content={imageWidth} />
<meta property='twitter:image:height' content={imageHeight} />
<link rel="icon" href="/favicon.svg" />
</Head>
<link rel='icon' href='/favicon.svg' />
</Head>
<ErrorBoundary>
<Component {...pageProps} />
</ErrorBoundary>
<IntroGuide
steps={steps}
stepsName={stepName}
stepsEnabled={stepsEnabled}
onExit={handleExit}
/>
{(process.env.NODE_ENV === 'development' || process.env.NODE_ENV === 'dev_stage') && <DevModeBadge />}
</>
)}
<ErrorBoundary>
<Component {...pageProps} />
</ErrorBoundary>
<IntroGuide
steps={steps}
stepsName={stepName}
stepsEnabled={stepsEnabled}
onExit={handleExit}
/>
{(process.env.NODE_ENV === 'development' || process.env.NODE_ENV === 'dev_stage') && (
<DevModeBadge />
)}
</>
)}
</div>
</Provider>
)
}

File diff suppressed because it is too large Load Diff

View File

@ -38,7 +38,7 @@ export default function Forgot() {
return (
<>
<Head>
<title>{getPageTitle('Login')}</title>
<title>{getPageTitle('Şifremi Unuttum')}</title>
</Head>
<SectionFullScreen bg='violet'>
@ -50,7 +50,7 @@ export default function Forgot() {
onSubmit={(values) => handleSubmit(values)}
>
<Form>
<FormField label='Email' help='Please enter your email'>
<FormField label='E-posta' help='E-posta adresinizi yazın'>
<Field name='email' />
</FormField>
@ -59,12 +59,12 @@ export default function Forgot() {
<BaseButtons>
<BaseButton
type='submit'
label={loading ? 'Loading...' : 'Submit' }
label={loading ? 'Yükleniyor...' : 'Gönder' }
color='info'
/>
<BaseButton
href={'/login'}
label={'Login'}
label={'Giriş yap'}
color='info'
/>
</BaseButtons>

View File

@ -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';
import type { ReactElement } from 'react';
import Head from 'next/head';
import Link from 'next/link';
import BaseButton from '../components/BaseButton';
import CardBox from '../components/CardBox';
import SectionFullScreen from '../components/SectionFullScreen';
import LayoutGuest from '../layouts/Guest';
import BaseDivider from '../components/BaseDivider';
import BaseButtons from '../components/BaseButtons';
import { getPageTitle } from '../config';
import { useAppSelector } from '../stores/hooks';
import CardBoxComponentTitle from "../components/CardBoxComponentTitle";
import { getPexelsImage, getPexelsVideo } from '../helpers/pexels';
const platformCards = [
{
title: 'Satış CRM',
description: 'Potansiyel müşteri, fırsat, kişi, aktivite ve takip adımlarını tek yerden yönetin.',
icon: mdiHandshake,
href: '/sales-hub',
cta: 'Satış merkezini aç',
},
{
title: 'Üretim ERP',
description: 'Malzeme, depo, stok, makine, üretim emri ve kalite operasyonlarını görünür kılın.',
icon: mdiFactory,
href: '/dashboard',
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() {
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>)
}
};
export default function IndexPage() {
return (
<div
style={
contentPosition === 'background'
? {
backgroundImage: `${
illustrationImage
? `url(${illustrationImage.src?.original})`
: 'linear-gradient(rgba(255, 255, 255, 0.5), rgba(255, 255, 255, 0.5))'
}`,
backgroundSize: 'cover',
backgroundPosition: 'left center',
backgroundRepeat: 'no-repeat',
}
: {}
}
>
<>
<Head>
<title>{getPageTitle('Starter Page')}</title>
<title>{getPageTitle('DakikTabela')}</title>
</Head>
<SectionFullScreen bg='violet'>
<div
className={`flex ${
contentPosition === 'right' ? 'flex-row-reverse' : 'flex-row'
} min-h-screen w-full`}
>
{contentType === 'image' && contentPosition !== 'background'
? imageBlock(illustrationImage)
: null}
{contentType === 'video' && contentPosition !== 'background'
? videoBlock(illustrationVideo)
: null}
<div className='flex items-center justify-center flex-col space-y-4 w-full lg:w-full'>
<CardBox className='w-full md:w-3/5 lg:w-2/3'>
<CardBoxComponentTitle title="Welcome to your DakikTabela Ops Suite app!"/>
<div className="space-y-3">
<p className='text-center text-gray-500'>This is a React.js/Node.js app generated by the <a className={`${textColor}`} href="https://flatlogic.com/generator">Flatlogic Web App Generator</a></p>
<p className='text-center text-gray-500'>For guides and documentation please check
your local README.md and the <a className={`${textColor}`} href="https://flatlogic.com/documentation">Flatlogic documentation</a></p>
</div>
<BaseButtons>
<BaseButton
<main className='min-h-screen bg-[#f8fafc] text-slate-900'>
<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)]'>
<div className='mx-auto flex min-h-screen w-full max-w-7xl flex-col px-6 py-8 lg:px-10'>
<header className='flex flex-col gap-4 border-b border-slate-200/80 pb-6 md:flex-row md:items-center md:justify-between'>
<div className='flex items-center gap-4'>
<Logo className='h-14 w-auto rounded-2xl bg-white p-2 shadow-sm' />
<div>
<p className='text-xs font-semibold uppercase tracking-[0.24em] text-cyan-700'>dakiktabela.com</p>
<h1 className='mt-1 text-xl font-semibold tracking-tight'>DakikTabela Operasyon Platformu</h1>
</div>
</div>
<nav className='flex flex-wrap items-center gap-3'>
<Link
href='/login'
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
</Link>
</nav>
</header>
<div className='grid flex-1 items-center gap-14 py-14 lg:grid-cols-[1.1fr_0.9fr] lg:py-20'>
<div className='space-y-8'>
<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'>
Kurumsal, Türkçe ve operasyon odaklı
</div>
<div className='space-y-5'>
<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 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'
label='Login'
color='info'
className='w-full'
/>
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'
>
Sisteme giriş yap
</Link>
</div>
</BaseButtons>
</CardBox>
</div>
</div>
</SectionFullScreen>
<div className='bg-black text-white flex flex-col text-center justify-center md:flex-row'>
<p className='py-6 text-sm'>© 2026 <span>{title}</span>. All rights reserved</p>
<Link className='py-6 ml-4 text-sm' href='/privacy-policy/'>
Privacy Policy
</Link>
</div>
<div className='grid gap-3'>
{promises.map((promise) => (
<div key={promise} className='flex items-start gap-3 rounded-2xl bg-white/85 p-4 shadow-sm'>
<svg viewBox='0 0 24 24' className='mt-0.5 h-5 w-5 shrink-0 fill-cyan-600'>
<path d={mdiCheckCircleOutline} />
</svg>
<p className='text-sm leading-6 text-slate-700'>{promise}</p>
</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) {
return <LayoutGuest>{page}</LayoutGuest>;
};
IndexPage.getLayout = function getLayout(page: ReactElement) {
return <LayoutGuest>{page}</LayoutGuest>
}

View File

@ -154,7 +154,7 @@ export default function Login() {
backgroundRepeat: 'no-repeat',
} : {}}>
<Head>
<title>{getPageTitle('Login')}</title>
<title>{getPageTitle('Giriş')}</title>
</Head>
<SectionFullScreen bg='violet'>
@ -170,25 +170,25 @@ export default function Login() {
<div className='flex flex-row text-gray-500 justify-between'>
<div>
<p className='mb-2'>Use{' '}
<p className='mb-2'>Kullan{' '}
<code className={`cursor-pointer ${textColor} `}
data-password="9c0c2fc3"
onClick={(e) => setLogin(e.target)}>super_admin@flatlogic.com</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} `}
data-password="9c0c2fc3"
onClick={(e) => setLogin(e.target)}>admin@flatlogic.com</code>{' / '}
<code className={`${textColor}`}>9c0c2fc3</code>{' / '}
to login as Admin</p>
ile Admin olarak giriş yapın</p>
<p>Use <code
className={`cursor-pointer ${textColor} `}
data-password="0d5e2b23b648"
onClick={(e) => setLogin(e.target)}>client@hello.com</code>{' / '}
<code className={`${textColor}`}>0d5e2b23b648</code>{' / '}
to login as User</p>
ile Kullanıcı olarak giriş yapın</p>
</div>
<div>
<BaseIcon
@ -210,15 +210,15 @@ export default function Login() {
>
<Form>
<FormField
label='Login'
help='Please enter your login'>
label='Giriş'
help='Giriş e-posta adresinizi yazın'>
<Field name='email' />
</FormField>
<div className='relative'>
<FormField
label='Password'
help='Please enter your password'>
label='Şifre'
help='Şifrenizi yazın'>
<Field name='password' type={showPassword ? 'text' : 'password'} />
</FormField>
<div
@ -234,12 +234,12 @@ export default function Login() {
</div>
<div className={'flex justify-between'}>
<FormCheckRadio type='checkbox' label='Remember'>
<FormCheckRadio type='checkbox' label='Beni hatırla'>
<Field type='checkbox' name='remember' />
</FormCheckRadio>
<Link className={`${textColor} text-blue-600`} href={'/forgot'}>
Forgot password?
Şifremi unuttum
</Link>
</div>
@ -249,16 +249,16 @@ export default function Login() {
<BaseButton
className={'w-full'}
type='submit'
label={isFetching ? 'Loading...' : 'Login'}
label={isFetching ? 'Yükleniyor...' : 'Giriş yap'}
color='info'
disabled={isFetching}
/>
</BaseButtons>
<br />
<p className={'text-center'}>
Dont have an account yet?{' '}
Henüz hesabınız yok mu?{' '}
<Link className={`${textColor}`} href={'/register'}>
New Account
Yeni hesap oluştur
</Link>
</p>
</Form>

View File

@ -65,18 +65,18 @@ const EditUsers = () => {
await dispatch(update({ id: currentUser.id, data }));
await dispatch(findMe());
await router.push('/users/users-list');
notify('success', 'Profile was updated!');
notify('success', 'Profil güncellendi.');
};
return (
<>
<Head>
<title>{getPageTitle('Edit profile')}</title>
<title>{getPageTitle('Profilimi Düzenle')}</title>
</Head>
<SectionMain>
<SectionTitleLineWithButton
icon={mdiChartTimelineVariant}
title='Edit profile'
title='Profilimi Düzenle'
main
>
{''}
@ -84,7 +84,7 @@ const EditUsers = () => {
<CardBox>
{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">
<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>}
<Formik
@ -95,7 +95,7 @@ const EditUsers = () => {
<Form>
<FormField>
<Field
label='Avatar'
label='Profil görseli'
color='info'
icon={mdiUpload}
path={'users/avatar'}
@ -108,23 +108,23 @@ const EditUsers = () => {
component={FormImagePicker}
></Field>
</FormField>
<FormField label='First Name'>
<Field name='firstName' placeholder='First Name' />
<FormField label='Ad'>
<Field name='firstName' placeholder='Ad' />
</FormField>
<FormField label='Last Name'>
<Field name='lastName' placeholder='Last Name' />
<FormField label='Soyad'>
<Field name='lastName' placeholder='Soyad' />
</FormField>
<FormField label='Phone Number'>
<Field name='phoneNumber' placeholder='Phone Number' />
<FormField label='Telefon'>
<Field name='phoneNumber' placeholder='Telefon numarası' />
</FormField>
<FormField label='E-Mail'>
<Field name='email' placeholder='E-Mail' disabled />
</FormField>
<FormField label='App Role' labelFor='app_role'>
<FormField label='Uygulama rolü' labelFor='app_role'>
<Field
name='app_role'
id='app_role'
@ -135,7 +135,7 @@ const EditUsers = () => {
></Field>
</FormField>
<FormField label='Disabled' labelFor='disabled'>
<FormField label='Pasif' labelFor='disabled'>
<Field
name='disabled'
id='disabled'
@ -144,24 +144,24 @@ const EditUsers = () => {
</FormField>
<FormField
label="Password"
label="Şifre"
>
<Field
name="password"
placeholder="password"
placeholder="Yeni şifre"
/>
</FormField>
<BaseDivider />
<BaseButtons>
<BaseButton type='submit' color='info' label='Submit' />
<BaseButton type='reset' color='info' outline label='Reset' />
<BaseButton type='submit' color='info' label='Kaydet' />
<BaseButton type='reset' color='info' outline label='Formu sıfırla' />
<BaseButton
type='reset'
color='danger'
outline
label='Cancel'
label='İptal'
onClick={() => router.push('/users/users-list')}
/>
</BaseButtons>

View File

@ -69,7 +69,7 @@ export default function Register() {
return (
<>
<Head>
<title>{getPageTitle('Login')}</title>
<title>{getPageTitle('Kayıt Ol')}</title>
</Head>
<SectionFullScreen bg='violet'>
@ -96,13 +96,13 @@ export default function Register() {
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' />
</FormField>
<FormField label='Password' help='Please enter your password'>
<FormField label='Şifre' help='Şifrenizi yazın'>
<Field type='password' name='password' />
</FormField>
<FormField label='Confirm Password' help='Please confirm your password'>
<FormField label='Şifre Tekrarı' help='Şifrenizi tekrar yazın'>
<Field type='password' name='confirm' />
</FormField>
@ -111,12 +111,12 @@ export default function Register() {
<BaseButtons>
<BaseButton
type='submit'
label={loading ? 'Loading...' : 'Register' }
label={loading ? 'Yükleniyor...' : 'Kayıt ol' }
color='info'
/>
<BaseButton
href={'/login'}
label={'Login'}
label={'Giriş yap'}
color='info'
/>
</BaseButtons>

View 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ıı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&apos;ı 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 ı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

View File

@ -1,9 +1,7 @@
import React, { ReactElement, useEffect, useState } from 'react';
import Head from 'next/head';
import 'react-datepicker/dist/react-datepicker.css';
import { useAppDispatch } from '../stores/hooks';
import { useAppSelector } from '../stores/hooks';
import { useAppDispatch, useAppSelector } from '../stores/hooks';
import { useRouter } from 'next/router';
import LayoutAuthenticated from '../layouts/Authenticated';
@ -57,12 +55,12 @@ const SearchView = () => {
return (
<>
<Head>
<title>Search Result</title>
<title>Arama Sonuçları</title>
</Head>
<SectionMain>
<SectionTitleLineWithButton
icon={mdiChartTimelineVariant}
title={'Search Result'}
title={'Arama Sonuçları'}
main
>
{''}
@ -75,7 +73,7 @@ const SearchView = () => {
<BaseDivider />
<BaseButton
color='info'
label='Back'
label='Geri dön'
onClick={() => router.push('/dashboard')}
/>
</CardBox>

View File

@ -44,11 +44,11 @@ export default function Verify() {
return (
<>
<Head>
<title>{getPageTitle('Verify Email')}</title>
<title>{getPageTitle('E-posta Doğrulama')}</title>
</Head>
<SectionFullScreen bg='violet'>
<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>
</SectionFullScreen>

View File

@ -41,6 +41,9 @@ module.exports = {
'fade-out': 'fade-out 250ms ease-in-out',
'fade-in': 'fade-in 250ms ease-in-out'
},
fontFamily: {
sans: ['Poppins', 'ui-sans-serif', 'system-ui', 'sans-serif'],
},
colors: {
dark: {
900: '#131618',