Team roles: construction personas with attire notes; fix dark-theme dropdown

- Replace roles with the 10 Construction Industry personas (Owner, Admin,
  Sales Manager, Sales Rep, Project Manager, Site Supervisor, Canvasser,
  Team Lead, Subcontractor, Other), each with description, attire &
  appearance cues and a common-overlap note
- Render Attire & appearance + Common overlap sections on each role card
- Remap members/invites to new role ids; add canvasser, team-lead and
  subcontractor members so every persona shows
- Set color-scheme per theme and style <select> options so the role
  dropdown is legible in dark mode

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-07-06 16:39:14 +05:30
parent f1a7bdc8ce
commit 25420bef8e
3 changed files with 178 additions and 31 deletions
+140 -27
View File
@@ -66,64 +66,174 @@ export type Role = {
description: string;
system: boolean; // system roles can't be deleted
perms: string[]; // granted permission ids
attire: string[]; // "attire & appearance" notes from the persona guide
overlap: string; // "common overlap" note — how the role blends with others
};
// Roles mirror the Construction Industry Personas & Attire reference guide:
// each role carries a plain-language description, the attire/appearance cues
// a person in that role typically shows, and the way the role commonly
// overlaps with others in a small contractor.
export const roles: Role[] = [
{
id: "owner",
name: "Owner",
color: "var(--orange)",
description: "Full, unrestricted access. There is exactly one account owner.",
description: "Sets the company standard, drives the vision, builds relationships and stays close to sales, operations, production, client care and financial performance.",
system: true,
perms: [...allPermissionIds],
attire: [
"Tucked-in button-down or refined work shirt",
"Fitted jeans or chinos with a belt",
"Nice boots or leather shoes",
"Quality watch — Rolex-style or similar",
"Polished, confident, hands-on presence",
],
overlap: "In smaller companies, owners are often involved in a little bit of everything: sales, estimating, production, payroll, hiring and customer issues.",
},
{
id: "admin",
name: "Admin",
color: "var(--purple)",
description: "Runs the workspace — manages people, roles and settings.",
description: "Keeps the office running — phones, calendars, customer updates, documents, onboarding paperwork, billing support and general organization.",
system: true,
perms: [...allPermissionIds],
attire: [
"Business casual blouse, sweater or cardigan",
"Clean, comfortable office attire",
"Simple jewelry and neat grooming",
"Organized desk presence",
"Approachable, calm and detail-oriented",
],
overlap: "In small teams, admins may also support HR, accounting, collections, dispatching, permit tracking and customer service.",
},
{
id: "manager",
id: "sales-manager",
name: "Sales Manager",
color: "var(--blue)",
description: "Leads the sales floor and oversees the crew's pipeline.",
description: "Leads the sales team — manages pipeline, trains reps, reviews numbers, sets expectations, supports closes and holds the team accountable.",
system: false,
perms: ["leads.manage", "pipeline.manage", "estimates.create", "dispatch.manage", "billing.view", "reports.view", "team.manage"],
attire: [
"Button-down shirt or polished business-casual top",
"Chinos or clean slacks",
"Leather shoes or clean boots",
"Folder, tablet or sales materials",
"More refined than a field rep, still jobsite-aware",
],
overlap: "Often supports recruiting, coaching, ride-alongs, lead routing, team meetings and performance reviews.",
},
{
id: "rep",
id: "sales-rep",
name: "Sales Rep",
color: "var(--green)",
description: "Works leads and moves deals through the pipeline.",
description: "Builds trust with homeowners, performs inspections, explains options, creates estimates, follows up and turns leads into signed projects.",
system: false,
perms: ["leads.manage", "pipeline.manage", "estimates.create"],
attire: [
"Branded polo or clean casual button-down",
"Well-fitted pants or jeans",
"Casual sneakers, boots or clean field shoes",
"Tablet, phone, folder or inspection tools",
"Polished, friendly and trustworthy look",
],
overlap: "In many smaller contractors, a sales rep may also act as project manager after the sale and stay responsible for the customer experience.",
},
{
id: "dispatcher",
name: "Dispatcher",
id: "project-manager",
name: "Project Manager",
color: "var(--cyan)",
description: "Schedules crews and coordinates site visits.",
description: "Owns the job after approval — scope review, material coordination, scheduling, customer updates, trade coordination, documentation and closeout.",
system: false,
perms: ["leads.manage", "pipeline.manage", "dispatch.manage", "billing.view", "reports.view"],
attire: [
"Button-down, polo or clean field shirt",
"Jeans or durable pants",
"Work boots or field-ready shoes",
"Tablet, plans, folder or measuring tools",
"Professional but ready to walk a jobsite",
],
overlap: "Commonly overlaps with sales, estimating, scheduling, collections and quality control.",
},
{
id: "site-supervisor",
name: "Site Supervisor",
color: "var(--red)",
description: "Runs daily field execution — watches quality and safety, keeps crews moving, verifies details and helps projects stay on schedule.",
system: false,
perms: ["dispatch.manage", "reports.view"],
attire: [
"Company t-shirt or work shirt",
"Ball cap or jobsite hat",
"Durable jeans or work pants",
"Work boots and jobsite watch",
"Practical, experienced, hands-on look",
],
overlap: "May coordinate subs, verify deliveries, answer field questions, document progress and report back to the project manager or owner.",
},
{
id: "estimator",
name: "Estimator",
id: "canvasser",
name: "Canvasser",
color: "var(--yellow)",
description: "Prepares estimates and proposals for approved leads.",
description: "Front-line lead generator — walks neighborhoods, knocks doors, qualifies homes, starts conversations and books inspections for sales reps.",
system: false,
perms: ["leads.manage", "estimates.create"],
perms: ["leads.manage"],
attire: [
"Young, clean, energetic appearance",
"Branded polo shirt",
"Lanyard ID badge",
"Comfortable pants or khakis",
"Clean sneakers, phone and shoulder bag",
],
overlap: "A strong canvasser may grow into a sales rep or team lead once they learn scripts, objections, territory discipline and appointment quality.",
},
{
id: "viewer",
name: "Viewer",
color: "var(--muted)",
description: "Read-only access to reports and billing — no edits.",
id: "team-lead",
name: "Team Lead",
color: "#ec4899",
description: "Leads a small group of reps or canvassers — a sales team lead or a canvassing team lead depending on the company structure.",
system: false,
perms: ["reports.view", "billing.view"],
perms: ["leads.manage", "pipeline.manage", "reports.view"],
attire: [
"Branded polo or clean field-casual shirt",
"Well-fitted pants with belt",
"Boots or clean sneakers",
"Tablet, phone, clipboard or route list",
"Confident, coachable, team-first presence",
],
overlap: "May run morning huddles, assign territory, coach scripts, inspect appointment quality and help new people ramp faster.",
},
{
id: "subcontractor",
name: "Subcontractor",
color: "var(--orange-2)",
description: "Specialized trade partner performing the actual skilled work — roofing, framing, electrical, HVAC, masonry, gutters, painting, drywall or flooring.",
system: false,
perms: ["dispatch.manage", "billing.view"],
attire: [
"Trade-specific workwear",
"Hard hat, high-vis vest, gloves and PPE when needed",
"Durable pants and work boots",
"Tools, belt or equipment as required",
"Rugged, skilled and dependable presence",
],
overlap: "Subs may have their own crews, foremen, schedules, invoices, insurance documents and safety requirements.",
},
{
id: "other",
name: "Other",
color: "var(--muted)",
description: "Utility roles — estimator, dispatcher, bookkeeper, HR coordinator, accounting support, warehouse support, permit coordinator, marketing assistant or specialist.",
system: false,
perms: ["reports.view"],
attire: [
"Dress for the function of the day",
"Office casual for desk work",
"Polo, jeans, boots or safety gear for field visits",
"Clean, reliable, company-aligned appearance",
"Flexible enough to support multiple departments",
],
overlap: "In smaller companies, other roles often become utility players covering gaps across operations, accounting, HR, dispatch, estimating and customer service.",
},
];
@@ -148,13 +258,16 @@ export type Member = {
export const members: Member[] = [
{ id: "u1", name: "James Carter", initials: "JC", email: "james.carter@lynkeduppro.com", title: "Founder & CEO", roleId: "owner", gradient: "linear-gradient(135deg,#fda913,#fd6d13)", status: "active", lastActive: "Active now", deals: 12, joined: "Mar 2021" },
{ id: "u2", name: "Emma Wilson", initials: "EW", email: "emma.wilson@lynkeduppro.com", title: "Head of Sales", roleId: "manager", gradient: "linear-gradient(135deg,#285ef0,#09b9c6)", status: "active", lastActive: "Active now", deals: 21, joined: "Aug 2021" },
{ id: "u3", name: "Liam Foster", initials: "LF", email: "liam.foster@lynkeduppro.com", title: "Senior Sales Rep", roleId: "rep", gradient: "linear-gradient(135deg,#9036e9,#285ef0)", status: "active", lastActive: "5 min ago", deals: 17, joined: "Jan 2022" },
{ id: "u4", name: "Sophie Turner", initials: "ST", email: "sophie.turner@lynkeduppro.com", title: "Account Executive", roleId: "rep", gradient: "linear-gradient(135deg,#14bc83,#09b9c6)", status: "away", lastActive: "32 min ago", deals: 9, joined: "May 2022" },
{ id: "u5", name: "Noah Mitchell", initials: "NM", email: "noah.mitchell@lynkeduppro.com", title: "Dispatch Coordinator", roleId: "dispatcher", gradient: "linear-gradient(135deg,#f0563f,#fda913)", status: "active", lastActive: "Active now", deals: 0, joined: "Sep 2022" },
{ id: "u6", name: "Olivia Bennett", initials: "OB", email: "olivia.bennett@lynkeduppro.com", title: "Operations Lead", roleId: "admin", gradient: "linear-gradient(135deg,#09b9c6,#285ef0)", status: "away", lastActive: "1 hour ago", deals: 3, joined: "Nov 2021" },
{ id: "u7", name: "Ethan Brooks", initials: "EB", email: "ethan.brooks@lynkeduppro.com", title: "Lead Estimator", roleId: "estimator", gradient: "linear-gradient(135deg,#ffd60a,#fda913)", status: "offline", lastActive: "Yesterday", deals: 6, joined: "Feb 2023" },
{ id: "u8", name: "Ava Reynolds", initials: "AR", email: "ava.reynolds@lynkeduppro.com", title: "Finance Analyst", roleId: "viewer", gradient: "linear-gradient(135deg,#9036e9,#f0563f)", status: "offline", lastActive: "2 days ago", deals: 0, joined: "Apr 2023" },
{ id: "u2", name: "Emma Wilson", initials: "EW", email: "emma.wilson@lynkeduppro.com", title: "Head of Sales", roleId: "sales-manager", gradient: "linear-gradient(135deg,#285ef0,#09b9c6)", status: "active", lastActive: "Active now", deals: 21, joined: "Aug 2021" },
{ id: "u3", name: "Liam Foster", initials: "LF", email: "liam.foster@lynkeduppro.com", title: "Senior Sales Rep", roleId: "sales-rep", gradient: "linear-gradient(135deg,#9036e9,#285ef0)", status: "active", lastActive: "5 min ago", deals: 17, joined: "Jan 2022" },
{ id: "u4", name: "Sophie Turner", initials: "ST", email: "sophie.turner@lynkeduppro.com", title: "Account Executive", roleId: "sales-rep", gradient: "linear-gradient(135deg,#14bc83,#09b9c6)", status: "away", lastActive: "32 min ago", deals: 9, joined: "May 2022" },
{ id: "u5", name: "Noah Mitchell", initials: "NM", email: "noah.mitchell@lynkeduppro.com", title: "Site Supervisor", roleId: "site-supervisor", gradient: "linear-gradient(135deg,#f0563f,#fda913)", status: "active", lastActive: "Active now", deals: 0, joined: "Sep 2022" },
{ id: "u6", name: "Olivia Bennett", initials: "OB", email: "olivia.bennett@lynkeduppro.com", title: "Office Admin", roleId: "admin", gradient: "linear-gradient(135deg,#09b9c6,#285ef0)", status: "away", lastActive: "1 hour ago", deals: 3, joined: "Nov 2021" },
{ id: "u7", name: "Ethan Brooks", initials: "EB", email: "ethan.brooks@lynkeduppro.com", title: "Project Manager", roleId: "project-manager", gradient: "linear-gradient(135deg,#ffd60a,#fda913)", status: "offline", lastActive: "Yesterday", deals: 6, joined: "Feb 2023" },
{ id: "u8", name: "Ava Reynolds", initials: "AR", email: "ava.reynolds@lynkeduppro.com", title: "Bookkeeper", roleId: "other", gradient: "linear-gradient(135deg,#9036e9,#f0563f)", status: "offline", lastActive: "2 days ago", deals: 0, joined: "Apr 2023" },
{ id: "u9", name: "Mason Reed", initials: "MR", email: "mason.reed@lynkeduppro.com", title: "Field Canvasser", roleId: "canvasser", gradient: "linear-gradient(135deg,#ffd60a,#14bc83)", status: "active", lastActive: "Active now", deals: 4, joined: "Jun 2023" },
{ id: "u10", name: "Chloe Adams", initials: "CA", email: "chloe.adams@lynkeduppro.com", title: "Sales Team Lead", roleId: "team-lead", gradient: "linear-gradient(135deg,#ec4899,#9036e9)", status: "away", lastActive: "18 min ago", deals: 11, joined: "Oct 2022" },
{ id: "u11", name: "Diego Ramirez", initials: "DR", email: "diego.ramirez@lynkeduppro.com", title: "Roofing Subcontractor", roleId: "subcontractor", gradient: "linear-gradient(135deg,#fd6d13,#f0563f)", status: "offline", lastActive: "3 days ago", deals: 0, joined: "Jan 2023" },
];
/* ---------------------------------------------------------- */
@@ -164,6 +277,6 @@ export const members: Member[] = [
export type Invite = { id: string; email: string; roleId: string; invitedBy: string; sentAt: string };
export const pendingInvites: Invite[] = [
{ id: "inv1", email: "daniel.hughes@gmail.com", roleId: "rep", invitedBy: "Emma Wilson", sentAt: "2 days ago" },
{ id: "inv2", email: "grace.murphy@outlook.com", roleId: "estimator", invitedBy: "James Carter", sentAt: "5 days ago" },
{ id: "inv1", email: "daniel.hughes@gmail.com", roleId: "sales-rep", invitedBy: "Emma Wilson", sentAt: "2 days ago" },
{ id: "inv2", email: "grace.murphy@outlook.com", roleId: "canvasser", invitedBy: "James Carter", sentAt: "5 days ago" },
];
+20 -3
View File
@@ -266,6 +266,23 @@ export function TeamManagement() {
</div>
<p className="tm-rolecard-desc">{r.description}</p>
<div className="tm-attire">
<div className="tm-attire-h"><Icon name="user" size={13} /> Attire &amp; appearance</div>
<ul className="tm-attire-list">
{r.attire.map((a) => (
<li key={a}><span className="tm-attire-dot" />{a}</li>
))}
</ul>
</div>
<div className="tm-overlap">
<span className="tm-overlap-ic"><Icon name="info" size={14} /></span>
<div>
<b>Common overlap</b>
<p>{r.overlap}</p>
</div>
</div>
<div className="tm-permlist">
{permissionGroups.map((g) => (
<div className="tm-permgroup" key={g.id}>
@@ -422,14 +439,14 @@ function RowMenu({ onEdit, onRemove, disabled }: { onEdit: () => void; onRemove:
function InviteModal({ open, roles, onClose, onInvite }: { open: boolean; roles: Role[]; onClose: () => void; onInvite: (email: string, roleId: string) => void }) {
const [email, setEmail] = useState("");
const [roleId, setRoleId] = useState("rep");
const [roleId, setRoleId] = useState("sales-rep");
const valid = /^[^@\s]+@[^@\s]+\.[^@\s]+$/.test(email.trim());
const role = roles.find((r) => r.id === roleId);
function submit() {
if (!valid) return;
onInvite(email.trim(), roleId);
setEmail(""); setRoleId("rep");
setEmail(""); setRoleId("sales-rep");
}
return (
@@ -472,7 +489,7 @@ function InviteModal({ open, roles, onClose, onInvite }: { open: boolean; roles:
function EditMemberModal({ member, roles, onClose, onSave }: { member: Member | null; roles: Role[]; onClose: () => void; onSave: (id: string, patch: Partial<Member>) => void }) {
const [title, setTitle] = useState("");
const [roleId, setRoleId] = useState("rep");
const [roleId, setRoleId] = useState("sales-rep");
// Sync local form when a different member is opened.
const key = member?.id ?? "";