import React, { useEffect, useMemo, useState } from "react";
import { createRoot } from "react-dom/client";
import "../public/assets/styles.css";
import "./react.css";

const MONTHS = [
  "January",
  "February",
  "March",
  "April",
  "May",
  "June",
  "July",
  "August",
  "September",
  "October",
  "November",
  "December"
];

const SERVICES = [
  { id: "seo-dashboard", shortLabel: "SEO", label: "SEO Dashboards", icon: "chart", description: "Build monthly SEO roadmaps directly in the hub and export them to Sheets when needed." },
  { id: "ppc", shortLabel: "PPC", label: "PPC", icon: "search", description: "Search ads, Shopping, Performance Max, and PPC reporting." },
  { id: "social", shortLabel: "Social", label: "Social", icon: "megaphone", description: "Organic social, paid social, creative plans, and campaign actions." },
  { id: "dmm", shortLabel: "DMM", label: "DMM", icon: "target", description: "Digital marketing management plans, retained work, and recurring actions." },
  { id: "content", shortLabel: "Content", label: "Content", icon: "content", description: "Briefs, calendars, drafts, and content production tasks." }
];

const TASK_STATUSES = [
  { id: "backlog", label: "Backlog" },
  { id: "in-progress", label: "In progress" },
  { id: "review", label: "Review" },
  { id: "waiting", label: "Waiting" },
  { id: "done", label: "Done" }
];

const KANBAN_COLUMNS = TASK_STATUSES.filter((status) => ["backlog", "in-progress", "review", "done"].includes(status.id));

const CLIENT_LIFECYCLES = [
  { id: "active", label: "Active" },
  { id: "paused", label: "Paused" },
  { id: "prospect", label: "Prospect" },
  { id: "churned", label: "Churned" }
];

const EMPLOYMENT_TYPES = ["Full time", "Part time", "Contractor", "Freelance"];

const SEEDED_CLIENTS = [
  { id: "yotspot", name: "Yotspot" },
  { id: "contemporary-garden-rooms", name: "Contemporary Garden Rooms" },
  { id: "btw-north", name: "BTW North" },
  { id: "howgill-lodge", name: "Howgill Lodge" },
  { id: "catgill-farm", name: "Catgill Farm" },
  { id: "ipull-upull-canada", name: "iPull-uPull Canada" },
  { id: "ipull-upull", name: "iPull-uPull" },
  { id: "maf-associates", name: "MAF Associates" },
  { id: "period-mouldings", name: "Period Mouldings" },
  { id: "smart-cells", name: "Smart Cells" },
  { id: "ropers-leisure", name: "Ropers Leisure" },
  { id: "schofields-insurance", name: "Schofields Insurance" },
  { id: "send-my-bag", name: "Send My Bag" }
];

const STORAGE_PREFIX = "agency-hub-react-v1";
const storageKeys = {
  month: `${STORAGE_PREFIX}:month`,
  hideDone: `${STORAGE_PREFIX}:hide-done`,
  clients: `${STORAGE_PREFIX}:clients`,
  profiles: `${STORAGE_PREFIX}:profiles`,
  links: `${STORAGE_PREFIX}:links`,
  team: `${STORAGE_PREFIX}:team`,
  time: `${STORAGE_PREFIX}:time`,
  jobs: `${STORAGE_PREFIX}:jobs`,
  taskTemplates: `${STORAGE_PREFIX}:task-templates`,
  subtasks: `${STORAGE_PREFIX}:subtasks`,
  comments: `${STORAGE_PREFIX}:comments`,
  activities: `${STORAGE_PREFIX}:activities`,
  sops: `${STORAGE_PREFIX}:sops`,
  shares: `${STORAGE_PREFIX}:shares`,
  audit: `${STORAGE_PREFIX}:audit`
};

const clientIcons = {
  yotspot: "send",
  "contemporary-garden-rooms": "grid",
  "btw-north": "chart",
  "howgill-lodge": "cloud",
  "catgill-farm": "cloud",
  "ipull-upull-canada": "grid",
  "ipull-upull": "grid",
  "maf-associates": "report",
  "period-mouldings": "grid",
  "smart-cells": "chart",
  "ropers-leisure": "send",
  "schofields-insurance": "shield",
  "send-my-bag": "send"
};

const clientTones = ["blue", "green", "orange", "purple", "pink", "teal", "slate"];

function App() {
  const [route, setRoute] = useState(routeFromHash);
  const [selectedMonth, setSelectedMonth] = useState(loadState(storageKeys.month, currentMonthName()));
  const [hideDone, setHideDone] = useState(loadState(storageKeys.hideDone, false));
  const [clients, setClients] = useState(loadState(storageKeys.clients, SEEDED_CLIENTS).map(normaliseClient));
  const [profiles, setProfiles] = useState(loadState(storageKeys.profiles, {}));
  const [resourceLinks, setResourceLinks] = useState(loadState(storageKeys.links, {}));
  const [teamMembers, setTeamMembers] = useState(loadState(storageKeys.team, []).map(normaliseTeamMember));
  const [timeEntries, setTimeEntries] = useState(loadState(storageKeys.time, []).map(normaliseTimeEntry));
  const [jobs, setJobs] = useState(loadState(storageKeys.jobs, []).map(normaliseJob));
  const [taskTemplates, setTaskTemplates] = useState(loadState(storageKeys.taskTemplates, []).map(normaliseTaskTemplate));
  const [subtasks, setSubtasks] = useState(loadState(storageKeys.subtasks, []).map(normaliseSubtask));
  const [comments, setComments] = useState(loadState(storageKeys.comments, []).map(normaliseComment));
  const [activities, setActivities] = useState(loadState(storageKeys.activities, []).map(normaliseActivity));
  const [sopArticles, setSopArticles] = useState(loadState(storageKeys.sops, []).map(normaliseSopArticle));
  const [clientShares, setClientShares] = useState(loadState(storageKeys.shares, []).map(normaliseClientShare));
  const [auditLog, setAuditLog] = useState(loadState(storageKeys.audit, []).map(normaliseAuditEntry));
  const [persistenceMode, setPersistenceMode] = useState("local");
  const [statusTime, setStatusTime] = useState(new Date().toISOString());

  useEffect(() => {
    const onHashChange = () => setRoute(routeFromHash());
    window.addEventListener("hashchange", onHashChange);
    return () => window.removeEventListener("hashchange", onHashChange);
  }, []);

  useEffect(() => {
    loadAgencyState();
  }, []);

  useEffect(() => saveState(storageKeys.month, selectedMonth), [selectedMonth]);
  useEffect(() => saveState(storageKeys.hideDone, hideDone), [hideDone]);
  useEffect(() => saveState(storageKeys.clients, clients), [clients]);
  useEffect(() => saveState(storageKeys.profiles, profiles), [profiles]);
  useEffect(() => saveState(storageKeys.links, resourceLinks), [resourceLinks]);
  useEffect(() => saveState(storageKeys.team, teamMembers), [teamMembers]);
  useEffect(() => saveState(storageKeys.time, timeEntries), [timeEntries]);
  useEffect(() => saveState(storageKeys.jobs, jobs), [jobs]);
  useEffect(() => saveState(storageKeys.taskTemplates, taskTemplates), [taskTemplates]);
  useEffect(() => saveState(storageKeys.subtasks, subtasks), [subtasks]);
  useEffect(() => saveState(storageKeys.comments, comments), [comments]);
  useEffect(() => saveState(storageKeys.activities, activities), [activities]);
  useEffect(() => saveState(storageKeys.sops, sopArticles), [sopArticles]);
  useEffect(() => saveState(storageKeys.shares, clientShares), [clientShares]);
  useEffect(() => saveState(storageKeys.audit, auditLog), [auditLog]);

  const sortedClients = useMemo(() => (
    clients
      .filter((client) => client.status === "current")
      .sort((a, b) => a.name.localeCompare(b.name))
  ), [clients]);

  const context = {
    selectedMonth,
    hideDone,
    persistenceMode,
    clients: sortedClients,
    profiles,
    resourceLinks,
    teamMembers,
    timeEntries,
    jobs,
    taskTemplates,
    subtasks,
    comments,
    activities,
    sopArticles,
    clientShares,
    auditLog,
    setSelectedMonth,
    setHideDone,
    addClient,
    updateClient,
    updateProfile,
    uploadClientLogo,
    addResourceLink,
    removeResourceLink,
    addTeamMember,
    updateTeamMember,
    removeTeamMember,
    addTimeEntry,
    removeTimeEntry,
    addJob,
    updateJob,
    removeJob,
    addTaskTemplate,
    removeTaskTemplate,
    cloneTaskTemplate,
    addSubtask,
    updateSubtask,
    removeSubtask,
    addComment,
    removeComment,
    addSopArticle,
    updateSopArticle,
    removeSopArticle,
    createClientShare,
    refreshState: loadAgencyState
  };

  const activeClient = route.view === "client"
    ? sortedClients.find((client) => client.id === route.clientId)
    : null;

  async function loadAgencyState() {
    try {
      const response = await fetch(`/api/state?ts=${Date.now()}`, { cache: "no-store" });
      const payload = await response.json();
      if (!response.ok || !payload.d1) {
        throw new Error(payload.detail || payload.message || "D1 is not available yet.");
      }

      setPersistenceMode("cloud");
      setClients((payload.clients || SEEDED_CLIENTS).map(normaliseClient));
      setProfiles(payload.clientProfiles || {});
      setResourceLinks(payload.resourceLinks || {});
      setTeamMembers((payload.teamMembers || []).map(normaliseTeamMember));
      setTimeEntries((payload.timeEntries || []).map(normaliseTimeEntry));
      setJobs((payload.jobs || []).map(normaliseJob));
      setTaskTemplates((payload.taskTemplates || []).map(normaliseTaskTemplate));
      setSubtasks((payload.subtasks || []).map(normaliseSubtask));
      setComments((payload.comments || []).map(normaliseComment));
      setActivities((payload.activities || []).map(normaliseActivity));
      setSopArticles((payload.sopArticles || []).map(normaliseSopArticle));
      setClientShares((payload.clientShares || []).map(normaliseClientShare));
      setAuditLog((payload.auditLog || []).map(normaliseAuditEntry));
      setStatusTime(payload.updatedAt || new Date().toISOString());
    } catch (error) {
      setPersistenceMode("local");
      setStatusTime(new Date().toISOString());
    }
  }

  async function sendCloudMutation(path, options = {}) {
    if (persistenceMode !== "cloud") return null;
    try {
      const response = await fetch(path, {
        method: options.method || "POST",
        headers: { "Content-Type": "application/json" },
        body: options.body ? JSON.stringify(options.body) : undefined
      });
      if (!response.ok) throw new Error(`Sync failed with ${response.status}`);
      setStatusTime(new Date().toISOString());
      return options.expectJson === false ? null : response.json();
    } catch (error) {
      setPersistenceMode("local");
      setStatusTime(new Date().toISOString());
      return null;
    }
  }

  function recordLocalActivity(payload) {
    const activity = normaliseActivity({
      id: makeId("activity"),
      ...payload,
      createdAt: new Date().toISOString()
    });
    setActivities((current) => [activity, ...current].slice(0, 300));
  }

  function recordLocalAudit(payload) {
    const audit = normaliseAuditEntry({
      id: makeId("audit"),
      ...payload,
      createdAt: new Date().toISOString()
    });
    setAuditLog((current) => [audit, ...current].slice(0, 300));
  }

  async function addClient(formData) {
    const name = cleanText(formData.get("name"));
    if (!name) return;
    const logo = await readImageData(formData.get("logo"));
    const client = normaliseClient({
      id: slugify(name),
      name,
      logo,
      status: "current",
      lifecycleStatus: cleanText(formData.get("lifecycleStatus"), "active"),
      retainerValue: Number(formData.get("retainerValue") || 0),
      renewalDate: cleanText(formData.get("renewalDate")),
      manual: true
    });
    setClients((current) => upsertById(current, client));
    if (logo) {
      setProfiles((current) => ({ ...current, [client.id]: { ...profileFor(current, client.id), logo } }));
    }
    sendCloudMutation("/api/clients", { body: client });
    recordLocalActivity({ clientId: client.id, eventType: "client.created", body: `${client.name} was added.` });
  }

  function updateClient(clientId, patch) {
    let updated = null;
    let previous = null;
    setClients((current) => current.map((client) => {
      if (client.id !== clientId) return client;
      previous = client;
      updated = normaliseClient({ ...client, ...patch });
      return updated;
    }));
    if (updated) {
      sendCloudMutation("/api/clients", { body: updated });
      recordLocalAudit({ entityType: "client", entityId: clientId, action: "client.updated", before: previous, after: updated });
      recordLocalActivity({ clientId, eventType: "client.updated", body: `${updated.name} was updated.` });
    }
  }

  function updateProfile(clientId, patch, sync = false) {
    let nextProfile = null;
    setProfiles((current) => {
      nextProfile = { ...profileFor(current, clientId), ...patch };
      return { ...current, [clientId]: nextProfile };
    });
    if (sync) {
      sendCloudMutation("/api/client-profile", {
        method: "PUT",
        body: { clientId, ...nextProfile }
      });
    }
  }

  async function uploadClientLogo(client, file) {
    const logo = await readImageData(file);
    if (!logo) return;
    updateProfile(client.id, { logo }, true);
    setClients((current) => upsertById(current, { ...client, logo }));
    sendCloudMutation("/api/clients", { body: { ...client, logo } });
  }

  function addResourceLink(clientId, sectionId, payload) {
    const link = {
      id: makeId("link"),
      title: cleanText(payload.title),
      url: normaliseUrl(payload.url),
      range: cleanText(payload.range),
      notes: cleanText(payload.notes)
    };
    if (!link.title || !link.url) return;
    setResourceLinks((current) => {
      const clientLinks = current[clientId] || {};
      return {
        ...current,
        [clientId]: {
          ...clientLinks,
          [sectionId]: [link, ...(clientLinks[sectionId] || [])]
        }
      };
    });
    sendCloudMutation("/api/resource-links", {
      body: { clientId, sectionId, ...link }
    });
  }

  function removeResourceLink(clientId, sectionId, linkId) {
    setResourceLinks((current) => {
      const clientLinks = current[clientId] || {};
      return {
        ...current,
        [clientId]: {
          ...clientLinks,
          [sectionId]: (clientLinks[sectionId] || []).filter((link) => link.id !== linkId)
        }
      };
    });
    sendCloudMutation(`/api/resource-links/${encodeURIComponent(linkId)}`, {
      method: "DELETE",
      body: {}
    });
  }

  async function addTeamMember(formData) {
    const name = cleanText(formData.get("name"));
    if (!name) return;
    const photo = await readImageData(formData.get("photo"));
    const member = normaliseTeamMember({
      id: makeId("staff"),
      name,
      role: cleanText(formData.get("role")),
      email: cleanText(formData.get("email")),
      capacity: Number(formData.get("capacity") || 0),
      monthlyCapacity: Number(formData.get("monthlyCapacity") || 0),
      skills: cleanText(formData.get("skills")),
      employmentType: cleanText(formData.get("employmentType")),
      startDate: cleanText(formData.get("startDate")),
      photo,
      allocations: {}
    });
    setTeamMembers((current) => [member, ...current]);
    sendCloudMutation("/api/team", { body: member });
    recordLocalActivity({ staffId: member.id, eventType: "staff.created", body: `${member.name} was added to the team.` });
  }

  function updateTeamMember(memberId, patch) {
    let updated = null;
    let previous = null;
    setTeamMembers((current) => current.map((member) => {
      if (member.id !== memberId) return member;
      previous = member;
      updated = normaliseTeamMember({ ...member, ...patch });
      return updated;
    }));
    if (updated) {
      sendCloudMutation("/api/team", { body: updated });
      recordLocalAudit({ entityType: "staff", entityId: memberId, action: "staff.updated", before: previous, after: updated });
    }
  }

  function removeTeamMember(memberId) {
    setTeamMembers((current) => current.filter((member) => member.id !== memberId));
    setTimeEntries((current) => current.filter((entry) => entry.memberId !== memberId));
    sendCloudMutation(`/api/team/${encodeURIComponent(memberId)}`, {
      method: "DELETE",
      body: {}
    });
  }

  function addTimeEntry(payload) {
    const entry = normaliseTimeEntry({
      id: makeId("time"),
      clientId: payload.clientId,
      memberId: payload.memberId,
      jobId: payload.jobId,
      serviceId: payload.serviceId,
      hours: Number(payload.hours || 0),
      durationMinutes: Number(payload.durationMinutes || Math.round(Number(payload.hours || 0) * 60)),
      billable: payload.billable === undefined ? true : Boolean(payload.billable),
      workType: payload.workType,
      workDate: payload.workDate || new Date().toISOString().slice(0, 10),
      notes: payload.notes,
      createdAt: new Date().toISOString()
    });
    if (!entry.clientId || !entry.memberId || !entry.hours) return;
    setTimeEntries((current) => [entry, ...current]);
    sendCloudMutation("/api/time-entries", { body: entry });
    recordLocalActivity({ clientId: entry.clientId, jobId: entry.jobId, staffId: entry.memberId, eventType: "time.logged", body: `${entry.hours} hours logged.` });
  }

  function removeTimeEntry(entryId) {
    setTimeEntries((current) => current.filter((entry) => entry.id !== entryId));
    sendCloudMutation(`/api/time-entries/${encodeURIComponent(entryId)}`, {
      method: "DELETE",
      body: {}
    });
  }

  function addJob(payload) {
    const job = normaliseJob({
      id: makeId("job"),
      clientId: payload.clientId,
      sectionId: payload.sectionId,
      month: payload.month || selectedMonth,
      groupName: payload.groupName,
      title: payload.title,
      description: payload.description,
      status: payload.status || "backlog",
      priority: payload.priority || "normal",
      ownerId: payload.ownerId,
      dueDate: payload.dueDate,
      reminderDate: payload.reminderDate,
      templateId: payload.templateId,
      sopId: payload.sopId,
      sourceType: "manual",
      sourceRef: "client-hub",
      createdAt: new Date().toISOString()
    });
    if (!job.clientId || !job.title) return;
    setJobs((current) => [job, ...current]);
    sendCloudMutation("/api/jobs", { body: job });
    recordLocalActivity({ clientId: job.clientId, jobId: job.id, staffId: job.ownerId, eventType: "task.created", body: `${job.title} was added.` });
  }

  function updateJob(jobId, patch) {
    let updated = null;
    setJobs((current) => current.map((job) => (
      job.id === jobId
        ? (updated = normaliseJob({
          ...job,
          ...patch,
          completedAt: patch.status === "done" && !job.completedAt ? new Date().toISOString() : patch.status && patch.status !== "done" ? "" : job.completedAt
        }))
        : job
    )));
    sendCloudMutation(`/api/jobs/${encodeURIComponent(jobId)}`, {
      method: "PATCH",
      body: patch
    });
    if (updated && patch.status) {
      recordLocalActivity({ clientId: updated.clientId, jobId, staffId: updated.ownerId, eventType: "task.status", body: `${updated.title} moved to ${statusLabel(updated.status)}.` });
    }
  }

  function removeJob(jobId) {
    setJobs((current) => current.filter((job) => job.id !== jobId));
    setTimeEntries((current) => current.map((entry) => (
      entry.jobId === jobId ? { ...entry, jobId: "" } : entry
    )));
    setSubtasks((current) => current.filter((subtask) => subtask.jobId !== jobId));
    setComments((current) => current.filter((comment) => comment.jobId !== jobId));
    sendCloudMutation(`/api/jobs/${encodeURIComponent(jobId)}`, {
      method: "DELETE",
      body: {}
    });
  }

  function addTaskTemplate(payload) {
    const template = normaliseTaskTemplate({
      id: makeId("template"),
      name: payload.name,
      sectionId: payload.sectionId,
      description: payload.description,
      tasks: templateTasksFromText(payload.tasksText)
    });
    if (!template.name || !template.tasks.length) return;
    setTaskTemplates((current) => [template, ...current]);
    sendCloudMutation("/api/task-templates", { body: template });
    recordLocalActivity({ eventType: "template.saved", body: `${template.name} template was saved.` });
  }

  function removeTaskTemplate(templateId) {
    setTaskTemplates((current) => current.filter((template) => template.id !== templateId));
    sendCloudMutation(`/api/task-templates/${encodeURIComponent(templateId)}`, {
      method: "DELETE",
      body: {}
    });
  }

  function cloneTaskTemplate(templateId, payload) {
    const template = taskTemplates.find((item) => item.id === templateId);
    if (!template || !payload.clientId) return;
    const created = template.tasks.map((task) => normaliseJob({
      id: makeId("job"),
      clientId: payload.clientId,
      sectionId: template.sectionId,
      month: payload.month || selectedMonth,
      groupName: task.groupName || template.name,
      title: task.title,
      description: task.description,
      status: "backlog",
      priority: task.priority || "normal",
      ownerId: payload.ownerId,
      dueDate: task.dueDate || dueDateFromOffset(payload.startDate, task.dueOffsetDays),
      templateId,
      sopId: task.sopId,
      sourceType: "template",
      sourceRef: template.name,
      createdAt: new Date().toISOString()
    }));
    setJobs((current) => [...created, ...current]);
    sendCloudMutation("/api/task-templates/clone", {
      body: { templateId, clientId: payload.clientId, month: payload.month || selectedMonth, ownerId: payload.ownerId, startDate: payload.startDate }
    });
    recordLocalActivity({ clientId: payload.clientId, eventType: "template.cloned", body: `${template.name} cloned into ${payload.month || selectedMonth}.` });
  }

  function addSubtask(jobId, label) {
    const subtask = normaliseSubtask({
      id: makeId("subtask"),
      jobId,
      label,
      createdAt: new Date().toISOString()
    });
    if (!subtask.jobId || !subtask.label) return;
    setSubtasks((current) => [...current, subtask]);
    sendCloudMutation("/api/subtasks", { body: subtask });
  }

  function updateSubtask(subtaskId, patch) {
    let updated = null;
    setSubtasks((current) => current.map((subtask) => {
      if (subtask.id !== subtaskId) return subtask;
      updated = normaliseSubtask({ ...subtask, ...patch });
      return updated;
    }));
    if (updated) {
      sendCloudMutation(`/api/subtasks/${encodeURIComponent(subtaskId)}`, {
        method: "PATCH",
        body: patch
      });
    }
  }

  function removeSubtask(subtaskId) {
    setSubtasks((current) => current.filter((subtask) => subtask.id !== subtaskId));
    sendCloudMutation(`/api/subtasks/${encodeURIComponent(subtaskId)}`, {
      method: "DELETE",
      body: {}
    });
  }

  function addComment(jobId, payload) {
    const comment = normaliseComment({
      id: makeId("comment"),
      jobId,
      staffId: payload.staffId,
      body: payload.body,
      mentions: extractMentions(payload.body),
      createdAt: new Date().toISOString()
    });
    if (!comment.jobId || !comment.body) return;
    setComments((current) => [...current, comment]);
    sendCloudMutation("/api/comments", { body: comment });
    const job = jobs.find((item) => item.id === jobId);
    recordLocalActivity({ clientId: job?.clientId, jobId, staffId: comment.staffId, eventType: "comment.created", body: `Comment added on ${job?.title || "a task"}.` });
  }

  function removeComment(commentId) {
    setComments((current) => current.filter((comment) => comment.id !== commentId));
    sendCloudMutation(`/api/comments/${encodeURIComponent(commentId)}`, {
      method: "DELETE",
      body: {}
    });
  }

  function addSopArticle(payload) {
    const article = normaliseSopArticle({
      id: makeId("sop"),
      title: payload.title,
      body: payload.body,
      tags: payload.tags,
      linkedService: payload.linkedService,
      createdAt: new Date().toISOString()
    });
    if (!article.title) return;
    setSopArticles((current) => [article, ...current]);
    sendCloudMutation("/api/sops", { body: article });
    recordLocalActivity({ eventType: "sop.saved", body: `${article.title} SOP was saved.` });
  }

  function updateSopArticle(articleId, patch) {
    let updated = null;
    setSopArticles((current) => current.map((article) => {
      if (article.id !== articleId) return article;
      updated = normaliseSopArticle({ ...article, ...patch });
      return updated;
    }));
    if (updated) {
      sendCloudMutation(`/api/sops/${encodeURIComponent(articleId)}`, {
        method: "PATCH",
        body: patch
      });
    }
  }

  function removeSopArticle(articleId) {
    setSopArticles((current) => current.filter((article) => article.id !== articleId));
    setJobs((current) => current.map((job) => (job.sopId === articleId ? { ...job, sopId: "" } : job)));
    sendCloudMutation(`/api/sops/${encodeURIComponent(articleId)}`, {
      method: "DELETE",
      body: {}
    });
  }

  async function createClientShare(clientId) {
    const localShare = normaliseClientShare({
      id: makeId("share"),
      clientId,
      token: makeId("public").replace(/^public_/, ""),
      active: true,
      createdAt: new Date().toISOString()
    });
    setClientShares((current) => [localShare, ...current.filter((share) => share.clientId !== clientId)]);
    const payload = await sendCloudMutation("/api/client-shares", { body: localShare });
    if (payload?.clientShare) {
      const share = normaliseClientShare(payload.clientShare);
      setClientShares((current) => [share, ...current.filter((item) => item.id !== share.id && item.clientId !== share.clientId)]);
      return share;
    }
    recordLocalActivity({ clientId, eventType: "share.created", body: "A read-only client share link was created." });
    return localShare;
  }

  return (
    <HubContext.Provider value={context}>
      <div className="app-frame react-app">
        <Sidebar route={route} />
        <main className="shell">
          <Topbar
            route={route}
            activeClient={activeClient}
            clients={sortedClients}
            selectedMonth={selectedMonth}
            setSelectedMonth={setSelectedMonth}
            hideDone={hideDone}
            setHideDone={setHideDone}
            refreshState={loadAgencyState}
          />
          <section className="status-row" aria-live="polite">
            <span className={`pill ${persistenceMode === "cloud" ? "live" : "snapshot"}`}>
              <Icon name="calendar" small />
              {persistenceMode === "cloud" ? "Hub data synced" : "Local hub data"}
              <strong>{formatDateTime(statusTime)}</strong>
            </span>
            <span className="status-copy">{selectedMonth} workspace across {sortedClients.length} clients.</span>
          </section>

          {route.view === "share" ? (
            <ShareView token={route.token} selectedMonth={selectedMonth} />
          ) : activeClient ? (
            <ClientHub client={activeClient} route={route} />
          ) : route.view === "staff" ? (
            <StaffWorkspace route={route} />
          ) : route.view === "tasks" ? (
            <TasksPage mode={route.sectionId || "list"} />
          ) : route.view === "templates" ? (
            <TemplatesPage />
          ) : route.view === "knowledge" ? (
            <KnowledgePage />
          ) : route.view === "activity" ? (
            <ActivityPage />
          ) : (
            <Overview />
          )}

          <footer className="footer">
            <span>
              <Icon name={persistenceMode === "cloud" ? "cloud" : "report"} small />
              {persistenceMode === "cloud" ? "Shared database sync is active." : "Using this browser until Cloudflare D1 is connected."}
            </span>
            <button className="link-button" type="button" onClick={() => window.print()}>Print</button>
          </footer>
        </main>
      </div>
    </HubContext.Provider>
  );
}

