40060-vm/frontend/src/pages/meal-command-center.tsx
Flatlogic Bot 9918c7098f Ver 1.0
2026-05-24 06:50:30 +00:00

714 lines
31 KiB
TypeScript

import * as icon from '@mdi/js';
import axios from 'axios';
import dayjs from 'dayjs';
import { Field, Form, Formik, FormikErrors } from 'formik';
import Head from 'next/head';
import React, { ReactElement, useCallback, useEffect, useMemo, useState } from 'react';
import BaseButton from '../components/BaseButton';
import BaseButtons from '../components/BaseButtons';
import BaseIcon from '../components/BaseIcon';
import CardBox from '../components/CardBox';
import FormField from '../components/FormField';
import LoadingSpinner from '../components/LoadingSpinner';
import NotificationBar from '../components/NotificationBar';
import SectionMain from '../components/SectionMain';
import SectionTitleLineWithButton from '../components/SectionTitleLineWithButton';
import { getPageTitle } from '../config';
import { hasPermission } from '../helpers/userPermissions';
import LayoutAuthenticated from '../layouts/Authenticated';
import { useAppSelector } from '../stores/hooks';
type FrameworkType = 'MERL' | 'MEL' | 'M&E';
type ReportingCycle = 'monthly' | 'quarterly' | 'semiannual' | 'annual';
type IndicatorStatus = 'baseline_due' | 'collecting' | 'on_track' | 'needs_attention';
type InitiativeStatus = 'planning' | 'active' | 'paused' | 'archived';
type FrameworkFilter = 'ALL' | FrameworkType;
type ProjectMember = {
id: string;
firstName?: string;
lastName?: string;
email?: string;
};
type MealProject = {
id: string;
name: string;
slug?: string | null;
description?: string | null;
status?: InitiativeStatus | null;
start_at?: string | null;
end_at?: string | null;
framework_type?: FrameworkType | null;
reporting_cycle?: ReportingCycle | null;
indicator_status?: IndicatorStatus | null;
primary_outcome?: string | null;
owner?: ProjectMember | null;
members?: ProjectMember[];
createdAt?: string;
};
type MealFormValues = {
name: string;
description: string;
framework_type: FrameworkType;
reporting_cycle: ReportingCycle;
indicator_status: IndicatorStatus;
primary_outcome: string;
status: InitiativeStatus;
start_at: string;
end_at: string;
members: string[];
};
type NoticeState = {
color: 'success' | 'danger';
text: string;
createdId?: string;
} | null;
const frameworkOptions: FrameworkType[] = ['MERL', 'MEL', 'M&E'];
const reportingCycleOptions: ReportingCycle[] = ['monthly', 'quarterly', 'semiannual', 'annual'];
const indicatorOptions: IndicatorStatus[] = ['baseline_due', 'collecting', 'on_track', 'needs_attention'];
const statusOptions: InitiativeStatus[] = ['planning', 'active', 'paused', 'archived'];
const initialValues: MealFormValues = {
name: '',
description: '',
framework_type: 'MERL',
reporting_cycle: 'quarterly',
indicator_status: 'baseline_due',
primary_outcome: '',
status: 'planning',
start_at: dayjs().format('YYYY-MM-DD'),
end_at: '',
members: [],
};
const FieldError = ({ error }: { error?: string }) =>
error ? <p className='-mt-3 mb-4 text-sm font-medium text-red-600'>{error}</p> : null;
const slugify = (value: string) => value
.toLowerCase()
.trim()
.replace(/&/g, ' and ')
.replace(/[^a-z0-9]+/g, '-')
.replace(/^-+|-+$/g, '')
.slice(0, 80);
const stripHtml = (value?: string | null) => (value || '')
.replace(/<[^>]+>/g, ' ')
.replace(/&nbsp;/g, ' ')
.replace(/\s+/g, ' ')
.trim();
const humanize = (value?: string | null) => {
if (!value) {
return 'Not set';
}
return value
.split('_')
.join(' ')
.replace(/\b\w/g, (letter) => letter.toUpperCase());
};
const formatDate = (value?: string | null) => {
if (!value) {
return 'Not scheduled';
}
return dayjs(value).format('DD MMM YYYY');
};
const getOwnerLabel = (owner?: ProjectMember | null) => {
if (!owner) {
return 'No lead assigned';
}
const fullName = [owner.firstName, owner.lastName].filter(Boolean).join(' ').trim();
return fullName || owner.email || 'Lead assigned';
};
const getSignalClass = (signal?: IndicatorStatus | null) => {
switch (signal) {
case 'on_track':
return 'border-emerald-200 bg-emerald-50 text-emerald-700';
case 'collecting':
return 'border-sky-200 bg-sky-50 text-sky-700';
case 'needs_attention':
return 'border-rose-200 bg-rose-50 text-rose-700';
case 'baseline_due':
default:
return 'border-amber-200 bg-amber-50 text-amber-700';
}
};
const getStatusClass = (status?: InitiativeStatus | null) => {
switch (status) {
case 'active':
return 'border-emerald-200 bg-emerald-50 text-emerald-700';
case 'paused':
return 'border-amber-200 bg-amber-50 text-amber-700';
case 'archived':
return 'border-slate-200 bg-slate-100 text-slate-600';
case 'planning':
default:
return 'border-indigo-200 bg-indigo-50 text-indigo-700';
}
};
const getFrameworkClass = (framework?: FrameworkType | null) => {
switch (framework) {
case 'MERL':
return 'border-[#C9E6E3] bg-[#F1FBF9] text-[#0E7C6B]';
case 'MEL':
return 'border-[#CFE2FF] bg-[#F3F8FF] text-[#1D4ED8]';
case 'M&E':
return 'border-[#E8D5FF] bg-[#F8F3FF] text-[#7C3AED]';
default:
return 'border-slate-200 bg-slate-50 text-slate-600';
}
};
const getFocusMessage = (project: MealProject) => {
if (project.indicator_status === 'needs_attention') {
return 'Flag the evidence gap, assign one owner, and schedule a short decision review this week.';
}
if (project.indicator_status === 'baseline_due') {
return 'Confirm baseline values before the next reporting cycle so trends stay credible.';
}
if (project.framework_type === 'MERL') {
return 'Blend monitoring, evaluation, research, and learning into a single evidence sprint.';
}
if (project.reporting_cycle === 'monthly') {
return 'Prepare a monthly pulse with one headline outcome, one risk, and one learning insight.';
}
return 'Keep the next learning review lightweight: outcome signal, evidence note, and one action owner.';
};
const summarize = (value?: string | null, fallback = 'Add a short initiative brief to anchor the portfolio card.') => {
const cleaned = stripHtml(value);
if (!cleaned) {
return fallback;
}
return cleaned.length > 140 ? `${cleaned.slice(0, 137)}...` : cleaned;
};
const MealCommandCenter = () => {
const { currentUser } = useAppSelector((state) => state.auth);
const [portfolio, setPortfolio] = useState<MealProject[]>([]);
const [selectedId, setSelectedId] = useState<string | null>(null);
const [activeFramework, setActiveFramework] = useState<FrameworkFilter>('ALL');
const [isLoading, setIsLoading] = useState(true);
const [notice, setNotice] = useState<NoticeState>(null);
const canCreateProjects = hasPermission(currentUser, 'CREATE_PROJECTS');
const loadPortfolio = useCallback(async (preferredId?: string | null) => {
setIsLoading(true);
try {
const { data } = await axios.get('/projects', {
params: {
limit: 50,
page: 0,
},
});
const rows = Array.isArray(data?.rows) ? data.rows : [];
setPortfolio(rows);
const nextSelectedId = preferredId && rows.some((item: MealProject) => item.id === preferredId)
? preferredId
: rows[0]?.id || null;
setSelectedId(nextSelectedId);
} catch (error) {
const message = axios.isAxiosError(error)
? typeof error.response?.data === 'string'
? error.response?.data
: error.message
: 'Unable to load the MEAL portfolio right now.';
console.error('MEAL portfolio load failed:', error);
setNotice({
color: 'danger',
text: message || 'Unable to load the MEAL portfolio right now.',
});
} finally {
setIsLoading(false);
}
}, []);
useEffect(() => {
if (!currentUser) {
return;
}
loadPortfolio(selectedId).catch((error) => {
console.error('MEAL portfolio bootstrap failed:', error);
});
}, [currentUser, loadPortfolio]);
const frameworkCounts = useMemo(
() => frameworkOptions.reduce<Record<FrameworkType, number>>((accumulator, framework) => {
accumulator[framework] = portfolio.filter((item) => item.framework_type === framework).length;
return accumulator;
}, { MERL: 0, MEL: 0, 'M&E': 0 }),
[portfolio],
);
const activeCount = useMemo(
() => portfolio.filter((item) => item.status === 'active').length,
[portfolio],
);
const attentionCount = useMemo(
() => portfolio.filter((item) => item.indicator_status === 'needs_attention').length,
[portfolio],
);
const filteredPortfolio = useMemo(
() => activeFramework === 'ALL'
? portfolio
: portfolio.filter((item) => item.framework_type === activeFramework),
[activeFramework, portfolio],
);
const selectedInitiative = useMemo(
() => portfolio.find((item) => item.id === selectedId) || null,
[portfolio, selectedId],
);
useEffect(() => {
if (!filteredPortfolio.length) {
if (selectedId !== null) {
setSelectedId(null);
}
return;
}
if (!selectedId || !filteredPortfolio.some((item) => item.id === selectedId)) {
setSelectedId(filteredPortfolio[0].id);
}
}, [filteredPortfolio, selectedId]);
if (!currentUser) {
return (
<>
<Head>
<title>{getPageTitle('MEAL Command Center')}</title>
</Head>
<SectionMain>
<SectionTitleLineWithButton icon={icon.mdiChartTimelineVariant} title='MEAL Command Center' main>
{''}
</SectionTitleLineWithButton>
<CardBox>
<LoadingSpinner />
</CardBox>
</SectionMain>
</>
);
}
return (
<>
<Head>
<title>{getPageTitle('MEAL Command Center')}</title>
</Head>
<SectionMain>
<SectionTitleLineWithButton icon={icon.mdiChartTimelineVariant} title='MEAL Command Center' main>
<BaseButton color='whiteDark' href='/projects/projects-list' label='Open Projects Admin' />
</SectionTitleLineWithButton>
<div className='mb-6 overflow-hidden rounded-3xl border border-[#CDE6EA] bg-gradient-to-br from-[#0B1F3A] via-[#17466C] to-[#15B8A6] text-white shadow-xl shadow-slate-200'>
<div className='grid gap-6 px-6 py-8 lg:grid-cols-[1.4fr_0.9fr] lg:px-8'>
<div>
<div className='mb-4 inline-flex items-center rounded-full border border-white/20 bg-white/10 px-4 py-2 text-xs font-semibold uppercase tracking-[0.24em] text-white/90'>
MERL / MEL / M&amp;E operating layer
</div>
<h1 className='max-w-3xl text-4xl font-semibold leading-tight md:text-5xl'>
Hello {currentUser.firstName || 'team'}, keep every initiative measurable, reviewable, and learning-ready.
</h1>
<p className='mt-4 max-w-2xl text-base leading-7 text-slate-100/90 md:text-lg'>
This first iteration turns the existing project register into a purpose-built MEAL portfolio: capture a new initiative,
assign the framework, set the reporting cadence, and review evidence health in one focused workspace.
</p>
<BaseButtons type='justify-start' className='mt-6'>
<BaseButton color='info' href='#meal-intake' label='Start a new intake' />
<BaseButton color='whiteDark' href='/dashboard' label='Back to overview' />
</BaseButtons>
<div className='mt-8 grid gap-3 sm:grid-cols-3'>
{frameworkOptions.map((framework) => (
<div key={framework} className='rounded-2xl border border-white/15 bg-white/10 p-4 backdrop-blur'>
<div className='text-xs uppercase tracking-[0.2em] text-white/70'>{framework}</div>
<div className='mt-2 text-3xl font-semibold'>{frameworkCounts[framework]}</div>
<p className='mt-2 text-sm text-white/80'>Framework-tagged initiatives currently in the portfolio.</p>
</div>
))}
</div>
</div>
<div className='rounded-3xl border border-white/15 bg-white/10 p-6 backdrop-blur'>
<p className='text-sm uppercase tracking-[0.22em] text-white/70'>Portfolio pulse</p>
<div className='mt-6 space-y-4'>
<div className='rounded-2xl bg-white/10 p-4'>
<div className='text-sm text-white/70'>Active delivery</div>
<div className='mt-1 text-3xl font-semibold'>{activeCount}</div>
</div>
<div className='rounded-2xl bg-white/10 p-4'>
<div className='text-sm text-white/70'>Needs attention</div>
<div className='mt-1 text-3xl font-semibold'>{attentionCount}</div>
</div>
<div className='rounded-2xl bg-white/10 p-4'>
<div className='text-sm text-white/70'>Reporting-ready</div>
<div className='mt-1 text-3xl font-semibold'>
{portfolio.filter((item) => ['collecting', 'on_track'].includes(item.indicator_status || '')).length}
</div>
</div>
</div>
<p className='mt-6 text-sm leading-6 text-white/85'>
Use this page for the thin-slice workflow: intake, confirmation, review, and a quick jump into the full admin record.
</p>
</div>
</div>
</div>
{notice && (
<NotificationBar
color={notice.color}
icon={notice.color === 'success' ? icon.mdiChartTimelineVariant : icon.mdiShieldAccountVariantOutline}
button={notice.createdId ? <BaseButton color='white' href={`/projects/projects-view/?id=${notice.createdId}`} label='Open detail' /> : undefined}
>
{notice.text}
</NotificationBar>
)}
<div className='grid gap-6 xl:grid-cols-[0.92fr_1.08fr]'>
<CardBox hasComponentLayout className='overflow-hidden'>
<div id='meal-intake' className='border-b border-slate-200 bg-slate-50 px-6 py-5'>
<div className='flex items-center gap-3'>
<div className='flex h-11 w-11 items-center justify-center rounded-2xl bg-[#E8FBF6] text-[#0E7C6B]'>
<BaseIcon path={icon.mdiAccountGroup} size={22} />
</div>
<div>
<h2 className='text-2xl font-semibold text-slate-900'>New MEAL initiative intake</h2>
<p className='text-sm text-slate-500'>Capture the minimum structure needed for MERL, MEL, or M&amp;E delivery.</p>
</div>
</div>
</div>
<div className='px-6 py-6'>
{canCreateProjects ? (
<Formik
initialValues={initialValues}
validate={(values) => {
const errors: FormikErrors<MealFormValues> = {};
if (!values.name.trim()) {
errors.name = 'Give the initiative a clear name.';
}
if (!values.primary_outcome.trim()) {
errors.primary_outcome = 'Describe the outcome or result this initiative is meant to improve.';
}
if (!values.start_at) {
errors.start_at = 'Pick a start date.';
}
if (values.end_at && values.start_at && dayjs(values.end_at).isBefore(dayjs(values.start_at))) {
errors.end_at = 'End date must come after the start date.';
}
return errors;
}}
onSubmit={async (values, { resetForm, setSubmitting }) => {
setNotice(null);
try {
const payload = {
...values,
slug: slugify(values.name),
owner: currentUser.id,
};
const { data } = await axios.post('/projects', { data: payload });
await loadPortfolio(data?.id || null);
resetForm();
setActiveFramework('ALL');
setNotice({
color: 'success',
text: `${values.name} was added to your MEAL portfolio and is ready for review.`,
createdId: data?.id,
});
} catch (error) {
const message = axios.isAxiosError(error)
? typeof error.response?.data === 'string'
? error.response?.data
: error.message
: 'We could not save the initiative just now.';
console.error('MEAL intake submission failed:', error);
setNotice({
color: 'danger',
text: message || 'We could not save the initiative just now.',
});
} finally {
setSubmitting(false);
}
}}
>
{({ errors, isSubmitting, touched }) => (
<Form>
<div className='grid gap-4 lg:grid-cols-2'>
<div>
<FormField label='Initiative name'>
<Field name='name' placeholder='e.g. Youth livelihoods quarterly review' />
</FormField>
<FieldError error={touched.name ? errors.name : undefined} />
</div>
<div>
<FormField label='Framework'>
<Field as='select' name='framework_type'>
{frameworkOptions.map((framework) => (
<option key={framework} value={framework}>{framework}</option>
))}
</Field>
</FormField>
</div>
</div>
<div className='grid gap-4 lg:grid-cols-2'>
<FormField label='Delivery stage'>
<Field as='select' name='status'>
{statusOptions.map((status) => (
<option key={status} value={status}>{humanize(status)}</option>
))}
</Field>
</FormField>
<FormField label='Reporting cadence'>
<Field as='select' name='reporting_cycle'>
{reportingCycleOptions.map((cycle) => (
<option key={cycle} value={cycle}>{humanize(cycle)}</option>
))}
</Field>
</FormField>
</div>
<div className='grid gap-4 lg:grid-cols-2'>
<FormField label='Evidence signal'>
<Field as='select' name='indicator_status'>
{indicatorOptions.map((indicator) => (
<option key={indicator} value={indicator}>{humanize(indicator)}</option>
))}
</Field>
</FormField>
</div>
<div className='grid gap-4 lg:grid-cols-2'>
<div>
<FormField label='Start date'>
<Field name='start_at' type='date' />
</FormField>
<FieldError error={touched.start_at ? errors.start_at : undefined} />
</div>
<div>
<FormField label='End date'>
<Field name='end_at' type='date' />
</FormField>
<FieldError error={touched.end_at ? errors.end_at : undefined} />
</div>
</div>
<div>
<FormField label='Primary outcome' hasTextareaHeight>
<Field as='textarea' name='primary_outcome' placeholder='What change, result, or key outcome will this initiative monitor or evaluate?' />
</FormField>
<FieldError error={touched.primary_outcome ? errors.primary_outcome : undefined} />
</div>
<FormField label='Context note' help='Optional: add a quick narrative, hypothesis, or reminder for the next learning review.' hasTextareaHeight>
<Field as='textarea' name='description' placeholder='Add context that helps the team interpret the data when review time comes.' />
</FormField>
<BaseButtons type='justify-start' className='mt-3'>
<BaseButton color='info' disabled={isSubmitting} label={isSubmitting ? 'Saving initiative...' : 'Save initiative'} type='submit' />
<BaseButton color='whiteDark' href='/projects/projects-list' label='Open full project list' />
</BaseButtons>
</Form>
)}
</Formik>
) : (
<div className='rounded-2xl border border-amber-200 bg-amber-50 px-4 py-5 text-sm text-amber-800'>
You can review the MEAL portfolio, but you need <span className='font-semibold'>CREATE_PROJECTS</span> permission to submit new initiatives.
</div>
)}
</div>
</CardBox>
<CardBox hasComponentLayout className='overflow-hidden'>
<div className='border-b border-slate-200 bg-slate-50 px-6 py-5'>
<div className='flex flex-col gap-4 xl:flex-row xl:items-end xl:justify-between'>
<div>
<h2 className='text-2xl font-semibold text-slate-900'>Portfolio radar</h2>
<p className='text-sm text-slate-500'>Review the latest initiatives, switch between frameworks, and inspect the evidence signal.</p>
</div>
<div className='flex flex-wrap gap-2'>
{(['ALL', ...frameworkOptions] as FrameworkFilter[]).map((framework) => (
<button
key={framework}
type='button'
onClick={() => setActiveFramework(framework)}
className={`rounded-full border px-4 py-2 text-sm font-medium transition ${activeFramework === framework ? 'border-[#0B5FFF] bg-[#0B5FFF] text-white shadow-sm' : 'border-slate-200 bg-white text-slate-600 hover:border-slate-300 hover:text-slate-900'}`}
>
{framework === 'ALL' ? 'All frameworks' : framework}
</button>
))}
</div>
</div>
</div>
<div className='grid gap-6 px-6 py-6 lg:grid-cols-[0.92fr_1.08fr]'>
<div className='space-y-4'>
{isLoading ? (
<LoadingSpinner />
) : filteredPortfolio.length ? (
filteredPortfolio.map((initiative) => (
<button
key={initiative.id}
type='button'
onClick={() => setSelectedId(initiative.id)}
className={`w-full rounded-3xl border p-5 text-left transition ${selectedId === initiative.id ? 'border-[#0B5FFF] bg-[#F3F8FF] shadow-sm' : 'border-slate-200 bg-white hover:border-slate-300 hover:shadow-sm'}`}
>
<div className='flex flex-wrap items-center gap-2'>
<span className={`rounded-full border px-3 py-1 text-xs font-semibold ${getFrameworkClass(initiative.framework_type)}`}>
{initiative.framework_type || 'Framework pending'}
</span>
<span className={`rounded-full border px-3 py-1 text-xs font-semibold ${getStatusClass(initiative.status)}`}>
{humanize(initiative.status)}
</span>
<span className={`rounded-full border px-3 py-1 text-xs font-semibold ${getSignalClass(initiative.indicator_status)}`}>
{humanize(initiative.indicator_status)}
</span>
</div>
<h3 className='mt-4 text-lg font-semibold text-slate-900'>{initiative.name}</h3>
<p className='mt-2 text-sm leading-6 text-slate-600'>{summarize(initiative.description)}</p>
<div className='mt-4 grid gap-3 text-sm text-slate-500 sm:grid-cols-2'>
<div>
<p className='text-xs font-semibold uppercase tracking-[0.18em] text-slate-400'>Lead</p>
<p className='mt-1 font-medium text-slate-700'>{getOwnerLabel(initiative.owner)}</p>
</div>
<div>
<p className='text-xs font-semibold uppercase tracking-[0.18em] text-slate-400'>Cadence</p>
<p className='mt-1 font-medium text-slate-700'>{humanize(initiative.reporting_cycle)}</p>
</div>
</div>
</button>
))
) : (
<div className='rounded-3xl border border-dashed border-slate-300 bg-slate-50 px-6 py-10 text-center'>
<p className='text-lg font-semibold text-slate-800'>No initiatives found for this view.</p>
<p className='mt-2 text-sm text-slate-500'>Try another framework filter, or create the first initiative from the intake panel.</p>
</div>
)}
</div>
<div className='rounded-3xl border border-slate-200 bg-slate-50 p-6'>
{selectedInitiative ? (
<>
<div className='flex flex-wrap items-center gap-2'>
<span className={`rounded-full border px-3 py-1 text-xs font-semibold ${getFrameworkClass(selectedInitiative.framework_type)}`}>
{selectedInitiative.framework_type || 'Framework pending'}
</span>
<span className={`rounded-full border px-3 py-1 text-xs font-semibold ${getStatusClass(selectedInitiative.status)}`}>
{humanize(selectedInitiative.status)}
</span>
</div>
<h3 className='mt-4 text-2xl font-semibold text-slate-900'>{selectedInitiative.name}</h3>
<p className='mt-3 text-sm leading-7 text-slate-600'>{summarize(selectedInitiative.description, 'No context note has been added yet for this initiative.')}</p>
<div className='mt-6 grid gap-4 md:grid-cols-2'>
<div className='rounded-2xl bg-white p-4 shadow-sm'>
<p className='text-xs font-semibold uppercase tracking-[0.18em] text-slate-400'>Primary outcome</p>
<p className='mt-3 text-sm leading-6 text-slate-700'>
{selectedInitiative.primary_outcome || 'No outcome statement yet.'}
</p>
</div>
<div className='rounded-2xl bg-white p-4 shadow-sm'>
<p className='text-xs font-semibold uppercase tracking-[0.18em] text-slate-400'>Recommended next move</p>
<p className='mt-3 text-sm leading-6 text-slate-700'>{getFocusMessage(selectedInitiative)}</p>
</div>
</div>
<div className='mt-6 grid gap-4 sm:grid-cols-2 xl:grid-cols-3'>
<div className='rounded-2xl border border-slate-200 bg-white p-4'>
<p className='text-xs font-semibold uppercase tracking-[0.18em] text-slate-400'>Lead owner</p>
<p className='mt-2 text-sm font-medium text-slate-700'>{getOwnerLabel(selectedInitiative.owner)}</p>
</div>
<div className='rounded-2xl border border-slate-200 bg-white p-4'>
<p className='text-xs font-semibold uppercase tracking-[0.18em] text-slate-400'>Collaborators</p>
<p className='mt-2 text-sm font-medium text-slate-700'>{selectedInitiative.members?.length || 0} team members</p>
</div>
<div className='rounded-2xl border border-slate-200 bg-white p-4'>
<p className='text-xs font-semibold uppercase tracking-[0.18em] text-slate-400'>Evidence signal</p>
<p className='mt-2 text-sm font-medium text-slate-700'>{humanize(selectedInitiative.indicator_status)}</p>
</div>
</div>
<div className='mt-6 grid gap-4 sm:grid-cols-2'>
<div className='rounded-2xl border border-slate-200 bg-white p-4'>
<p className='text-xs font-semibold uppercase tracking-[0.18em] text-slate-400'>Schedule</p>
<p className='mt-2 text-sm font-medium text-slate-700'>{formatDate(selectedInitiative.start_at)} {formatDate(selectedInitiative.end_at)}</p>
</div>
<div className='rounded-2xl border border-slate-200 bg-white p-4'>
<p className='text-xs font-semibold uppercase tracking-[0.18em] text-slate-400'>Reporting cadence</p>
<p className='mt-2 text-sm font-medium text-slate-700'>{humanize(selectedInitiative.reporting_cycle)}</p>
</div>
</div>
<BaseButtons type='justify-start' className='mt-6'>
<BaseButton color='info' href={`/projects/projects-view/?id=${selectedInitiative.id}`} label='Open full detail' />
<BaseButton color='whiteDark' href={`/projects/projects-edit/?id=${selectedInitiative.id}`} label='Adjust record' />
</BaseButtons>
</>
) : (
<div className='flex h-full min-h-[260px] items-center justify-center rounded-3xl border border-dashed border-slate-300 bg-white px-6 text-center'>
<div>
<p className='text-lg font-semibold text-slate-800'>Select an initiative to inspect it.</p>
<p className='mt-2 text-sm text-slate-500'>The detail panel will show the current outcome, evidence signal, cadence, and next recommended move.</p>
</div>
</div>
)}
</div>
</div>
</CardBox>
</div>
</SectionMain>
</>
);
};
MealCommandCenter.getLayout = function getLayout(page: ReactElement) {
return (
<LayoutAuthenticated permission='READ_PROJECTS'>
{page}
</LayoutAuthenticated>
);
};
export default MealCommandCenter;