const HubContext = React.createContext(null);

function useHub() {
  return React.useContext(HubContext);
}

function Sidebar({ route }) {
  const active = route.view === "client" ? "clients" : route.view;
  return (
    <aside className="sidebar" aria-label="Agency navigation">
      <a className="brand" href="#overview">
        <span>AH</span>
        <strong>Agency Hub</strong>
      </a>
      <nav className="side-nav">
        <span className="nav-title">Workspace</span>
        <a className={active === "overview" ? "active" : ""} href="#overview">Overview</a>
        <a className={active === "clients" ? "active" : ""} href="#clients">Clients</a>
        <a className={active === "tasks" && route.sectionId !== "board" ? "active" : ""} href="#tasks">Tasks</a>
        <a className={active === "tasks" && route.sectionId === "board" ? "active" : ""} href="#tasks/board">Kanban</a>
        <span className="nav-title">People</span>
        <a className={active === "staff" && route.sectionId !== "capacity" ? "active" : ""} href="#staff">Staff</a>
        <a className={route.view === "staff" && route.sectionId === "capacity" ? "active" : ""} href="#staff/capacity">Capacity</a>
        <span className="nav-title">Systems</span>
        <a className={active === "templates" ? "active" : ""} href="#templates">Templates</a>
        <a className={active === "knowledge" ? "active" : ""} href="#knowledge">Knowledge</a>
        <a className={active === "activity" ? "active" : ""} href="#activity">Activity</a>
      </nav>
    </aside>
  );
}

function Topbar({ route, activeClient, clients, selectedMonth, setSelectedMonth, hideDone, setHideDone, refreshState }) {
  const monthIndex = MONTHS.indexOf(selectedMonth);
  const sectionId = route.sectionId || "seo-dashboard";

  function changeClient(event) {
    const clientId = event.target.value;
    window.location.hash = clientId ? clientPath(clientId, sectionId) : "#overview";
  }

  return (
    <header className="topbar">
      <div>
        <p className="eyebrow">Client dashboard</p>
        <h1>Agency Client Hub</h1>
        <nav className="view-nav" aria-label="Dashboard view">
          <a id="overviewLink" className={route.view === "overview" ? "active" : ""} href="#overview">All clients</a>
          {activeClient ? <span id="activeClientLabel">/ {activeClient.name}</span> : null}
        </nav>
      </div>
      <div className="actions" aria-label="Hub actions">
        {activeClient ? (
          <select className="select client-select" value={activeClient.id} onChange={changeClient} aria-label="Select client">
            {clients.map((client) => <option key={client.id} value={client.id}>{client.name}</option>)}
          </select>
        ) : null}
        <button className="btn" type="button" disabled={monthIndex <= 0} onClick={() => setSelectedMonth(MONTHS[Math.max(monthIndex - 1, 0)])}>Prev</button>
        <select className="select" value={selectedMonth} onChange={(event) => setSelectedMonth(event.target.value)} aria-label="Roadmap month">
          {MONTHS.map((month) => <option key={month} value={month}>{month}</option>)}
        </select>
        <button className="btn" type="button" disabled={monthIndex >= MONTHS.length - 1} onClick={() => setSelectedMonth(MONTHS[Math.min(monthIndex + 1, MONTHS.length - 1)])}>Next</button>
        <button className="btn" type="button" onClick={refreshState}>Refresh</button>
        <button className={`btn ${hideDone ? "active" : ""}`} type="button" onClick={() => setHideDone(!hideDone)}>Hide done</button>
        <button className="btn btn-print" type="button" onClick={() => window.print()}>
          <Icon name="print" />
          Print
        </button>
      </div>
    </header>
  );
}

function Overview() {
  const hub = useHub();
  const totals = allSeoTotals(hub.clients, hub.jobs, hub.selectedMonth);
  const openActions = Math.max(totals.total - totals.done, 0);
  const openTasks = openJobs(hub.jobs, hub.selectedMonth).length;
  const overdueTasks = overdueJobs(hub.jobs);
  const trackedHours = totalTrackedHours(hub.timeEntries, hub.selectedMonth);
  const monthlyCapacity = hub.teamMembers.reduce((sum, member) => sum + Number(member.monthlyCapacity || 0), 0);

  async function handleClientSubmit(event) {
    event.preventDefault();
    await hub.addClient(new FormData(event.currentTarget));
    event.currentTarget.reset();
  }

  return (
    <>
      <section className="ops-panel">
        <section className="ops-hero" id="overview">
          <div>
            <p className="eyebrow">Agency operations</p>
            <h2>Clients and delivery status in one workspace.</h2>
            <p className="ops-hero-copy">Open a client hub to add monthly roadmaps, manage service tasks, log time, and keep capacity visible.</p>
            <a className="source source-secondary ops-hero-link" href="#staff">Open staff workspace <Icon name="arrow" small /></a>
          </div>
          <div className="ops-metrics">
            <article><strong>{hub.clients.length}</strong><span>Current clients</span></article>
            <article><strong>{openActions}</strong><span>Open {hub.selectedMonth} SEO roadmap tasks</span></article>
            <article><strong>{openTasks}</strong><span>Open {hub.selectedMonth} tasks</span></article>
            <article className={overdueTasks.length ? "metric-alert" : ""}><strong>{overdueTasks.length}</strong><span>Overdue tasks</span></article>
            <article><strong>{resourceLinkCount(hub.resourceLinks)}</strong><span>Linked trackers</span></article>
            <article><strong>{trackedHours}/{monthlyCapacity || 0}</strong><span>{hub.selectedMonth} tracked/capacity hrs</span></article>
          </div>
        </section>

        {overdueTasks.length ? <OverdueNotice jobs={overdueTasks.slice(0, 5)} /> : null}

        <section className="add-client-card">
          <div>
            <h2>Add client</h2>
            <p>Add non-SEO or new clients and manage their work directly in the hub.</p>
          </div>
            <form className="client-add-form" onSubmit={handleClientSubmit}>
              <input name="name" type="text" placeholder="Client name" required />
              <select name="lifecycleStatus" defaultValue="active" aria-label="Lifecycle status">
                {CLIENT_LIFECYCLES.map((status) => <option key={status.id} value={status.id}>{status.label}</option>)}
              </select>
              <input name="retainerValue" type="number" min="0" step="1" placeholder="Monthly retainer" />
              <input name="renewalDate" type="date" aria-label="Renewal date" />
              <label className="file-field">
              <Icon name="camera" small />
              <span>Logo</span>
              <input name="logo" type="file" accept="image/*" />
            </label>
            <button className="btn btn-compact" type="submit"><Icon name="plus" small /> Add client</button>
          </form>
        </section>
      </section>

      <section className="summary-grid" id="clients" aria-label="Client overview">
        {hub.clients.map((client) => <ClientOverviewCard key={client.id} client={client} />)}
      </section>
    </>
  );
}

function OverdueNotice({ jobs }) {
  const { clients } = useHub();
  return (
    <section className="notice-card">
      <div>
        <h2>{jobs.length} overdue task{jobs.length === 1 ? "" : "s"}</h2>
        <p>Tasks past their deadline and not marked complete.</p>
      </div>
      <div className="notice-list">
        {jobs.map((job) => {
          const client = clients.find((item) => item.id === job.clientId);
          return (
            <a key={job.id} href={clientPath(job.clientId, job.sectionId)}>
              <strong>{job.title}</strong>
              <span>{client?.name || "Client"} - due {formatShortDate(job.dueDate)}</span>
            </a>
          );
        })}
      </div>
    </section>
  );
}

function ClientOverviewCard({ client }) {
  const hub = useHub();
  const taskSummary = clientTaskSummary(client, hub.jobs, hub.selectedMonth);
  const pct = percentage(taskSummary.done, taskSummary.total);
  const allocated = allocatedHoursForClient(hub.teamMembers, client.id);
  const used = hoursForClient(hub.timeEntries, client.id, hub.selectedMonth);
  const overdue = overdueJobs(hub.jobs, client.id).length;
  const tone = toneForClient(client.id, hub.clients);
  const health = clientHealth(client, hub.jobs, hub.timeEntries, hub.teamMembers, hub.selectedMonth);

  return (
    <article className="client-overview-card" data-tone={tone} style={clientToneStyle(tone)}>
      <div className="client-overview-main">
        <a className="client-overview-title" href={clientPath(client.id)}>
          <ClientLogo client={client} />
          <span>
            <strong>{client.name}</strong>
            <small>{taskSummary.done}/{taskSummary.total} tasks completed - {used}/{allocated} hrs used</small>
          </span>
        </a>
          <div className="client-overview-actions">
            <span className={`health-pill ${health.level}`}>{health.label}</span>
            {overdue ? <span className="overdue-pill">{overdue} overdue</span> : <span className="ok-pill">No overdue tasks</span>}
            <a className="source source-secondary" href={clientPath(client.id)}>Open hub <Icon name="arrow" small /></a>
          </div>
      </div>
      <div className="client-overview-progress">
        <strong>{pct}%</strong>
        <div className="progress" aria-hidden="true"><i style={{ width: `${pct}%` }} /></div>
        <span>{allocated - used} hrs remaining</span>
      </div>
      <div className="client-service-grid">
        {SERVICES.map((service) => <ServiceChip key={service.id} client={client} service={service} />)}
      </div>
      <div className="client-team-row">
        <span>Assigned staff</span>
        <div>{assignedMembersForClient(hub.teamMembers, client.id).length ? assignedMembersForClient(hub.teamMembers, client.id).map((member) => <span className="team-pill" key={member.id}>{member.name}</span>) : <span className="muted-mini">No staff assigned</span>}</div>
      </div>
    </article>
  );
}

function ServiceChip({ client, service }) {
  const { teamMembers, timeEntries, selectedMonth } = useHub();
  const allocated = allocatedHoursForClient(teamMembers, client.id, service.id);
  const used = hoursForClientService(timeEntries, client.id, service.id, selectedMonth);
  const pct = allocated ? Math.min(percentage(used, allocated), 100) : 0;
  return (
    <article className="service-chip">
      <div>
        <strong>{service.shortLabel}</strong>
        <span>{used}/{allocated} hrs - {allocated - used} left</span>
      </div>
      <div className="progress" aria-hidden="true"><i style={{ width: `${pct}%` }} /></div>
    </article>
  );
}

function ClientHub({ client, route }) {
  const hub = useHub();
  const profile = profileFor(hub.profiles, client.id);
  const totals = seoTotals(client.id, hub.jobs, hub.selectedMonth);
  const pct = percentage(totals.done, totals.total);
  const activeSection = route.sectionId || "seo-dashboard";
  const tone = toneForClient(client.id, hub.clients);
  const health = clientHealth(client, hub.jobs, hub.timeEntries, hub.teamMembers, hub.selectedMonth);
  const currentShare = hub.clientShares.find((share) => share.clientId === client.id && share.active);
  const [shareUrl, setShareUrl] = useState(currentShare ? `${window.location.origin}/#share/${currentShare.token}` : "");

  useEffect(() => {
    if (!route.sectionId) return;
    const sectionNode = document.getElementById(activeSection);
    if (!sectionNode) return;
    const prefersReducedMotion = window.matchMedia("(prefers-reduced-motion: reduce)").matches;
    window.requestAnimationFrame(() => sectionNode.scrollIntoView({
      block: "start",
      behavior: prefersReducedMotion ? "auto" : "smooth"
    }));
  }, [activeSection, client.id, route.sectionId]);

  useEffect(() => {
    setShareUrl(currentShare ? `${window.location.origin}/#share/${currentShare.token}` : "");
  }, [currentShare?.token]);

  async function handleLogoChange(event) {
    try {
      await hub.uploadClientLogo(client, event.target.files?.[0]);
    } catch (error) {
      window.alert(error.message);
    }
  }

  function submitClientMeta(event) {
    event.preventDefault();
    const formData = new FormData(event.currentTarget);
    hub.updateClient(client.id, {
      lifecycleStatus: cleanText(formData.get("lifecycleStatus"), "active"),
      retainerValue: Number(formData.get("retainerValue") || 0),
      renewalDate: cleanText(formData.get("renewalDate"))
    });
  }

  function submitContact(event) {
    event.preventDefault();
    const formData = new FormData(event.currentTarget);
    const contact = {
      id: makeId("contact"),
      name: cleanText(formData.get("name")),
      role: cleanText(formData.get("role")),
      email: cleanText(formData.get("email")),
      phone: cleanText(formData.get("phone"))
    };
    if (!contact.name && !contact.email) return;
    hub.updateProfile(client.id, {
      contactsList: [contact, ...(profile.contactsList || [])]
    }, true);
    event.currentTarget.reset();
  }

  function removeContact(contactId) {
    hub.updateProfile(client.id, {
      contactsList: (profile.contactsList || []).filter((contact) => contact.id !== contactId)
    }, true);
  }

  async function createShareLink() {
    const share = await hub.createClientShare(client.id);
    if (share?.token) setShareUrl(`${window.location.origin}/#share/${share.token}`);
  }

  return (
    <section className="client-hub">
      <section className="hub-hero" data-tone={tone} style={clientToneStyle(tone)}>
        <div className="hub-profile">
          <div className="hub-company">
            <div>
              <ClientLogo client={client} large />
              <label className="file-field logo-upload">
                <Icon name="camera" small />
                <span>Logo</span>
                <input type="file" accept="image/*" onChange={handleLogoChange} />
              </label>
            </div>
            <div>
              <span className="field-label">Company Name</span>
              <h2>{client.name}</h2>
              <div className={`health-pill ${health.level}`}><span>{health.label}</span>{health.reason}</div>
              <form className="client-meta-form" onSubmit={submitClientMeta}>
                <select name="lifecycleStatus" defaultValue={client.lifecycleStatus || "active"} aria-label="Client status">
                  {CLIENT_LIFECYCLES.map((status) => <option key={status.id} value={status.id}>{status.label}</option>)}
                </select>
                <input name="retainerValue" type="number" min="0" step="1" defaultValue={client.retainerValue || ""} placeholder="Monthly retainer" />
                <input name="renewalDate" type="date" defaultValue={client.renewalDate || ""} aria-label="Renewal date" />
                <button className="btn btn-compact" type="submit">Save</button>
              </form>
            </div>
          </div>
          <div className="client-notes-stack">
            <label className="field-block notes-field">
              <span>Client Notes</span>
              <textarea
                rows="6"
                placeholder="Add retained knowledge, access notes, preferences, recurring priorities, or reporting context"
                value={profile.notes || ""}
                onChange={(event) => hub.updateProfile(client.id, { notes: event.target.value })}
                onBlur={(event) => hub.updateProfile(client.id, { notes: event.target.value }, true)}
              />
            </label>
            <div className="share-box">
              <button className="source source-secondary" type="button" onClick={createShareLink}>Create read-only progress link</button>
              {shareUrl ? <input value={shareUrl} readOnly aria-label="Read-only share link" /> : null}
            </div>
          </div>
        </div>
        <section className="contacts-panel">
          <div className="staff-section-head compact-head">
            <div>
              <h3>Contacts</h3>
              <p>Main day-to-day people for this client.</p>
            </div>
          </div>
          <form className="contact-form" onSubmit={submitContact}>
            <input name="name" type="text" placeholder="Name" />
            <input name="role" type="text" placeholder="Role" />
            <input name="email" type="email" placeholder="Email" />
            <input name="phone" type="text" placeholder="Phone" />
            <button className="btn btn-compact" type="submit"><Icon name="plus" small /> Add</button>
          </form>
          <div className="contact-list">
            {(profile.contactsList || []).length ? profile.contactsList.map((contact) => (
              <article key={contact.id}>
                <strong>{contact.name || contact.email}</strong>
                <span>{[contact.role, contact.email, contact.phone].filter(Boolean).join(" - ")}</span>
                <button className="icon-button" type="button" onClick={() => removeContact(contact.id)} aria-label="Remove contact"><Icon name="close" small /></button>
              </article>
            )) : <p className="empty-note">No structured contacts added yet.</p>}
          </div>
          <label className="field-block legacy-contact-field">
            <span>Legacy contact notes</span>
            <textarea
              rows="2"
              placeholder="Quick contact notes"
              value={profile.contacts || ""}
              onChange={(event) => hub.updateProfile(client.id, { contacts: event.target.value })}
              onBlur={(event) => hub.updateProfile(client.id, { contacts: event.target.value }, true)}
            />
          </label>
        </section>
        <div className="hub-stats">
          <div>
            <strong className="hub-percent">{pct}%</strong>
            <span className="hub-count">{totals.done}/{totals.total} {hub.selectedMonth} SEO roadmap tasks done</span>
          </div>
          <div className="progress" aria-hidden="true"><i style={{ width: `${pct}%` }} /></div>
        </div>
      </section>

      <nav className="section-tabs" aria-label={`${client.name} sections`}>
        {SERVICES.map((section) => (
          <a key={section.id} href={clientPath(client.id, section.id)} className={section.id === activeSection ? "active" : ""}>
            <Icon name={section.icon} small />
            {section.label}
          </a>
        ))}
      </nav>

      <div className="hub-sections">
        {SERVICES.map((section) => (
          <ServiceSection key={section.id} client={client} section={section} />
        ))}
      </div>
    </section>
  );
}

function ServiceSection({ client, section }) {
  return (
    <section className="hub-section" id={section.id} data-section={section.id}>
      <div className="section-head">
        <div>
          <h2><Icon name={section.icon} />{section.label}</h2>
          <p>{section.description}</p>
        </div>
        <a className="source source-secondary" href={exportUrl(section.id === "seo-dashboard" ? "roadmap" : "jobs", client.id, section.id)}>
          Export {section.id === "seo-dashboard" ? "roadmap" : "tasks"}
        </a>
      </div>
      <TaskWorkspace client={client} section={section} />
      {section.id !== "seo-dashboard" ? <ResourceWorkspace client={client} section={section} /> : null}
    </section>
  );
}

function TaskWorkspace({ client, section }) {
  const hub = useHub();
  const tasks = jobsFor(hub.jobs, client.id, section.id, hub.selectedMonth);
  const openCount = tasks.filter((job) => job.status !== "done").length;

  function submitTask(event) {
    event.preventDefault();
    const formData = new FormData(event.currentTarget);
    hub.addJob({
      clientId: client.id,
      sectionId: section.id,
      month: hub.selectedMonth,
        groupName: cleanText(formData.get("groupName")) || (section.id === "seo-dashboard" ? "Roadmap" : ""),
        title: cleanText(formData.get("title")),
        ownerId: cleanText(formData.get("ownerId")),
        status: cleanText(formData.get("status"), "backlog"),
        priority: cleanText(formData.get("priority"), "normal"),
        dueDate: cleanText(formData.get("dueDate")),
        reminderDate: cleanText(formData.get("reminderDate")),
        sopId: cleanText(formData.get("sopId")),
        description: cleanText(formData.get("description"))
      });
    event.currentTarget.reset();
  }

  return (
    <div className={`section-jobs ${section.id === "seo-dashboard" ? "roadmap-builder" : ""}`}>
      <div className="section-jobs-head">
        <div>
          <strong>{openCount} open tasks</strong>
          <span>{tasks.length} total in {hub.selectedMonth} {section.label}</span>
        </div>
        <a className="link-button" href={exportUrl(section.id === "seo-dashboard" ? "roadmap" : "jobs", client.id, section.id)}>Export tasks</a>
      </div>
      <form className="section-task-form" onSubmit={submitTask}>
        <input name="title" type="text" placeholder={`Add ${section.id === "seo-dashboard" ? "roadmap task" : `${section.label} task`}`} required />
        <input name="groupName" type="text" placeholder={section.id === "seo-dashboard" ? "Roadmap group" : "Workstream"} />
          <select name="ownerId" aria-label="Task owner">
            <option value="">Owner</option>
            {hub.teamMembers.map((member) => <option key={member.id} value={member.id}>{member.name}</option>)}
          </select>
          <select name="status" aria-label="Task status" defaultValue="backlog">
            {TASK_STATUSES.map((status) => <option key={status.id} value={status.id}>{status.label}</option>)}
          </select>
          <select name="priority" aria-label="Task priority">
            <option value="normal">Normal</option>
            <option value="high">High</option>
            <option value="low">Low</option>
          </select>
          <input name="dueDate" type="date" aria-label="Deadline" />
          <input name="reminderDate" type="date" aria-label="Reminder date" />
          <select name="sopId" aria-label="Linked SOP">
            <option value="">SOP</option>
            {hub.sopArticles
              .filter((article) => !article.linkedService || article.linkedService === section.id)
              .map((article) => <option key={article.id} value={article.id}>{article.title}</option>)}
          </select>
          <textarea name="description" rows="2" placeholder="Notes, brief, or context" />
        <button className="btn btn-compact" type="submit"><Icon name="plus" small /> Add task</button>
      </form>
      {section.id === "seo-dashboard" ? (
        <RoadmapTaskGroups client={client} tasks={tasks} />
      ) : (
        <TaskList tasks={tasks} />
      )}
    </div>
  );
}

function RoadmapTaskGroups({ client, tasks }) {
  const hub = useHub();
  const visibleTasks = hub.hideDone ? tasks.filter((task) => task.status !== "done") : tasks;
  if (!visibleTasks.length) {
    return <div className="groups"><section className="group"><p className="empty-note">No {hub.selectedMonth} SEO roadmap tasks yet. Add the first one above.</p></section></div>;
  }

  const grouped = visibleTasks.reduce((groups, job) => {
    const group = job.groupName || "Roadmap";
    groups[group] ||= [];
    groups[group].push(job);
    return groups;
  }, {});

  return (
    <div className="groups">
      {Object.entries(grouped).map(([group, groupJobs]) => (
        <section className="group" key={group}>
          <h3><span className="group-icon"><Icon name={groupIconName(group)} /></span>{group}</h3>
          <div className="task-list">
            {groupJobs.map((job) => <RoadmapTask key={job.id} client={client} job={job} />)}
          </div>
        </section>
      ))}
    </div>
  );
}

function RoadmapTask({ client, job }) {
  const hub = useHub();
  const owner = hub.teamMembers.find((member) => member.id === job.ownerId);
  const overdue = overdueJobs(hub.jobs, client.id).some((item) => item.id === job.id);
  const meta = [
    statusLabel(job.status),
    owner?.name || "Unassigned",
    job.dueDate ? `due ${formatShortDate(job.dueDate)}` : "",
    job.reminderDate ? `remind ${formatShortDate(job.reminderDate)}` : "",
    overdue ? "overdue" : ""
  ].filter(Boolean).join(" - ");

  return (
    <article className={`task roadmap-task ${job.status === "done" ? "done" : ""} ${overdue ? "is-overdue" : ""}`}>
      <input type="checkbox" checked={job.status === "done"} onChange={(event) => hub.updateJob(job.id, { status: event.target.checked ? "done" : "backlog" })} aria-label={`Mark ${job.title} complete`} />
      <div>
        <strong>{job.title}</strong>
        <small>{meta}</small>
        {job.description ? <em>{job.description}</em> : null}
        <TaskDetail job={job} />
      </div>
      <div className="roadmap-task-actions">
        <button className="icon-button" type="button" onClick={() => hub.removeJob(job.id)} aria-label={`Remove ${job.title}`}><Icon name="close" small /></button>
      </div>
    </article>
  );
}

function TaskList({ tasks }) {
  const hub = useHub();
  const visibleTasks = hub.hideDone ? tasks.filter((task) => task.status !== "done") : tasks;
  if (!visibleTasks.length) return <p className="empty-note">No tasks created for this month yet.</p>;
  return (
    <div className="section-jobs-list">
      {visibleTasks.map((job) => {
          const owner = hub.teamMembers.find((member) => member.id === job.ownerId);
          const client = hub.clients.find((item) => item.id === job.clientId);
          const overdue = overdueJobs(hub.jobs, job.clientId).some((item) => item.id === job.id);
          return (
          <article className={`section-job ${overdue ? "is-overdue" : ""}`} key={job.id}>
            <div>
              <strong>{job.title}</strong>
                <span>{[client?.name, serviceLabel(job.sectionId), statusLabel(job.status), owner?.name || "Unassigned"].filter(Boolean).join(" - ")}{job.dueDate ? ` - due ${formatShortDate(job.dueDate)}` : ""}{overdue ? " - overdue" : ""}</span>
                {job.description ? <p>{job.description}</p> : null}
                <TaskDetail job={job} />
              </div>
            <div className="job-actions">
                <select value={job.status} onChange={(event) => hub.updateJob(job.id, { status: event.target.value })} aria-label="Update task status">
                  {TASK_STATUSES.map((status) => <option key={status.id} value={status.id}>{status.label}</option>)}
                </select>
              <button className="icon-button" type="button" onClick={() => hub.removeJob(job.id)} aria-label="Remove task"><Icon name="close" small /></button>
            </div>
            </article>
          );
        })}
      </div>
    );
  }

function TaskDetail({ job }) {
  const hub = useHub();
  const [showDetails, setShowDetails] = useState(false);
  const taskSubtasks = hub.subtasks.filter((subtask) => subtask.jobId === job.id);
  const taskComments = hub.comments.filter((comment) => comment.jobId === job.id);
  const sop = hub.sopArticles.find((article) => article.id === job.sopId);

  function submitSubtask(event) {
    event.preventDefault();
    const formData = new FormData(event.currentTarget);
    hub.addSubtask(job.id, formData.get("label"));
    event.currentTarget.reset();
  }

  function submitComment(event) {
    event.preventDefault();
    const formData = new FormData(event.currentTarget);
    hub.addComment(job.id, {
      staffId: formData.get("staffId"),
      body: formData.get("body")
    });
    event.currentTarget.reset();
  }

  return (
    <div className="task-detail">
      <button className="link-button" type="button" onClick={() => setShowDetails(!showDetails)}>
        {showDetails ? "Hide details" : `Details (${taskSubtasks.length} subtasks, ${taskComments.length} comments)`}
      </button>
      {showDetails ? (
        <div className="task-detail-panel">
          {sop ? (
            <article className="sop-inline">
              <strong>{sop.title}</strong>
              <span>{sop.tags || serviceLabel(sop.linkedService)}</span>
            </article>
          ) : null}
          <div className="subtask-list">
            {taskSubtasks.map((subtask) => (
              <label key={subtask.id}>
                <input type="checkbox" checked={subtask.done} onChange={(event) => hub.updateSubtask(subtask.id, { done: event.target.checked })} />
                <span>{subtask.label}</span>
                <button className="icon-button" type="button" onClick={() => hub.removeSubtask(subtask.id)} aria-label="Remove subtask"><Icon name="close" small /></button>
              </label>
            ))}
          </div>
          <form className="mini-form" onSubmit={submitSubtask}>
            <input name="label" type="text" placeholder="Add subtask" required />
            <button className="btn btn-compact" type="submit"><Icon name="plus" small /> Add</button>
          </form>
          <div className="comment-list">
            {taskComments.map((comment) => {
              const staff = hub.teamMembers.find((member) => member.id === comment.staffId);
              return (
                <article key={comment.id}>
                  <strong>{staff?.name || "Team"}</strong>
                  <span>{formatDateTime(comment.createdAt)}</span>
                  <p>{comment.body}</p>
                  <button className="icon-button" type="button" onClick={() => hub.removeComment(comment.id)} aria-label="Remove comment"><Icon name="close" small /></button>
                </article>
              );
            })}
          </div>
          <form className="comment-form" onSubmit={submitComment}>
            <select name="staffId" aria-label="Comment author">
              <option value="">Author</option>
              {hub.teamMembers.map((member) => <option key={member.id} value={member.id}>{member.name}</option>)}
            </select>
            <textarea name="body" rows="2" placeholder="Add comment, use @name for a mention" required />
            <button className="btn btn-compact" type="submit">Comment</button>
          </form>
        </div>
      ) : null}
    </div>
  );
}

function ResourceWorkspace({ client, section }) {
  const hub = useHub();
  const links = hub.resourceLinks[client.id]?.[section.id] || [];
  const [activePreviewId, setActivePreviewId] = useState("");
  const [previewState, setPreviewState] = useState({ status: "idle", payload: null, message: "" });

  function submitLink(event) {
    event.preventDefault();
    const formData = new FormData(event.currentTarget);
    hub.addResourceLink(client.id, section.id, {
      title: formData.get("title"),
      url: formData.get("url"),
      range: formData.get("range"),
      notes: formData.get("notes")
    });
    event.currentTarget.reset();
  }

  async function loadTrackerPreview(link) {
    if (activePreviewId === link.id && previewState.status === "ready") {
      setActivePreviewId("");
      setPreviewState({ status: "idle", payload: null, message: "" });
      return;
    }

    setActivePreviewId(link.id);
    setPreviewState({ status: "loading", payload: null, message: "" });

    try {
      const endpoint = new URL("/api/tracker", window.location.origin);
      endpoint.searchParams.set("url", link.url);
      if (link.range) endpoint.searchParams.set("range", link.range);
      const response = await fetch(endpoint.toString(), { cache: "no-store" });
      const payload = await response.json();
      if (!response.ok) throw new Error(payload.detail || payload.error || `Tracker API returned ${response.status}`);
      setPreviewState({ status: "ready", payload, message: "" });
    } catch (error) {
      setPreviewState({ status: "error", payload: null, message: error.message });
    }
  }

  return (
    <div className="resource-layout">
      <form className="resource-form" onSubmit={submitLink}>
        <label>
          <span>Spreadsheet or doc title</span>
          <input name="title" type="text" placeholder={`${section.label} tracker`} required />
        </label>
        <label>
          <span>Link</span>
          <input name="url" type="text" inputMode="url" placeholder="https://docs.google.com/..." required />
        </label>
        <label>
          <span>Sheet range</span>
          <input name="range" type="text" placeholder="Optional: Sheet1!A1:G40" />
        </label>
        <label className="resource-notes">
          <span>Notes</span>
          <textarea name="notes" rows="3" placeholder="What this is used for, owner, cadence, or status" />
        </label>
        <button className="btn btn-compact" type="submit"><Icon name="plus" small /> Add link</button>
      </form>
      <div className="resource-list" aria-label={`${section.label} links`}>
        {links.length ? links.map((link) => (
          <article className="resource-row" key={link.id}>
            <div>
              <a href={link.url} target="_blank" rel="noreferrer">{link.title} <Icon name="external" small /></a>
              {link.range ? <span className="resource-range">{link.range}</span> : null}
              {link.notes ? <p>{link.notes}</p> : null}
              <div className="resource-actions">
                {isGoogleTrackerUrl(link.url) ? (
                  <button className="link-button tracker-button" type="button" onClick={() => loadTrackerPreview(link)}>
                    {activePreviewId === link.id && previewState.status === "loading" ? "Loading..." : activePreviewId === link.id ? "Hide data" : "Show data"}
                  </button>
                ) : (
                  <span className="tracker-hint">Preview supports Google Sheets and Google Docs links.</span>
                )}
              </div>
              {activePreviewId === link.id ? <TrackerPreview state={previewState} /> : null}
            </div>
            <button className="icon-button" type="button" onClick={() => hub.removeResourceLink(client.id, section.id, link.id)} aria-label={`Remove ${link.title}`}><Icon name="close" small /></button>
          </article>
        )) : <p className="empty-note">No {section.label} links added yet.</p>}
      </div>
    </div>
  );
}

function TrackerPreview({ state }) {
  if (state.status === "loading") return <div className="tracker-preview"><p className="tracker-loading">Loading tracker data...</p></div>;
  if (state.status === "error") return <div className="tracker-preview"><p className="tracker-error">{state.message}</p></div>;
  if (state.status !== "ready" || !state.payload) return null;

  const payload = state.payload;
  if (payload.type === "document") {
    const paragraphs = payload.paragraphs || [];
    return (
      <div className="tracker-preview tracker-document">
        <div className="tracker-title"><strong>{payload.title || "Google Doc"}</strong><span>{paragraphs.length} paragraphs shown</span></div>
        {paragraphs.length ? paragraphs.map((paragraph, index) => <p key={`${paragraph}-${index}`}>{paragraph}</p>) : <p className="empty-note">No text found in this document.</p>}
      </div>
    );
  }

  const rows = payload.values || [];
  const maxColumns = rows.reduce((max, row) => Math.max(max, row.length), 0);
  const headers = rows[0] || [];
  const bodyRows = rows.slice(1);

  return (
    <div className="tracker-preview tracker-table">
      <div className="tracker-title"><strong>{payload.title || "Google Sheet"}</strong><span>{[payload.sheetName, payload.range].filter(Boolean).join(" | ")}</span></div>
      {rows.length ? (
        <div className="tracker-table-wrap">
          <table>
            <thead>
              <tr>{Array.from({ length: maxColumns }).map((_, index) => <th key={index}>{headers[index] || `Column ${index + 1}`}</th>)}</tr>
            </thead>
            <tbody>
              {bodyRows.map((row, rowIndex) => (
                <tr key={rowIndex}>
                  {Array.from({ length: maxColumns }).map((_, columnIndex) => <td key={columnIndex}>{row[columnIndex] || ""}</td>)}
                </tr>
              ))}
            </tbody>
          </table>
        </div>
      ) : <p className="empty-note">No values found in this sheet range.</p>}
    </div>
  );
}

function TasksPage({ mode }) {
  const hub = useHub();
  const [filters, setFilters] = useState({ clientId: "", ownerId: "", sectionId: "", status: "" });
  const filteredTasks = hub.jobs.filter((job) => (
    isTaskInMonth(job, hub.selectedMonth)
    && (!filters.clientId || job.clientId === filters.clientId)
    && (!filters.ownerId || job.ownerId === filters.ownerId)
    && (!filters.sectionId || job.sectionId === filters.sectionId)
    && (!filters.status || normaliseKanbanStatus(job.status) === filters.status || job.status === filters.status)
  ));
  const openCount = filteredTasks.filter((job) => job.status !== "done").length;
  const overdueCount = overdueJobs(filteredTasks).length;

  function updateFilter(key, value) {
    setFilters((current) => ({ ...current, [key]: value }));
  }

  return (
    <section className="workspace-page">
      <section className="workspace-hero">
        <div>
          <p className="eyebrow">Task control</p>
          <h2>{mode === "board" ? "Kanban board" : "My tasks and agency workload"}</h2>
          <p>{openCount} open tasks across {filteredTasks.length} matching tasks. {overdueCount ? `${overdueCount} overdue.` : "No overdue tasks in this filter."}</p>
          <nav className="section-tabs compact-tabs">
            <a className={mode !== "board" ? "active" : ""} href="#tasks"><Icon name="report" small /> List</a>
            <a className={mode === "board" ? "active" : ""} href="#tasks/board"><Icon name="grid" small /> Board</a>
          </nav>
        </div>
        <div className="ops-metrics">
          <article><strong>{filteredTasks.length}</strong><span>Total tasks</span></article>
          <article><strong>{openCount}</strong><span>Open tasks</span></article>
          <article className={overdueCount ? "metric-alert" : ""}><strong>{overdueCount}</strong><span>Overdue</span></article>
          <article><strong>{filteredTasks.filter((job) => job.status === "done").length}</strong><span>Done</span></article>
        </div>
      </section>

      <section className="filter-bar">
        <select value={filters.clientId} onChange={(event) => updateFilter("clientId", event.target.value)} aria-label="Filter client">
          <option value="">All clients</option>
          {hub.clients.map((client) => <option key={client.id} value={client.id}>{client.name}</option>)}
        </select>
        <select value={filters.ownerId} onChange={(event) => updateFilter("ownerId", event.target.value)} aria-label="Filter owner">
          <option value="">All owners</option>
          {hub.teamMembers.map((member) => <option key={member.id} value={member.id}>{member.name}</option>)}
        </select>
        <select value={filters.sectionId} onChange={(event) => updateFilter("sectionId", event.target.value)} aria-label="Filter service">
          <option value="">All services</option>
          {SERVICES.map((service) => <option key={service.id} value={service.id}>{service.label}</option>)}
        </select>
        <select value={filters.status} onChange={(event) => updateFilter("status", event.target.value)} aria-label="Filter status">
          <option value="">All statuses</option>
          {TASK_STATUSES.map((status) => <option key={status.id} value={status.id}>{status.label}</option>)}
        </select>
        <a className="source source-secondary" href={exportUrl("jobs")}>Export tasks</a>
      </section>

      {mode === "board" ? <KanbanBoard tasks={filteredTasks} /> : <TaskList tasks={filteredTasks} />}
    </section>
  );
}

function KanbanBoard({ tasks }) {
  return (
    <section className="kanban-board" aria-label="Kanban task board">
      {KANBAN_COLUMNS.map((column) => {
        const columnTasks = tasks.filter((job) => normaliseKanbanStatus(job.status) === column.id);
        return (
          <div className="kanban-column" key={column.id}>
            <div className="kanban-column-head">
              <strong>{column.label}</strong>
              <span>{columnTasks.length}</span>
            </div>
            <div className="kanban-card-list">
              {columnTasks.length ? columnTasks.map((job) => <KanbanCard key={job.id} job={job} />) : <p className="empty-note">Nothing here.</p>}
            </div>
          </div>
        );
      })}
    </section>
  );
}

function KanbanCard({ job }) {
  const hub = useHub();
  const client = hub.clients.find((item) => item.id === job.clientId);
  const owner = hub.teamMembers.find((member) => member.id === job.ownerId);
  const overdue = overdueJobs([job]).length > 0;
  return (
    <article className={`kanban-card ${overdue ? "is-overdue" : ""}`}>
      <strong>{job.title}</strong>
      <span>{client?.name || "Client"} - {serviceLabel(job.sectionId)}</span>
      <small>{owner?.name || "Unassigned"}{job.dueDate ? ` - due ${formatShortDate(job.dueDate)}` : ""}</small>
      <select value={job.status} onChange={(event) => hub.updateJob(job.id, { status: event.target.value })} aria-label="Move task">
        {TASK_STATUSES.map((status) => <option key={status.id} value={status.id}>{status.label}</option>)}
      </select>
    </article>
  );
}

function TemplatesPage() {
  const hub = useHub();
  const [serviceFilter, setServiceFilter] = useState("");
  const templates = hub.taskTemplates.filter((template) => !serviceFilter || template.sectionId === serviceFilter);

  function submitTemplate(event) {
    event.preventDefault();
    const formData = new FormData(event.currentTarget);
    hub.addTaskTemplate({
      name: formData.get("name"),
      sectionId: formData.get("sectionId"),
      description: formData.get("description"),
      tasksText: formData.get("tasksText")
    });
    event.currentTarget.reset();
  }

  return (
    <section className="workspace-page">
      <section className="workspace-hero">
        <div>
          <p className="eyebrow">Reusable delivery</p>
          <h2>Task templates and onboarding checklists.</h2>
          <p>Create repeatable service task sets, then clone them into any client month.</p>
        </div>
        <div className="ops-metrics">
          <article><strong>{hub.taskTemplates.length}</strong><span>Templates</span></article>
          <article><strong>{hub.taskTemplates.filter((template) => template.name.toLowerCase().includes("onboarding")).length}</strong><span>Onboarding sets</span></article>
          <article><strong>{hub.jobs.filter((job) => job.templateId).length}</strong><span>Tasks from templates</span></article>
        </div>
      </section>
      <section className="template-layout">
        <form className="template-form" onSubmit={submitTemplate}>
          <h3>Create template</h3>
          <input name="name" type="text" placeholder="Template name" required />
          <select name="sectionId" defaultValue="seo-dashboard">
            {SERVICES.map((service) => <option key={service.id} value={service.id}>{service.label}</option>)}
          </select>
          <textarea name="description" rows="3" placeholder="When this template should be used" />
          <textarea name="tasksText" rows="8" placeholder={"One task per line. Optional format: Group | Task title | priority | due offset days"} required />
          <button className="btn btn-compact" type="submit"><Icon name="plus" small /> Save template</button>
        </form>
        <section className="template-list">
          <div className="filter-bar inline-filter">
            <select value={serviceFilter} onChange={(event) => setServiceFilter(event.target.value)} aria-label="Filter templates">
              <option value="">All services</option>
              {SERVICES.map((service) => <option key={service.id} value={service.id}>{service.label}</option>)}
            </select>
            <a className="source source-secondary" href="/api/export.csv?type=templates">Export templates</a>
          </div>
          {templates.length ? templates.map((template) => <TemplateCard key={template.id} template={template} />) : <p className="empty-note">No templates saved yet.</p>}
        </section>
      </section>
    </section>
  );
}

function TemplateCard({ template }) {
  const hub = useHub();

  function submitClone(event) {
    event.preventDefault();
    const formData = new FormData(event.currentTarget);
    hub.cloneTaskTemplate(template.id, {
      clientId: formData.get("clientId"),
      month: formData.get("month"),
      ownerId: formData.get("ownerId"),
      startDate: formData.get("startDate")
    });
    event.currentTarget.reset();
  }

  return (
    <article className="template-card">
      <div className="template-card-head">
        <div>
          <strong>{template.name}</strong>
          <span>{serviceLabel(template.sectionId)} - {template.tasks.length} tasks</span>
        </div>
        <button className="icon-button" type="button" onClick={() => hub.removeTaskTemplate(template.id)} aria-label="Remove template"><Icon name="close" small /></button>
      </div>
      {template.description ? <p>{template.description}</p> : null}
      <ul>
        {template.tasks.slice(0, 6).map((task, index) => <li key={`${task.title}-${index}`}>{task.groupName ? `${task.groupName}: ` : ""}{task.title}</li>)}
      </ul>
      <form className="clone-form" onSubmit={submitClone}>
        <select name="clientId" required aria-label="Clone to client">
          <option value="">Client</option>
          {hub.clients.map((client) => <option key={client.id} value={client.id}>{client.name}</option>)}
        </select>
        <select name="month" defaultValue={hub.selectedMonth} aria-label="Clone to month">
          {MONTHS.map((month) => <option key={month} value={month}>{month}</option>)}
        </select>
        <select name="ownerId" aria-label="Template owner">
          <option value="">Owner</option>
          {hub.teamMembers.map((member) => <option key={member.id} value={member.id}>{member.name}</option>)}
        </select>
        <input name="startDate" type="date" aria-label="Start date for due offsets" />
        <button className="btn btn-compact" type="submit">Clone</button>
      </form>
    </article>
  );
}

function KnowledgePage() {
  const hub = useHub();
  const [serviceFilter, setServiceFilter] = useState("");
  const articles = hub.sopArticles.filter((article) => !serviceFilter || article.linkedService === serviceFilter);

  function submitSop(event) {
    event.preventDefault();
    const formData = new FormData(event.currentTarget);
    hub.addSopArticle({
      title: formData.get("title"),
      linkedService: formData.get("linkedService"),
      tags: formData.get("tags"),
      body: formData.get("body")
    });
    event.currentTarget.reset();
  }

  return (
    <section className="workspace-page">
      <section className="workspace-hero">
        <div>
          <p className="eyebrow">Knowledge base</p>
          <h2>SOPs linked to day-to-day tasks.</h2>
          <p>Keep instructions close to the work so service delivery is easier to repeat.</p>
        </div>
        <div className="ops-metrics">
          <article><strong>{hub.sopArticles.length}</strong><span>SOP articles</span></article>
          <article><strong>{hub.jobs.filter((job) => job.sopId).length}</strong><span>Linked tasks</span></article>
        </div>
      </section>
      <section className="knowledge-layout">
        <form className="template-form" onSubmit={submitSop}>
          <h3>Add SOP</h3>
          <input name="title" type="text" placeholder="SOP title" required />
          <select name="linkedService" defaultValue="">
            <option value="">Any service</option>
            {SERVICES.map((service) => <option key={service.id} value={service.id}>{service.label}</option>)}
          </select>
          <input name="tags" type="text" placeholder="Tags, comma separated" />
          <textarea name="body" rows="10" placeholder="Process, checklist, caveats, links, examples" />
          <button className="btn btn-compact" type="submit"><Icon name="plus" small /> Save SOP</button>
        </form>
        <section className="knowledge-list">
          <div className="filter-bar inline-filter">
            <select value={serviceFilter} onChange={(event) => setServiceFilter(event.target.value)} aria-label="Filter SOPs">
              <option value="">All SOPs</option>
              {SERVICES.map((service) => <option key={service.id} value={service.id}>{service.label}</option>)}
            </select>
            <a className="source source-secondary" href="/api/export.csv?type=sops">Export SOPs</a>
          </div>
          {articles.length ? articles.map((article) => <SopCard key={article.id} article={article} />) : <p className="empty-note">No SOPs saved yet.</p>}
        </section>
      </section>
    </section>
  );
}

function SopCard({ article }) {
  const hub = useHub();
  const [editing, setEditing] = useState(false);

  function submitEdit(event) {
    event.preventDefault();
    const formData = new FormData(event.currentTarget);
    hub.updateSopArticle(article.id, {
      title: formData.get("title"),
      linkedService: formData.get("linkedService"),
      tags: formData.get("tags"),
      body: formData.get("body")
    });
    setEditing(false);
  }

  if (editing) {
    return (
      <form className="sop-card sop-edit-card" onSubmit={submitEdit}>
        <input name="title" defaultValue={article.title} />
        <select name="linkedService" defaultValue={article.linkedService || ""}>
          <option value="">Any service</option>
          {SERVICES.map((service) => <option key={service.id} value={service.id}>{service.label}</option>)}
        </select>
        <input name="tags" defaultValue={article.tags} />
        <textarea name="body" rows="8" defaultValue={article.body} />
        <div className="job-actions">
          <button className="btn btn-compact" type="submit">Save</button>
          <button className="btn btn-compact" type="button" onClick={() => setEditing(false)}>Cancel</button>
        </div>
      </form>
    );
  }

  return (
    <article className="sop-card">
      <div className="template-card-head">
        <div>
          <strong>{article.title}</strong>
          <span>{[serviceLabel(article.linkedService), article.tags].filter(Boolean).join(" - ")}</span>
        </div>
        <div className="job-actions">
          <button className="link-button" type="button" onClick={() => setEditing(true)}>Edit</button>
          <button className="icon-button" type="button" onClick={() => hub.removeSopArticle(article.id)} aria-label="Remove SOP"><Icon name="close" small /></button>
        </div>
      </div>
      {article.body ? <p>{article.body}</p> : <p className="empty-note">No SOP body yet.</p>}
    </article>
  );
}

function ActivityPage() {
  const hub = useHub();
  return (
    <section className="workspace-page">
      <section className="workspace-hero">
        <div>
          <p className="eyebrow">Collaboration</p>
          <h2>Activity feed and audit history.</h2>
          <p>Task, comment, time, template, staff, and client changes in one trace.</p>
        </div>
        <div className="ops-metrics">
          <article><strong>{hub.activities.length}</strong><span>Activity items</span></article>
          <article><strong>{hub.auditLog.length}</strong><span>Audit records</span></article>
          <article><strong>{mentionNotifications(hub.comments).length}</strong><span>Open mentions</span></article>
        </div>
      </section>
      <section className="activity-grid">
        <div className="activity-card">
          <div className="staff-section-head">
            <div>
              <h2>Activity</h2>
              <p>Recent client and task events.</p>
            </div>
            <a className="source source-secondary" href="/api/export.csv?type=activity">Export</a>
          </div>
          <ActivityList activities={hub.activities} />
        </div>
        <div className="activity-card">
          <div className="staff-section-head">
            <div>
              <h2>Audit</h2>
              <p>Higher-risk changes such as client and allocation updates.</p>
            </div>
            <a className="source source-secondary" href="/api/export.csv?type=audit">Export</a>
          </div>
          <AuditList auditLog={hub.auditLog} />
        </div>
      </section>
    </section>
  );
}

function ActivityList({ activities }) {
  const hub = useHub();
  if (!activities.length) return <p className="empty-note">No activity recorded yet.</p>;
  return (
    <div className="activity-list">
      {activities.map((activity) => {
        const client = hub.clients.find((item) => item.id === activity.clientId);
        const staff = hub.teamMembers.find((item) => item.id === activity.staffId);
        return (
          <article key={activity.id}>
            <strong>{activity.body || statusLabel(activity.eventType)}</strong>
            <span>{[client?.name, staff?.name, formatDateTime(activity.createdAt)].filter(Boolean).join(" - ")}</span>
          </article>
        );
      })}
    </div>
  );
}

function AuditList({ auditLog }) {
  if (!auditLog.length) return <p className="empty-note">No audit records yet.</p>;
  return (
    <div className="activity-list">
      {auditLog.map((entry) => (
        <article key={entry.id}>
          <strong>{entry.action}</strong>
          <span>{entry.entityType} / {entry.entityId} - {formatDateTime(entry.createdAt)}</span>
        </article>
      ))}
    </div>
  );
}

function ShareView({ token, selectedMonth }) {
  const [state, setState] = useState({ status: "loading", payload: null, message: "" });

  useEffect(() => {
    let cancelled = false;
    async function loadShare() {
      try {
        const response = await fetch(`/api/share/${encodeURIComponent(token)}?month=${encodeURIComponent(selectedMonth)}`, { cache: "no-store" });
        const payload = await response.json();
        if (!response.ok) throw new Error(payload.detail || payload.error || "Share link could not be loaded.");
        if (!cancelled) setState({ status: "ready", payload, message: "" });
      } catch (error) {
        if (!cancelled) setState({ status: "error", payload: null, message: error.message });
      }
    }
    loadShare();
    return () => {
      cancelled = true;
    };
  }, [token, selectedMonth]);

  if (state.status === "loading") return <section className="workspace-page"><p className="empty-note">Loading shared client progress...</p></section>;
  if (state.status === "error") return <section className="workspace-page"><p className="tracker-error">{state.message}</p></section>;

  const jobs = (state.payload?.jobs || []).map(normaliseJob);
  const done = jobs.filter((job) => job.status === "done").length;
  return (
    <section className="workspace-page share-page">
      <section className="workspace-hero">
        <div>
          <p className="eyebrow">Read-only progress</p>
          <h2>{state.payload.client.name}</h2>
          <p>{done}/{jobs.length} tasks complete for {selectedMonth}.</p>
        </div>
        <div className="ops-metrics">
          <article><strong>{percentage(done, jobs.length)}%</strong><span>Complete</span></article>
          <article><strong>{jobs.filter((job) => job.status !== "done").length}</strong><span>Open</span></article>
        </div>
      </section>
      <div className="section-jobs-list">
        {jobs.length ? jobs.map((job) => (
          <article className="section-job" key={job.id}>
            <div>
              <strong>{job.title}</strong>
              <span>{serviceLabel(job.sectionId)} - {statusLabel(job.status)}{job.dueDate ? ` - due ${formatShortDate(job.dueDate)}` : ""}</span>
              {job.groupName ? <p>{job.groupName}</p> : null}
            </div>
          </article>
        )) : <p className="empty-note">No shared tasks for this month yet.</p>}
      </div>
    </section>
  );
}

function StaffWorkspace({ route }) {
  const hub = useHub();
  const member = route.memberId ? hub.teamMembers.find((item) => item.id === route.memberId) : null;
  const weeklyCapacity = hub.teamMembers.reduce((sum, item) => sum + Number(item.capacity || 0), 0);
  const monthlyCapacity = hub.teamMembers.reduce((sum, item) => sum + Number(item.monthlyCapacity || 0), 0);
  const weeklyHours = totalTrackedHours(hub.timeEntries, hub.selectedMonth, "week");
  const monthlyHours = totalTrackedHours(hub.timeEntries, hub.selectedMonth);
  const utilisation = monthlyCapacity ? percentage(monthlyHours, monthlyCapacity) : 0;

  async function submitTeam(event) {
    event.preventDefault();
    await hub.addTeamMember(new FormData(event.currentTarget));
    event.currentTarget.reset();
  }

  if (member) return <StaffProfile member={member} />;

  return (
    <section className="staff-page">
      <section className="staff-hero">
        <div>
          <p className="eyebrow">Staff workspace</p>
          <h2>People, time, and capacity.</h2>
          <p>Track staff profiles, allocations, and delivery time from one place.</p>
          <nav className="section-tabs compact-tabs">
            <a href="#staff" className={route.sectionId !== "capacity" ? "active" : ""}><Icon name="users" small /> Team Members</a>
            <a href="#staff/capacity" className={route.sectionId === "capacity" ? "active" : ""}><Icon name="chart" small /> Capacity</a>
          </nav>
        </div>
        <div className="ops-metrics">
          <article><strong>{hub.teamMembers.length}</strong><span>Active staff</span></article>
          <article><strong>{weeklyHours}/{weeklyCapacity || 0}</strong><span>Week tracked/capacity</span></article>
          <article><strong>{monthlyHours}/{monthlyCapacity || 0}</strong><span>{hub.selectedMonth} tracked/capacity</span></article>
          <article><strong>{utilisation}%</strong><span>Monthly utilisation</span></article>
        </div>
      </section>

      {route.sectionId === "capacity" ? <CapacityMatrix /> : (
        <section className="staff-section" id="staff-directory">
          <div className="staff-section-head">
            <div>
              <h2>Team Members</h2>
              <p>Add people, profile photos, and their working capacity.</p>
            </div>
          </div>
          <form className="staff-add-form" onSubmit={submitTeam}>
            <input name="name" type="text" placeholder="Team member" required />
            <input name="role" type="text" placeholder="Role" />
              <input name="email" type="email" placeholder="Email" />
              <input name="capacity" type="number" min="0" step="0.25" placeholder="Weekly hrs" />
              <input name="monthlyCapacity" type="number" min="0" step="0.25" placeholder="Monthly hrs" />
              <select name="employmentType" defaultValue="" aria-label="Employment type">
                <option value="">Type</option>
                {EMPLOYMENT_TYPES.map((type) => <option key={type} value={type}>{type}</option>)}
              </select>
              <input name="startDate" type="date" aria-label="Start date" />
              <input name="skills" type="text" placeholder="Skills" />
              <label className="file-field">
              <Icon name="camera" small />
              <span>Photo</span>
              <input name="photo" type="file" accept="image/*" />
            </label>
            <button className="btn btn-compact" type="submit"><Icon name="plus" small /> Add staff</button>
          </form>
          <div className="staff-grid">
            {hub.teamMembers.length ? hub.teamMembers.map((item) => <StaffCard key={item.id} member={item} />) : <p className="empty-note">No staff added yet.</p>}
          </div>
        </section>
      )}
    </section>
  );
}

function StaffCard({ member }) {
  const hub = useHub();
  const used = hoursForMember(hub.timeEntries, member.id, hub.selectedMonth);
  const allocated = allocatedHoursForMember(member);
  const openOwned = openJobs(hub.jobs, hub.selectedMonth).filter((job) => job.ownerId === member.id).length;
  const utilisation = member.monthlyCapacity ? percentage(used, member.monthlyCapacity) : 0;
  return (
    <a className={`staff-card ${utilisation > 100 ? "is-over-capacity" : utilisation > 85 ? "is-warm-capacity" : ""}`} href={`#staff/${encodeURIComponent(member.id)}`}>
      <div className="staff-avatar">{member.photo ? <img src={member.photo} alt="" /> : <Icon name="user" />}</div>
      <div>
        <strong>{member.name}</strong>
        <span>{member.role || "Unassigned"} - {openOwned} open tasks{member.skills ? ` - ${member.skills}` : ""}</span>
      </div>
      <div className="staff-card-meta">
        <span>{used}/{member.monthlyCapacity || 0} hrs used ({utilisation}%)</span>
        <span>{allocated} hrs allocated</span>
      </div>
    </a>
  );
}

function StaffProfile({ member }) {
  const hub = useHub();
  const memberTime = hub.timeEntries.filter((entry) => entry.memberId === member.id);

  async function handlePhotoChange(event) {
    try {
      const photo = await readImageData(event.target.files?.[0]);
      if (photo) hub.updateTeamMember(member.id, { photo });
    } catch (error) {
      window.alert(error.message);
    }
  }

  function submitProfile(event) {
    event.preventDefault();
    const formData = new FormData(event.currentTarget);
    hub.updateTeamMember(member.id, {
      name: cleanText(formData.get("name")),
      role: cleanText(formData.get("role")),
        email: cleanText(formData.get("email")),
        capacity: Number(formData.get("capacity") || 0),
        monthlyCapacity: Number(formData.get("monthlyCapacity") || 0),
        skills: cleanText(formData.get("skills")),
        employmentType: cleanText(formData.get("employmentType")),
        startDate: cleanText(formData.get("startDate")),
        notes: cleanText(formData.get("notes"))
      });
    }

  function submitAbsence(event) {
    event.preventDefault();
    const formData = new FormData(event.currentTarget);
    const absence = {
      id: makeId("absence"),
      type: cleanText(formData.get("type")),
      startDate: cleanText(formData.get("startDate")),
      endDate: cleanText(formData.get("endDate")),
      notes: cleanText(formData.get("notes"))
    };
    if (!absence.startDate) return;
    hub.updateTeamMember(member.id, { absences: [absence, ...(member.absences || [])] });
    event.currentTarget.reset();
  }

  function removeAbsence(absenceId) {
    hub.updateTeamMember(member.id, { absences: (member.absences || []).filter((absence) => absence.id !== absenceId) });
  }

  function submitAllocations(event) {
    event.preventDefault();
    const formData = new FormData(event.currentTarget);
    const allocations = {};
    hub.clients.forEach((client) => {
      const split = {};
      SERVICES.forEach((service) => {
        const value = Number(formData.get(`allocation:${client.id}:${service.id}`) || 0);
        if (value > 0) split[service.id] = value;
      });
      if (Object.keys(split).length) allocations[client.id] = split;
    });
    hub.updateTeamMember(member.id, { allocations });
  }

  return (
    <section className="staff-page">
      <section className="staff-section staff-profile-head">
        <a className="link-button" href="#staff">Back to staff</a>
        <div>
          <div className="staff-avatar staff-avatar-large">{member.photo ? <img src={member.photo} alt="" /> : <Icon name="user" />}</div>
          <label className="file-field staff-photo-upload">
            <Icon name="camera" small />
            <span>Photo</span>
            <input type="file" accept="image/*" onChange={handlePhotoChange} />
          </label>
        </div>
        <div>
          <h2>{member.name}</h2>
          <p>{member.role || "Unassigned"} - {hoursForMember(hub.timeEntries, member.id, hub.selectedMonth)}/{member.monthlyCapacity || 0} hrs used this month</p>
        </div>
        <button className="icon-button" type="button" onClick={() => hub.removeTeamMember(member.id)} aria-label="Remove staff member"><Icon name="close" small /></button>
      </section>

        <TimerControl memberScope={member} />
        <TimeLog memberScope={member} />
        <WeeklyTimesheet member={member} />

      <section className="staff-section">
        <div className="staff-section-head">
          <div>
            <h2>Profile</h2>
            <p>Update working capacity and notes.</p>
          </div>
        </div>
        <form className="staff-profile-form" onSubmit={submitProfile}>
          <input name="name" type="text" defaultValue={member.name} placeholder="Name" />
          <input name="role" type="text" defaultValue={member.role} placeholder="Role" />
            <input name="email" type="email" defaultValue={member.email} placeholder="Email" />
            <input name="capacity" type="number" min="0" step="0.25" defaultValue={member.capacity || ""} placeholder="Weekly hrs" />
            <input name="monthlyCapacity" type="number" min="0" step="0.25" defaultValue={member.monthlyCapacity || ""} placeholder="Monthly hrs" />
            <select name="employmentType" defaultValue={member.employmentType || ""}>
              <option value="">Employment type</option>
              {EMPLOYMENT_TYPES.map((type) => <option key={type} value={type}>{type}</option>)}
            </select>
            <input name="startDate" type="date" defaultValue={member.startDate || ""} aria-label="Start date" />
            <input name="skills" type="text" defaultValue={member.skills || ""} placeholder="Skills, comma separated" />
            <textarea name="notes" rows="3" defaultValue={member.notes || ""} placeholder="Notes" />
            <button className="btn btn-compact" type="submit">Save profile</button>
          </form>
        </section>

        <section className="staff-section">
          <div className="staff-section-head">
            <div>
              <h2>Absences</h2>
              <p>Record holidays, sickness, or unavailable blocks.</p>
            </div>
          </div>
          <form className="absence-form" onSubmit={submitAbsence}>
            <input name="type" type="text" placeholder="Type" />
            <input name="startDate" type="date" required aria-label="Absence start" />
            <input name="endDate" type="date" aria-label="Absence end" />
            <input name="notes" type="text" placeholder="Notes" />
            <button className="btn btn-compact" type="submit"><Icon name="plus" small /> Add absence</button>
          </form>
          <div className="ops-list">
            {(member.absences || []).length ? member.absences.map((absence) => (
              <article className="ops-row" key={absence.id}>
                <div>
                  <strong>{absence.type || "Absence"}</strong>
                  <span>{formatShortDate(absence.startDate)}{absence.endDate ? ` - ${formatShortDate(absence.endDate)}` : ""}</span>
                  {absence.notes ? <p>{absence.notes}</p> : null}
                </div>
                <button className="icon-button" type="button" onClick={() => removeAbsence(absence.id)} aria-label="Remove absence"><Icon name="close" small /></button>
              </article>
            )) : <p className="empty-note">No absences logged.</p>}
          </div>
        </section>

      <section className="staff-section">
        <div className="staff-section-head">
          <div>
            <h2>Client Allocations</h2>
            <p>Split designated monthly hours by client and service.</p>
          </div>
        </div>
        <form className="capacity-table-wrap" onSubmit={submitAllocations}>
          <table className="capacity-table">
            <thead>
              <tr>
                <th>Client</th>
                {SERVICES.map((service) => <th key={service.id}>{service.shortLabel}</th>)}
                <th>Total</th>
              </tr>
            </thead>
            <tbody>
              {hub.clients.map((client) => {
                const split = member.allocations?.[client.id] || {};
                return (
                  <tr key={client.id}>
                    <td>{client.name}</td>
                    {SERVICES.map((service) => (
                      <td key={service.id}>
                        <input name={`allocation:${client.id}:${service.id}`} type="number" min="0" step="0.25" defaultValue={split[service.id] || ""} aria-label={`${client.name} ${service.label} allocation`} />
                      </td>
                    ))}
                    <td>{serviceSplitTotal(split)}</td>
                  </tr>
                );
              })}
            </tbody>
          </table>
          <button className="btn btn-compact" type="submit">Save allocations</button>
        </form>
      </section>

      <section className="staff-section">
        <div className="staff-section-head">
          <div>
            <h2>Recent Time</h2>
            <p>Latest logged work for this staff member.</p>
          </div>
        </div>
        <TimeRows entries={memberTime} />
      </section>
    </section>
    );
  }

function TimerControl({ memberScope }) {
  const hub = useHub();
  const [timer, setTimer] = useState({ running: false, startedAt: 0, elapsed: 0, clientId: "", serviceId: SERVICES[0].id, jobId: "" });

  useEffect(() => {
    if (!timer.running) return undefined;
    const interval = window.setInterval(() => {
      setTimer((current) => ({ ...current, elapsed: Math.floor((Date.now() - current.startedAt) / 1000) }));
    }, 1000);
    return () => window.clearInterval(interval);
  }, [timer.running]);

  const eligibleJobs = openJobs(hub.jobs, hub.selectedMonth).filter((job) => (
    (!timer.clientId || job.clientId === timer.clientId)
    && (!timer.serviceId || job.sectionId === timer.serviceId)
  ));

  function startTimer(event) {
    event.preventDefault();
    const formData = new FormData(event.currentTarget);
    setTimer({
      running: true,
      startedAt: Date.now(),
      elapsed: 0,
      clientId: formData.get("clientId"),
      serviceId: formData.get("serviceId"),
      jobId: formData.get("jobId")
    });
  }

  function stopTimer() {
    const durationMinutes = Math.max(Math.round(timer.elapsed / 60), 1);
    hub.addTimeEntry({
      clientId: timer.clientId,
      memberId: memberScope.id,
      serviceId: timer.serviceId,
      jobId: timer.jobId,
      durationMinutes,
      hours: Number((durationMinutes / 60).toFixed(2)),
      workType: "Timer",
      workDate: new Date().toISOString().slice(0, 10),
      billable: true,
      notes: `Timer entry for ${formatDuration(timer.elapsed)}`
    });
    setTimer({ running: false, startedAt: 0, elapsed: 0, clientId: "", serviceId: SERVICES[0].id, jobId: "" });
  }

  return (
    <section className="staff-section timer-card">
      <div className="staff-section-head">
        <div>
          <h2>Timer</h2>
          <p>Start a timer, then save it straight into the time log.</p>
        </div>
        <strong>{formatDuration(timer.elapsed)}</strong>
      </div>
      <form className="ops-form-time timer-form" onSubmit={startTimer}>
        <select name="clientId" required disabled={timer.running} value={timer.clientId} onChange={(event) => setTimer((current) => ({ ...current, clientId: event.target.value }))} aria-label="Timer client">
          <option value="">Client</option>
          {hub.clients.map((client) => <option key={client.id} value={client.id}>{client.name}</option>)}
        </select>
        <select name="serviceId" disabled={timer.running} value={timer.serviceId} onChange={(event) => setTimer((current) => ({ ...current, serviceId: event.target.value }))} aria-label="Timer service">
          {SERVICES.map((service) => <option key={service.id} value={service.id}>{service.shortLabel}</option>)}
        </select>
        <select name="jobId" disabled={timer.running} value={timer.jobId} onChange={(event) => setTimer((current) => ({ ...current, jobId: event.target.value }))} aria-label="Timer task">
          <option value="">No task</option>
          {eligibleJobs.map((job) => <option key={job.id} value={job.id}>{job.title}</option>)}
        </select>
        {timer.running ? (
          <button className="btn btn-compact" type="button" onClick={stopTimer}>Stop & log</button>
        ) : (
          <button className="btn btn-compact" type="submit">Start timer</button>
        )}
      </form>
    </section>
  );
}

function WeeklyTimesheet({ member }) {
  const hub = useHub();
  const rows = weeklyTimesheetRows(hub.timeEntries.filter((entry) => entry.memberId === member.id), hub.selectedMonth);
  return (
    <section className="staff-section">
      <div className="staff-section-head">
        <div>
          <h2>Weekly Timesheet</h2>
          <p>Grouped time for this staff member in {hub.selectedMonth}.</p>
        </div>
        <a className="source source-secondary" href={`/api/export.csv?type=time&staffId=${encodeURIComponent(member.id)}`}>Export time</a>
      </div>
      <div className="capacity-table-wrap">
        <table className="capacity-table timesheet-table">
          <thead>
            <tr>
              <th>Week</th>
              {SERVICES.map((service) => <th key={service.id}>{service.shortLabel}</th>)}
              <th>Total</th>
            </tr>
          </thead>
          <tbody>
            {rows.length ? rows.map((row) => (
              <tr key={row.week}>
                <td>{row.week}</td>
                {SERVICES.map((service) => <td key={service.id}>{row.services[service.id] || 0}</td>)}
                <td>{row.total}</td>
              </tr>
            )) : <tr><td colSpan={SERVICES.length + 2}>No time logged for this month.</td></tr>}
          </tbody>
        </table>
      </div>
    </section>
  );
}

function CapacityMatrix() {
  const hub = useHub();
  const previewMonths = visibleMonthWindow(hub.selectedMonth, 3);

  function submitInlineAllocations(event) {
    event.preventDefault();
    const formData = new FormData(event.currentTarget);
    hub.teamMembers.forEach((member) => {
      const allocations = {};
      hub.clients.forEach((client) => {
        const split = {};
        SERVICES.forEach((service) => {
          const value = Number(formData.get(`allocation:${member.id}:${client.id}:${service.id}`) || 0);
          if (value > 0) split[service.id] = value;
        });
        if (Object.keys(split).length) allocations[client.id] = split;
      });
      hub.updateTeamMember(member.id, { allocations });
    });
  }

  return (
    <section className="staff-section" id="staff-capacity">
      <div className="staff-section-head">
        <div>
          <h2>Capacity</h2>
          <p>Allocated, used, and remaining hours by client and service for {hub.selectedMonth}.</p>
        </div>
        <div className="export-actions">
          <a className="source source-secondary" href="/api/export.csv?type=staff">Export staff</a>
          <a className="source source-secondary" href="/api/export.csv?type=time">Export time</a>
        </div>
      </div>
      <section className="capacity-breakdown">
        <div className="staff-section-head">
          <div>
            <h2>3 Month Capacity</h2>
            <p>Used hours against recorded monthly capacity by staff member.</p>
          </div>
        </div>
        <div className="capacity-table-wrap">
          <table className="capacity-table">
            <thead>
              <tr>
                <th>Staff</th>
                {previewMonths.map((month) => <th key={month}>{month}</th>)}
              </tr>
            </thead>
            <tbody>
              {hub.teamMembers.length ? hub.teamMembers.map((member) => (
                <tr key={member.id}>
                  <td><a href={`#staff/${encodeURIComponent(member.id)}`}>{member.name}</a></td>
                  {previewMonths.map((month) => {
                    const used = hoursForMember(hub.timeEntries, member.id, month);
                    const pct = member.monthlyCapacity ? percentage(used, member.monthlyCapacity) : 0;
                    return <td key={month} className={capacityClass(pct)}>{used}/{member.monthlyCapacity || 0} ({pct}%)</td>;
                  })}
                </tr>
              )) : <tr><td colSpan={previewMonths.length + 1}>No staff added yet.</td></tr>}
            </tbody>
          </table>
        </div>
      </section>
      <div className="capacity-table-wrap">
        <table className="capacity-table">
          <thead>
            <tr>
              <th>Client</th>
              {SERVICES.map((service) => <th key={service.id}>{service.shortLabel}</th>)}
              <th>Allocated</th>
              <th>Used</th>
              <th>Remaining</th>
            </tr>
          </thead>
          <tbody>
            {hub.clients.map((client) => {
              const allocated = allocatedHoursForClient(hub.teamMembers, client.id);
              const used = hoursForClient(hub.timeEntries, client.id, hub.selectedMonth);
              return (
                <tr key={client.id}>
                  <td><a href={clientPath(client.id)}>{client.name}</a></td>
                  {SERVICES.map((service) => {
                    const serviceAllocated = allocatedHoursForClient(hub.teamMembers, client.id, service.id);
                    const serviceUsed = hoursForClientService(hub.timeEntries, client.id, service.id, hub.selectedMonth);
                    return <td key={service.id}>{serviceUsed}/{serviceAllocated}</td>;
                  })}
                  <td>{allocated}</td>
                  <td>{used}</td>
                  <td className={allocated && used > allocated ? "danger-cell" : ""}>{allocated - used}</td>
                </tr>
              );
            })}
          </tbody>
        </table>
      </div>
      <section className="capacity-breakdown">
        <div className="staff-section-head">
          <div>
            <h2>Staff Allocation Breakdown</h2>
            <p>Designated hours by staff member, client, and service.</p>
          </div>
        </div>
        <form className="capacity-table-wrap inline-capacity-form" onSubmit={submitInlineAllocations}>
          <table className="capacity-table">
            <thead>
              <tr>
                <th>Staff</th>
                <th>Client</th>
                {SERVICES.map((service) => <th key={service.id}>{service.shortLabel}</th>)}
                <th>Total</th>
                <th>Used</th>
                <th>Remaining</th>
              </tr>
            </thead>
            <tbody>
              {hub.teamMembers.length ? hub.teamMembers.flatMap((member) => hub.clients.map((client) => {
                const split = member.allocations?.[client.id] || {};
                const allocated = serviceSplitTotal(split);
                const used = hub.timeEntries
                  .filter((entry) => entry.memberId === member.id && entry.clientId === client.id && isSameMonth(entry.workDate || entry.createdAt, hub.selectedMonth))
                  .reduce((total, entry) => total + Number(entry.hours || 0), 0);
                return (
                  <tr key={`${member.id}:${client.id}`}>
                    <td><a href={`#staff/${encodeURIComponent(member.id)}`}>{member.name}</a></td>
                    <td><a href={clientPath(client.id)}>{client.name}</a></td>
                    {SERVICES.map((service) => (
                      <td key={service.id}>
                        <input name={`allocation:${member.id}:${client.id}:${service.id}`} type="number" min="0" step="0.25" defaultValue={split[service.id] || ""} aria-label={`${member.name} ${client.name} ${service.shortLabel} hours`} />
                      </td>
                    ))}
                    <td>{allocated}</td>
                    <td>{used}</td>
                    <td className={allocated && used > allocated ? "danger-cell" : ""}>{allocated - used}</td>
                  </tr>
                );
              })) : (
                <tr><td colSpan={SERVICES.length + 5}>No staff allocations saved yet.</td></tr>
              )}
            </tbody>
          </table>
          <button className="btn btn-compact" type="submit">Save capacity matrix</button>
        </form>
      </section>
      <TimeLog />
    </section>
  );
}

function TimeLog({ memberScope = null }) {
  const hub = useHub();
  const [selectedClient, setSelectedClient] = useState("");
  const [selectedService, setSelectedService] = useState(SERVICES[0].id);
  const eligibleJobs = openJobs(hub.jobs, hub.selectedMonth).filter((job) => (
    (!selectedClient || job.clientId === selectedClient)
    && (!selectedService || job.sectionId === selectedService)
  ));

  function submitTime(event) {
    event.preventDefault();
    const formData = new FormData(event.currentTarget);
    hub.addTimeEntry({
      clientId: formData.get("clientId"),
      memberId: memberScope?.id || formData.get("memberId"),
      jobId: formData.get("jobId"),
        serviceId: selectedService || formData.get("serviceId"),
        hours: formData.get("hours"),
        durationMinutes: Math.round(Number(formData.get("hours") || 0) * 60),
        billable: formData.get("billable") === "on",
        workType: formData.get("workType"),
      workDate: formData.get("workDate"),
      notes: formData.get("notes")
    });
    event.currentTarget.reset();
    setSelectedClient("");
    setSelectedService(SERVICES[0].id);
  }

  return (
    <section className="staff-section">
      <div className="staff-section-head">
        <div>
          <h2>Time Log</h2>
          <p>Manual time entry by client, staff member, service, and task.</p>
        </div>
      </div>
      <form className="ops-form-time" onSubmit={submitTime}>
        <select name="clientId" required aria-label="Client" value={selectedClient} onChange={(event) => setSelectedClient(event.target.value)}>
          <option value="">Client</option>
          {hub.clients.map((client) => <option key={client.id} value={client.id}>{client.name}</option>)}
        </select>
        {memberScope ? (
          <input name="memberName" type="text" value={memberScope.name} readOnly aria-label="Staff member" />
        ) : (
          <select name="memberId" required aria-label="Staff">
            <option value="">Staff</option>
            {hub.teamMembers.map((member) => <option key={member.id} value={member.id}>{member.name}</option>)}
          </select>
        )}
        <select name="serviceId" aria-label="Service" value={selectedService} onChange={(event) => setSelectedService(event.target.value)}>
          {SERVICES.map((service) => <option key={service.id} value={service.id}>{service.shortLabel}</option>)}
        </select>
        <select name="jobId" aria-label="Task">
          <option value="">No task</option>
          {eligibleJobs.map((job) => {
            const client = hub.clients.find((item) => item.id === job.clientId);
            return <option key={job.id} value={job.id}>{client?.name || "Client"} - {job.title}</option>;
          })}
        </select>
          <input name="hours" type="number" min="0" step="0.25" placeholder="Hours" required />
          <input name="workDate" type="date" />
          <input name="workType" type="text" placeholder="Work type" />
          <label className="inline-check">
            <input name="billable" type="checkbox" defaultChecked />
            <span>Billable</span>
          </label>
          <textarea name="notes" rows="2" placeholder="Notes" />
        <button className="btn btn-compact" type="submit"><Icon name="plus" small /> Log</button>
      </form>
      <TimeRows entries={hub.timeEntries.slice(0, 12)} />
    </section>
  );
}

function TimeRows({ entries }) {
  const hub = useHub();
  if (!entries.length) return <p className="empty-note">No time logged yet.</p>;
  return (
    <div className="ops-list">
      {entries.map((entry) => {
        const client = hub.clients.find((item) => item.id === entry.clientId);
        const member = hub.teamMembers.find((item) => item.id === entry.memberId);
        const job = hub.jobs.find((item) => item.id === entry.jobId);
        return (
          <article className="ops-row" key={entry.id}>
            <div>
              <strong>{entry.hours} hrs - {client?.name || "Client"}</strong>
                <span>{member?.name || "Staff"} - {serviceLabel(entry.serviceId)} - {job?.title || entry.workType || "General work"}{entry.workDate ? ` - ${formatShortDate(entry.workDate)}` : ""} - {entry.billable ? "billable" : "non-billable"}</span>
              {entry.notes ? <p>{entry.notes}</p> : null}
            </div>
            <button className="icon-button" type="button" onClick={() => hub.removeTimeEntry(entry.id)} aria-label="Remove time entry"><Icon name="close" small /></button>
          </article>
        );
      })}
    </div>
  );
}

function ClientLogo({ client, large = false }) {
  const { profiles } = useHub();
  const logo = profileFor(profiles, client.id).logo || client.logo || "";
  return (
    <div className={`client-logo ${large ? "client-logo-large" : ""}`}>
      {logo ? <img src={logo} alt="" /> : <Icon name={clientIcons[client.id] || "report"} />}
    </div>
  );
}

function Icon({ name, small = false }) {
  const paths = {
    target: <><path d="M12 21a9 9 0 1 0-9-9 9 9 0 0 0 9 9Z" /><path d="M12 17a5 5 0 1 0-5-5 5 5 0 0 0 5 5Z" /><path d="M12 13a1 1 0 1 0-1-1 1 1 0 0 0 1 1Z" /></>,
    send: <><path d="m22 2-7 20-4-9-9-4Z" /><path d="M22 2 11 13" /></>,
    chart: <><path d="M4 19V5" /><path d="M4 19h16" /><path d="M8 16v-5" /><path d="M12 16V8" /><path d="M16 16v-9" /><path d="m8 11 4-3 4 1 4-5" /></>,
    grid: <><rect x="4" y="4" width="6" height="6" rx="1" /><rect x="14" y="4" width="6" height="6" rx="1" /><rect x="4" y="14" width="6" height="6" rx="1" /><rect x="14" y="14" width="6" height="6" rx="1" /></>,
    shield: <path d="M12 22s8-4 8-10V5l-8-3-8 3v7c0 6 8 10 8 10Z" />,
    report: <><path d="M8 3h8l3 3v15H5V3Z" /><path d="M16 3v4h4" /><path d="M8 12h8" /><path d="M8 16h6" /></>,
    consultancy: <><path d="M21 15a4 4 0 0 1-4 4H8l-5 3V7a4 4 0 0 1 4-4h10a4 4 0 0 1 4 4Z" /><path d="M8 9h8" /><path d="M8 13h5" /></>,
    focus: <><path d="M12 21a9 9 0 1 0-9-9 9 9 0 0 0 9 9Z" /><path d="M12 17a5 5 0 1 0-5-5 5 5 0 0 0 5 5Z" /></>,
    links: <><path d="M10 13a5 5 0 0 0 7 0l2-2a5 5 0 0 0-7-7l-1 1" /><path d="M14 11a5 5 0 0 0-7 0l-2 2a5 5 0 0 0 7 7l1-1" /></>,
    content: <><path d="M4 20h16" /><path d="M5 16 16 5l3 3L8 19H5Z" /><path d="m14 7 3 3" /></>,
    search: <><circle cx="11" cy="11" r="7" /><path d="m21 21-4.3-4.3" /></>,
    megaphone: <><path d="m3 11 18-5v12L3 13v-2Z" /><path d="M11 14v5a2 2 0 0 1-2 2H8a2 2 0 0 1-2-2v-6" /><path d="M21 10a4 4 0 0 1 0 4" /></>,
    calendar: <><path d="M8 2v4" /><path d="M16 2v4" /><rect x="3" y="4" width="18" height="18" rx="2" /><path d="M3 10h18" /></>,
    external: <><path d="M14 3h7v7" /><path d="M10 14 21 3" /><path d="M21 14v5a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2V5a2 2 0 0 1 2-2h5" /></>,
    cloud: <path d="M17.5 19H7a5 5 0 1 1 1-9.9A7 7 0 0 1 21 12.5 3.5 3.5 0 0 1 17.5 19Z" />,
    plus: <><path d="M12 5v14" /><path d="M5 12h14" /></>,
    close: <><path d="M18 6 6 18" /><path d="m6 6 12 12" /></>,
    arrow: <><path d="M5 12h14" /><path d="m12 5 7 7-7 7" /></>,
    user: <><path d="M20 21a8 8 0 0 0-16 0" /><circle cx="12" cy="7" r="4" /></>,
    users: <><path d="M16 21a6 6 0 0 0-12 0" /><circle cx="10" cy="7" r="4" /><path d="M22 21a5 5 0 0 0-5-5" /><path d="M17 3.2a4 4 0 0 1 0 7.6" /></>,
    camera: <><path d="M14.5 4 16 7h3a2 2 0 0 1 2 2v9a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2V9a2 2 0 0 1 2-2h3l1.5-3Z" /><circle cx="12" cy="13" r="4" /></>,
    print: <><path d="M6 9V3h12v6" /><path d="M6 18H4a2 2 0 0 1-2-2v-5a2 2 0 0 1 2-2h16a2 2 0 0 1 2 2v5a2 2 0 0 1-2 2h-2" /><path d="M6 14h12v7H6z" /></>
  };
  return <svg className={`icon ${small ? "icon-small" : ""}`} aria-hidden="true" viewBox="0 0 24 24">{paths[name] || paths.report}</svg>;
}

function loadState(key, fallback) {
  try {
    const raw = localStorage.getItem(key);
    return raw ? JSON.parse(raw) : fallback;
  } catch {
    return fallback;
  }
}

function saveState(key, value) {
  try {
    localStorage.setItem(key, JSON.stringify(value));
  } catch {
    // Browser storage may be disabled; cloud sync still carries the live app.
  }
}

function routeFromHash() {
  const hash = window.location.hash.replace(/^#/, "");
  const parts = hash.split("/").filter(Boolean);
  if (!parts.length || parts[0] === "overview" || parts[0] === "clients") {
    return { view: "overview" };
  }
  if (parts[0] === "tasks") {
    return { view: "tasks", sectionId: parts[1] ? decodeURIComponent(parts[1]) : "list" };
  }
  if (parts[0] === "templates") {
    return { view: "templates" };
  }
  if (parts[0] === "knowledge") {
    return { view: "knowledge" };
  }
  if (parts[0] === "activity") {
    return { view: "activity" };
  }
  if (parts[0] === "share" && parts[1]) {
    return { view: "share", token: decodeURIComponent(parts[1]) };
  }
  if (parts[0] === "staff") {
    if (parts[1] === "capacity") return { view: "staff", sectionId: "capacity" };
    return { view: "staff", memberId: parts[1] ? decodeURIComponent(parts[1]) : null };
  }
  if (parts[0] === "client" && parts[1]) {
    return {
      view: "client",
      clientId: decodeURIComponent(parts[1]),
      sectionId: parts[2] ? decodeURIComponent(parts[2]) : null
    };
  }
  return { view: "overview" };
}

function clientPath(clientId, sectionId = "") {
  return `#client/${encodeURIComponent(clientId)}${sectionId ? `/${encodeURIComponent(sectionId)}` : ""}`;
}

function exportUrl(type, clientId, sectionId = "") {
  const params = new URLSearchParams({ type, clientId, month: loadState(storageKeys.month, currentMonthName()) });
  if (sectionId) params.set("sectionId", sectionId);
  return `/api/export.csv?${params.toString()}`;
}

function normaliseClient(client = {}) {
  return {
    id: client.id || slugify(client.name || "Client"),
    name: client.name || "Client",
    status: client.status || "current",
    lifecycleStatus: client.lifecycleStatus || client.lifecycle_status || "active",
    retainerValue: Number(client.retainerValue || client.retainer_value || 0),
    renewalDate: client.renewalDate || client.renewal_date || "",
    source: client.source || client.sourceUrl || "",
    manual: Boolean(client.manual),
    logo: client.logo || client.logoData || ""
  };
}

function normaliseTeamMember(member = {}) {
  const weeklyCapacity = Number(member.capacity || member.capacityHours || 0);
  const monthlyCapacity = Number(member.monthlyCapacity || member.capacityMonthly || (weeklyCapacity ? weeklyCapacity * 4 : 0));
  return {
    id: member.id || makeId("staff"),
    name: member.name || "",
    role: member.role || "",
    email: member.email || "",
    accessRole: member.accessRole || member.access_role || "staff",
    accessStatus: member.accessStatus || member.access_status || "active",
    capacity: weeklyCapacity,
    monthlyCapacity,
    photo: member.photo || member.photoData || "",
    notes: member.notes || "",
    skills: member.skills || "",
    employmentType: member.employmentType || member.employment_type || "",
    startDate: member.startDate || member.start_date || "",
    absences: normaliseAbsences(member.absences || member.absences_json),
    allocations: normaliseAllocations(member.allocations || member.client_allocations)
  };
}

function normaliseJob(job = {}) {
  return {
    id: job.id || makeId("job"),
    clientId: job.clientId || job.client_id || "",
    sectionId: normaliseServiceId(job.sectionId || job.section_id || "seo-dashboard"),
    month: job.month || "",
    groupName: job.groupName || job.group_name || "",
    title: job.title || "",
    description: job.description || "",
    status: job.status || "todo",
    priority: job.priority || "normal",
    ownerId: job.ownerId || job.owner_id || "",
    dueDate: job.dueDate || job.due_date || "",
    reminderDate: job.reminderDate || job.reminder_date || "",
    templateId: job.templateId || job.template_id || "",
    sopId: job.sopId || job.sop_id || "",
    completedAt: job.completedAt || job.completed_at || "",
    sourceType: job.sourceType || job.source_type || "",
    sourceRef: job.sourceRef || job.source_ref || "",
    createdAt: job.createdAt || job.created_at || new Date().toISOString()
  };
}

function normaliseTimeEntry(entry = {}) {
  return {
    id: entry.id || makeId("time"),
    clientId: entry.clientId || entry.client_id || "",
    memberId: entry.memberId || entry.staffId || entry.staff_id || "",
    jobId: entry.jobId || entry.job_id || "",
    serviceId: normaliseServiceId(entry.serviceId || entry.service_id || entry.workType || "seo-dashboard"),
    hours: Number(entry.hours || 0),
    durationMinutes: Number(entry.durationMinutes || entry.duration_minutes || Math.round(Number(entry.hours || 0) * 60)),
    billable: entry.billable === undefined ? true : Boolean(entry.billable),
    workType: entry.workType || entry.work_type || "",
    workDate: entry.workDate || entry.work_date || "",
    notes: entry.notes || "",
    createdAt: entry.createdAt || entry.created_at || new Date().toISOString()
  };
}

function normaliseTaskTemplate(template = {}) {
  return {
    id: template.id || makeId("template"),
    name: template.name || "",
    sectionId: normaliseServiceId(template.sectionId || template.section_id || "seo-dashboard"),
    description: template.description || "",
    tasks: Array.isArray(template.tasks) ? template.tasks.map(normaliseTemplateTask).filter((task) => task.title) : templateTasksFromText(parseMaybeJsonArray(template.tasks_json).length ? parseMaybeJsonArray(template.tasks_json) : template.tasksText || template.tasks_json || ""),
    active: template.active === undefined ? true : Boolean(template.active),
    createdAt: template.createdAt || template.created_at || new Date().toISOString(),
    updatedAt: template.updatedAt || template.updated_at || ""
  };
}

function normaliseTemplateTask(task = {}) {
  if (typeof task === "string") return { title: task, groupName: "", description: "", priority: "normal", dueOffsetDays: 0, sopId: "" };
  return {
    title: task.title || task.label || "",
    groupName: task.groupName || task.group_name || "",
    description: task.description || "",
    priority: task.priority || "normal",
    dueOffsetDays: Number(task.dueOffsetDays || task.due_offset_days || 0),
    dueDate: task.dueDate || task.due_date || "",
    sopId: task.sopId || task.sop_id || ""
  };
}

function normaliseSubtask(subtask = {}) {
  return {
    id: subtask.id || makeId("subtask"),
    jobId: subtask.jobId || subtask.job_id || "",
    label: subtask.label || "",
    done: Boolean(subtask.done),
    createdAt: subtask.createdAt || subtask.created_at || new Date().toISOString(),
    updatedAt: subtask.updatedAt || subtask.updated_at || ""
  };
}

function normaliseComment(comment = {}) {
  return {
    id: comment.id || makeId("comment"),
    jobId: comment.jobId || comment.job_id || "",
    staffId: comment.staffId || comment.staff_id || "",
    body: comment.body || "",
    mentions: Array.isArray(comment.mentions) ? comment.mentions : parseMaybeJsonArray(comment.mentions_json),
    createdAt: comment.createdAt || comment.created_at || new Date().toISOString()
  };
}

function normaliseActivity(activity = {}) {
  return {
    id: activity.id || makeId("activity"),
    clientId: activity.clientId || activity.client_id || "",
    jobId: activity.jobId || activity.job_id || "",
    staffId: activity.staffId || activity.staff_id || "",
    eventType: activity.eventType || activity.event_type || "",
    body: activity.body || "",
    createdAt: activity.createdAt || activity.created_at || new Date().toISOString()
  };
}

function normaliseSopArticle(article = {}) {
  return {
    id: article.id || makeId("sop"),
    title: article.title || "",
    body: article.body || "",
    tags: article.tags || "",
    linkedService: article.linkedService || article.linked_service || "",
    createdAt: article.createdAt || article.created_at || new Date().toISOString(),
    updatedAt: article.updatedAt || article.updated_at || ""
  };
}

function normaliseClientShare(share = {}) {
  return {
    id: share.id || makeId("share"),
    clientId: share.clientId || share.client_id || "",
    token: share.token || "",
    active: share.active === undefined ? true : Boolean(share.active),
    expiresAt: share.expiresAt || share.expires_at || "",
    createdAt: share.createdAt || share.created_at || new Date().toISOString()
  };
}

function normaliseAuditEntry(entry = {}) {
  return {
    id: entry.id || makeId("audit"),
    staffId: entry.staffId || entry.staff_id || "",
    entityType: entry.entityType || entry.entity_type || "",
    entityId: entry.entityId || entry.entity_id || "",
    action: entry.action || "",
    before: entry.before || parseMaybeJsonObject(entry.before_json),
    after: entry.after || parseMaybeJsonObject(entry.after_json),
    createdAt: entry.createdAt || entry.created_at || new Date().toISOString()
  };
}

function normaliseAllocations(allocations = {}) {
  if (typeof allocations === "string") {
    try {
      return normaliseAllocations(JSON.parse(allocations));
    } catch {
      return {};
    }
  }
  return Object.fromEntries(Object.entries(allocations || {}).map(([clientId, split]) => {
    if (typeof split === "number" || typeof split === "string") {
      const hours = Math.max(Number(split || 0), 0);
      return [clientId, hours ? { "seo-dashboard": hours } : {}];
    }
    const serviceSplit = {};
    SERVICES.forEach((service) => {
      const hours = Math.max(Number(split?.[service.id] || split?.[service.shortLabel] || 0), 0);
      if (hours) serviceSplit[service.id] = hours;
    });
    return [clientId, serviceSplit];
    }).filter(([, split]) => serviceSplitTotal(split)));
  }

function normaliseAbsences(absences = []) {
  const parsed = Array.isArray(absences) ? absences : parseMaybeJsonArray(absences);
  return parsed.map((absence) => ({
    id: absence.id || makeId("absence"),
    type: absence.type || "",
    startDate: absence.startDate || absence.start_date || "",
    endDate: absence.endDate || absence.end_date || "",
    notes: absence.notes || ""
  })).filter((absence) => absence.startDate);
}

function parseMaybeJsonArray(value) {
  if (!value) return [];
  if (Array.isArray(value)) return value;
  try {
    const parsed = JSON.parse(value);
    return Array.isArray(parsed) ? parsed : [];
  } catch {
    return [];
  }
}

function parseMaybeJsonObject(value) {
  if (!value) return {};
  if (typeof value === "object" && !Array.isArray(value)) return value;
  try {
    const parsed = JSON.parse(value);
    return parsed && typeof parsed === "object" && !Array.isArray(parsed) ? parsed : {};
  } catch {
    return {};
  }
}

function profileFor(profiles, clientId) {
  const profile = profiles[clientId] || {};
  return {
    contacts: profile.contacts || "",
    contactsList: Array.isArray(profile.contactsList) ? profile.contactsList : parseMaybeJsonArray(profile.contacts_json),
    notes: profile.notes || "",
    logo: profile.logo || ""
  };
}

function upsertById(items, item) {
  return items.some((current) => current.id === item.id)
    ? items.map((current) => current.id === item.id ? item : current)
    : [item, ...items];
}

function cleanText(value, fallback = "") {
  const text = String(value ?? "").trim();
  return text || fallback;
}

function slugify(value) {
  const slug = cleanText(value).toLowerCase().replace(/&/g, " and ").replace(/[^a-z0-9]+/g, "-").replace(/^-+|-+$/g, "");
  return slug || `client-${Date.now().toString(36)}`;
}

function makeId(prefix) {
  const random = crypto.randomUUID ? crypto.randomUUID() : `${Date.now()}-${Math.random()}`;
  return `${prefix}_${String(random).replace(/[^a-zA-Z0-9]/g, "").slice(0, 24)}`;
}

function currentMonthName() {
  return new Intl.DateTimeFormat("en-GB", { month: "long" }).format(new Date());
}

function normaliseServiceId(value) {
  const text = cleanText(value).toLowerCase();
  const match = SERVICES.find((service) => (
    service.id === text
    || service.shortLabel.toLowerCase() === text
    || service.label.toLowerCase() === text
  ));
  return match?.id || "seo-dashboard";
}

function serviceLabel(serviceId) {
  if (!serviceId) return "";
  return SERVICES.find((service) => service.id === serviceId)?.shortLabel || "SEO";
}

function isTaskInMonth(job, month) {
  return !month || !job.month || job.month === month;
}

function jobsFor(jobs, clientId, sectionId = "", month = "") {
  return jobs.filter((job) => (
    job.clientId === clientId
    && (!sectionId || job.sectionId === sectionId)
    && (!month || !job.month || job.month === month)
    ));
  }

function normaliseKanbanStatus(status) {
  if (status === "todo") return "backlog";
  if (status === "cancelled") return "done";
  return TASK_STATUSES.some((item) => item.id === status) ? status : "backlog";
}

function openJobs(jobs, month = "") {
  return jobs.filter((job) => (
    (!month || !job.month || job.month === month)
    && !["done", "cancelled"].includes(String(job.status || "").toLowerCase())
  ));
}

function overdueJobs(jobs, clientId = "") {
  const today = new Date();
  today.setHours(0, 0, 0, 0);
  return jobs.filter((job) => {
    if (clientId && job.clientId !== clientId) return false;
    if (["done", "cancelled"].includes(String(job.status || "").toLowerCase())) return false;
    if (!job.dueDate) return false;
    const due = new Date(job.dueDate);
    due.setHours(0, 0, 0, 0);
    return !Number.isNaN(due.getTime()) && due < today;
  });
}

function seoTotals(clientId, jobs, month) {
  const tasks = jobsFor(jobs, clientId, "seo-dashboard", month);
  return {
    done: tasks.filter((job) => job.status === "done").length,
    total: tasks.length
  };
}

function allSeoTotals(clients, jobs, month) {
  return clients.reduce((total, client) => {
    const clientTotals = seoTotals(client.id, jobs, month);
    total.done += clientTotals.done;
    total.total += clientTotals.total;
    return total;
  }, { done: 0, total: 0 });
}

function clientTaskSummary(client, jobs, month) {
  const clientJobs = jobsFor(jobs, client.id, "", month);
  return {
    done: clientJobs.filter((job) => job.status === "done").length,
    total: clientJobs.length
    };
  }

function clientHealth(client, jobs, entries, members, month) {
  if (["paused", "churned"].includes(client.lifecycleStatus)) {
    return { level: "amber", label: statusLabel(client.lifecycleStatus), reason: "Status needs review" };
  }
  const overdue = overdueJobs(jobs, client.id).length;
  if (overdue) return { level: "red", label: "At risk", reason: `${overdue} overdue task${overdue === 1 ? "" : "s"}` };
  const allocated = allocatedHoursForClient(members, client.id);
  const used = hoursForClient(entries, client.id, month);
  if (allocated && used > allocated) return { level: "red", label: "Over capacity", reason: `${used - allocated} hrs over` };
  if (allocated && used / allocated > 0.85) return { level: "amber", label: "Watch", reason: `${percentage(used, allocated)}% hours used` };
  return { level: "green", label: "Healthy", reason: allocated ? `${allocated - used} hrs remaining` : "No pressure flagged" };
}

function percentage(done, total) {
  return total ? Math.round((done / total) * 100) : 0;
}

function serviceSplitTotal(split = {}) {
  return SERVICES.reduce((total, service) => total + Number(split?.[service.id] || 0), 0);
}

function allocatedHoursForMember(member) {
  return Object.values(member.allocations || {}).reduce((total, split) => total + serviceSplitTotal(split), 0);
}

function allocatedHoursForClient(members, clientId, serviceId = "") {
  return members.reduce((total, member) => {
    const split = member.allocations?.[clientId] || {};
    return total + (serviceId ? Number(split[serviceId] || 0) : serviceSplitTotal(split));
  }, 0);
}

function assignedMembersForClient(members, clientId) {
  return members.filter((member) => serviceSplitTotal(member.allocations?.[clientId] || {}) > 0);
}

function staffAllocationRows(members, clients, entries, month) {
  return members.flatMap((member) => Object.entries(member.allocations || {}).map(([clientId, split]) => {
    const client = clients.find((item) => item.id === clientId);
    const allocated = serviceSplitTotal(split);
    const used = entries
      .filter((entry) => entry.memberId === member.id && entry.clientId === clientId && isSameMonth(entry.workDate || entry.createdAt, month))
      .reduce((total, entry) => total + Number(entry.hours || 0), 0);
    return client && allocated ? { member, client, split, allocated, used } : null;
  }).filter(Boolean)).sort((a, b) => (
    a.member.name.localeCompare(b.member.name) || a.client.name.localeCompare(b.client.name)
    ));
  }

function visibleMonthWindow(selectedMonth, count = 3) {
  const start = Math.max(MONTHS.indexOf(selectedMonth), 0);
  return Array.from({ length: count }, (_, index) => MONTHS[(start + index) % MONTHS.length]);
}

function capacityClass(percent) {
  if (percent > 100) return "danger-cell heat-danger";
  if (percent > 85) return "heat-warm";
  if (percent > 0) return "heat-good";
  return "";
}

function weeklyTimesheetRows(entries, month) {
  const rows = new Map();
  entries
    .filter((entry) => isSameMonth(entry.workDate || entry.createdAt, month))
    .forEach((entry) => {
      const week = weekLabel(entry.workDate || entry.createdAt);
      const current = rows.get(week) || { week, services: {}, total: 0 };
      current.services[entry.serviceId] = Number(((current.services[entry.serviceId] || 0) + Number(entry.hours || 0)).toFixed(2));
      current.total = Number((current.total + Number(entry.hours || 0)).toFixed(2));
      rows.set(week, current);
    });
  return Array.from(rows.values()).sort((a, b) => a.week.localeCompare(b.week));
}

function weekLabel(dateValue) {
  const date = new Date(dateValue || new Date());
  if (Number.isNaN(date.getTime())) return "Unknown week";
  const start = new Date(date);
  const day = start.getDay() || 7;
  start.setHours(0, 0, 0, 0);
  start.setDate(start.getDate() - day + 1);
  const end = new Date(start);
  end.setDate(start.getDate() + 6);
  return `${formatShortDate(start.toISOString())} - ${formatShortDate(end.toISOString())}`;
}

function isSameMonth(dateValue, month) {
  if (!month) return true;
  const date = new Date(dateValue || new Date());
  return !Number.isNaN(date.getTime()) && MONTHS[date.getMonth()] === month;
}

function isCurrentWeek(dateValue) {
  const date = new Date(dateValue || new Date());
  const today = new Date();
  const start = new Date(today);
  const day = start.getDay() || 7;
  start.setHours(0, 0, 0, 0);
  start.setDate(start.getDate() - day + 1);
  const end = new Date(start);
  end.setDate(start.getDate() + 7);
  return date >= start && date < end;
}

function totalTrackedHours(entries, month, period = "month") {
  return entries
    .filter((entry) => period === "week" ? isCurrentWeek(entry.workDate || entry.createdAt) : isSameMonth(entry.workDate || entry.createdAt, month))
    .reduce((total, entry) => total + Number(entry.hours || 0), 0);
}

function hoursForClient(entries, clientId, month) {
  return entries
    .filter((entry) => entry.clientId === clientId && isSameMonth(entry.workDate || entry.createdAt, month))
    .reduce((total, entry) => total + Number(entry.hours || 0), 0);
}

function hoursForClientService(entries, clientId, serviceId, month) {
  return entries
    .filter((entry) => entry.clientId === clientId && entry.serviceId === serviceId && isSameMonth(entry.workDate || entry.createdAt, month))
    .reduce((total, entry) => total + Number(entry.hours || 0), 0);
}

function hoursForMember(entries, memberId, month) {
  return entries
    .filter((entry) => entry.memberId === memberId && isSameMonth(entry.workDate || entry.createdAt, month))
    .reduce((total, entry) => total + Number(entry.hours || 0), 0);
}

function resourceLinkCount(resourceLinks) {
  return Object.values(resourceLinks).reduce((clientTotal, sections) => (
    clientTotal + Object.values(sections || {}).reduce((sectionTotal, links) => sectionTotal + links.length, 0)
  ), 0);
}

function formatShortDate(value) {
  const date = new Date(value);
  if (Number.isNaN(date.getTime())) return value;
  return new Intl.DateTimeFormat("en-GB", { day: "2-digit", month: "short" }).format(date);
}

function formatDateTime(value) {
  const date = new Date(value);
  if (Number.isNaN(date.getTime())) return "unknown";
  return new Intl.DateTimeFormat("en-GB", {
    day: "2-digit",
    month: "long",
    year: "numeric",
    hour: "2-digit",
    minute: "2-digit"
  }).format(date);
}

function statusLabel(status) {
  return {
    active: "Active",
    paused: "Paused",
    prospect: "Prospect",
    churned: "Churned",
    backlog: "Backlog",
    todo: "To do",
    "in-progress": "In progress",
    review: "Review",
    waiting: "Waiting",
    done: "Done",
    cancelled: "Cancelled"
  }[status] || status;
}

function templateTasksFromText(value) {
  if (Array.isArray(value)) return value.map(normaliseTemplateTask).filter((task) => task.title);
  return cleanText(value)
    .split(/\r?\n/)
    .map((line) => cleanText(line.replace(/^[-*]\s*/, "")))
    .filter(Boolean)
    .map((line) => {
      const [groupName, title, priority, dueOffsetDays] = line.split("|").map((part) => cleanText(part));
      if (!title) return { title: groupName, groupName: "", description: "", priority: "normal", dueOffsetDays: 0, sopId: "" };
      return {
        groupName,
        title,
        description: "",
        priority: priority || "normal",
        dueOffsetDays: Number(dueOffsetDays || 0),
        sopId: ""
      };
    });
}

function extractMentions(value) {
  return Array.from(new Set((cleanText(value).match(/@[\w .-]{2,40}/g) || []).map((mention) => mention.slice(1).trim()).filter(Boolean)));
}

function mentionNotifications(comments) {
  return comments.flatMap((comment) => (comment.mentions || []).map((mention) => ({ comment, mention })));
}

function dueDateFromOffset(startDate, offsetDays) {
  if (!startDate || !offsetDays) return "";
  const date = new Date(startDate);
  if (Number.isNaN(date.getTime())) return "";
  date.setDate(date.getDate() + Number(offsetDays || 0));
  return date.toISOString().slice(0, 10);
}

function formatDuration(totalSeconds) {
  const hours = Math.floor(totalSeconds / 3600);
  const minutes = Math.floor((totalSeconds % 3600) / 60);
  const seconds = totalSeconds % 60;
  return [hours, minutes, seconds].map((part) => String(part).padStart(2, "0")).join(":");
}

function groupIconName(group) {
  const label = String(group || "").toLowerCase();
  if (label.includes("report")) return "report";
  if (label.includes("consult")) return "consultancy";
  if (label.includes("focus")) return "focus";
  if (label.includes("link") || label.includes("pr")) return "links";
  if (label.includes("content")) return "content";
  return "report";
}

function normaliseUrl(value) {
  const trimmed = cleanText(value);
  if (!trimmed) return "";
  if (/^https?:\/\//i.test(trimmed)) return trimmed;
  return `https://${trimmed}`;
}

function isGoogleTrackerUrl(value) {
  return /^https:\/\/docs\.google\.com\//i.test(cleanText(value));
}

function toneForClient(clientId, clients) {
  const index = clients.findIndex((client) => client.id === clientId);
  return clientTones[Math.max(index, 0) % clientTones.length];
}

function clientToneStyle(tone) {
  const tones = {
    blue: ["#3b6cdd", "#edf3ff"],
    green: ["#087e83", "#e6fdff"],
    orange: ["#93653d", "#f8f1ea"],
    purple: ["#8d4e76", "#fbf1f7"],
    pink: ["#b54d83", "#fbedf5"],
    teal: ["#0b8f80", "#e8faf7"],
    slate: ["#253031", "#eef2f2"]
  };
  const [color, soft] = tones[tone] || tones.pink;
  return { "--client": color, "--client-soft": soft };
}

async function readImageData(file) {
  if (!file || !file.size) return "";
  if (!file.type.startsWith("image/")) {
    throw new Error("Please choose an image file.");
  }
  const dataUrl = await new Promise((resolve, reject) => {
    const reader = new FileReader();
    reader.addEventListener("load", () => resolve(reader.result));
    reader.addEventListener("error", () => reject(new Error("Could not read the image.")));
    reader.readAsDataURL(file);
  });
  return new Promise((resolve, reject) => {
    const image = new Image();
    image.addEventListener("load", () => {
      const size = 320;
      const canvas = document.createElement("canvas");
      canvas.width = size;
      canvas.height = size;
      const context = canvas.getContext("2d");
      const scale = Math.max(size / image.width, size / image.height);
      const width = image.width * scale;
      const height = image.height * scale;
      context.drawImage(image, (size - width) / 2, (size - height) / 2, width, height);
      resolve(canvas.toDataURL("image/jpeg", 0.82));
    });
    image.addEventListener("error", () => reject(new Error("Could not resize the image.")));
    image.src = dataUrl;
  });
}

createRoot(document.getElementById("root")).render(<App />);
