add subcontractor task assign/list for admin and owner feature
This commit is contained in:
@@ -5000,6 +5000,176 @@ const KANBAN_PROJECT_DATA = {
|
||||
},
|
||||
};
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Subcontractor Management (Feature 8.2 – Task Assignment)
|
||||
// Subcontractors are independent of any single company so the same record can
|
||||
// be referenced by tasks coming from multiple Admin/Owner orgs.
|
||||
// ---------------------------------------------------------------------------
|
||||
export const SUBCONTRACTOR_TASK_PRIORITIES = [
|
||||
{ value: 'low', label: 'Low', color: '#6B7280' },
|
||||
{ value: 'medium', label: 'Medium', color: '#3B82F6' },
|
||||
{ value: 'high', label: 'High', color: '#EF4444' },
|
||||
];
|
||||
|
||||
export const SUBCONTRACTOR_TASK_STATUSES = [
|
||||
'Assigned',
|
||||
'In Progress',
|
||||
'Completed',
|
||||
'Cancelled',
|
||||
];
|
||||
|
||||
const MOCK_SUBCONTRACTORS = [
|
||||
{
|
||||
id: 'sub_001',
|
||||
name: 'Carlos Subcontractor',
|
||||
companyName: 'Electric Pro Services',
|
||||
tradeType: 'Electrical',
|
||||
email: 'carlos@electricpro.com',
|
||||
phone: '469-555-9012',
|
||||
status: 'active',
|
||||
rating: 4.8,
|
||||
companies: ['lynkeduppro'],
|
||||
joinedAt: '2024-08-01',
|
||||
},
|
||||
{
|
||||
id: 'sub_002',
|
||||
name: 'Maya Patel',
|
||||
companyName: 'Skyline Roof Crew',
|
||||
tradeType: 'Roofing',
|
||||
email: 'maya@skylineroof.com',
|
||||
phone: '214-555-1247',
|
||||
status: 'active',
|
||||
rating: 4.6,
|
||||
companies: ['lynkeduppro', 'acmebuilds'],
|
||||
joinedAt: '2024-05-12',
|
||||
},
|
||||
{
|
||||
id: 'sub_003',
|
||||
name: 'Dwayne Holland',
|
||||
companyName: 'Holland Painting Co.',
|
||||
tradeType: 'Painting',
|
||||
email: 'dwayne@hollandpaint.com',
|
||||
phone: '972-555-8312',
|
||||
status: 'active',
|
||||
rating: 4.4,
|
||||
companies: ['lynkeduppro'],
|
||||
joinedAt: '2024-09-21',
|
||||
},
|
||||
{
|
||||
id: 'sub_004',
|
||||
name: 'Jennifer Wu',
|
||||
companyName: 'Wu Plumbing',
|
||||
tradeType: 'Plumbing',
|
||||
email: 'jenn@wuplumbing.com',
|
||||
phone: '469-555-3340',
|
||||
status: 'active',
|
||||
rating: 4.9,
|
||||
companies: ['lynkeduppro', 'acmebuilds', 'planobuilders'],
|
||||
joinedAt: '2024-02-10',
|
||||
},
|
||||
{
|
||||
id: 'sub_005',
|
||||
name: 'Greg Alston',
|
||||
companyName: 'Alston Window Pros',
|
||||
tradeType: 'Windows & Glazing',
|
||||
email: 'greg@alstonwindows.com',
|
||||
phone: '214-555-0299',
|
||||
status: 'inactive',
|
||||
rating: 4.1,
|
||||
companies: ['lynkeduppro'],
|
||||
joinedAt: '2023-11-15',
|
||||
},
|
||||
];
|
||||
|
||||
const MOCK_SUBCONTRACTOR_TASKS = [
|
||||
{
|
||||
id: 'sct_001',
|
||||
companyId: 'lynkeduppro',
|
||||
companyName: 'LynkedUp Pro Roofing',
|
||||
subcontractorId: 'sub_002',
|
||||
subcontractorName: 'Maya Patel',
|
||||
assignedBy: 'owner_001',
|
||||
assignedByName: 'Justin Johnson',
|
||||
title: 'Replace damaged ridge cap',
|
||||
location: '2612 Dunwick Dr, Plano, TX 75023',
|
||||
description: 'Two ridge cap shingles missing on the south slope after wind storm. Replace with matching tile and seal.',
|
||||
dueDate: '2026-05-25',
|
||||
priority: 'high',
|
||||
status: 'Assigned',
|
||||
photos: [
|
||||
{ id: 'sct_001_p1', name: 'Roof_Damage.jpg', url: '/assets/images/properties/Hail_Damaged_Shingles.jpg', annotation: 'Repair this corner' },
|
||||
{ id: 'sct_001_p2', name: 'Roof_Wide.jpg', url: '/assets/images/properties/Storm_Worn_Roof.jpg', annotation: 'Inspect adjacent tiles too' },
|
||||
],
|
||||
createdAt: '2026-05-15T09:12:00Z',
|
||||
updatedAt: '2026-05-15T09:12:00Z',
|
||||
},
|
||||
{
|
||||
id: 'sct_002',
|
||||
companyId: 'lynkeduppro',
|
||||
companyName: 'LynkedUp Pro Roofing',
|
||||
subcontractorId: 'sub_001',
|
||||
subcontractorName: 'Carlos Subcontractor',
|
||||
assignedBy: 'owner_001',
|
||||
assignedByName: 'Justin Johnson',
|
||||
title: 'Replace breaker panel',
|
||||
location: '6613 Phoenix Pl, Plano, TX 75023',
|
||||
description: 'Outdated 100A panel — upgrade to 200A. Permit pulled, materials staged on site.',
|
||||
dueDate: '2026-05-22',
|
||||
priority: 'medium',
|
||||
status: 'In Progress',
|
||||
photos: [
|
||||
{ id: 'sct_002_p1', name: 'Old_Panel.jpg', url: '/assets/images/properties/Beige_Two_Story_House.jpg', annotation: 'Existing 100A panel — disconnect and remove' },
|
||||
],
|
||||
createdAt: '2026-05-10T15:40:00Z',
|
||||
updatedAt: '2026-05-12T08:20:00Z',
|
||||
},
|
||||
{
|
||||
id: 'sct_003',
|
||||
companyId: 'lynkeduppro',
|
||||
companyName: 'LynkedUp Pro Roofing',
|
||||
subcontractorId: 'sub_003',
|
||||
subcontractorName: 'Dwayne Holland',
|
||||
assignedBy: 'ADM01',
|
||||
assignedByName: 'Admin One',
|
||||
title: 'Touch-up exterior paint',
|
||||
location: '3913 Arizona Pl, Plano, TX 75023',
|
||||
description: 'Front fascia and side trim need fresh coat after siding repair.',
|
||||
dueDate: '2026-05-18',
|
||||
priority: 'low',
|
||||
status: 'Completed',
|
||||
photos: [],
|
||||
createdAt: '2026-05-05T11:00:00Z',
|
||||
updatedAt: '2026-05-14T17:30:00Z',
|
||||
},
|
||||
];
|
||||
|
||||
const MOCK_NOTIFICATIONS = [
|
||||
{
|
||||
id: 'notif_001',
|
||||
recipientUserId: 'sub_001',
|
||||
recipientRole: 'SUBCONTRACTOR',
|
||||
type: 'task_assigned',
|
||||
message: 'You have been assigned a task by LynkedUp Pro Roofing',
|
||||
taskId: 'sct_002',
|
||||
fromCompanyId: 'lynkeduppro',
|
||||
fromCompanyName: 'LynkedUp Pro Roofing',
|
||||
isRead: false,
|
||||
createdAt: '2026-05-10T15:40:00Z',
|
||||
},
|
||||
{
|
||||
id: 'notif_002',
|
||||
recipientUserId: 'sub_002',
|
||||
recipientRole: 'SUBCONTRACTOR',
|
||||
type: 'task_assigned',
|
||||
message: 'You have been assigned a task by LynkedUp Pro Roofing',
|
||||
taskId: 'sct_001',
|
||||
fromCompanyId: 'lynkeduppro',
|
||||
fromCompanyName: 'LynkedUp Pro Roofing',
|
||||
isRead: false,
|
||||
createdAt: '2026-05-15T09:12:00Z',
|
||||
},
|
||||
];
|
||||
|
||||
// --- CONTEXT SETUP ---
|
||||
|
||||
const MockStoreContext = createContext();
|
||||
@@ -5028,6 +5198,11 @@ export const MockStoreProvider = ({ children }) => {
|
||||
const [kanbanColumns, setKanbanColumns] = useState(KANBAN_COLUMNS_INITIAL);
|
||||
const [kanbanLeads, setKanbanLeads] = useState(KANBAN_LEADS_INITIAL);
|
||||
|
||||
// Subcontractor management (Feature 8.2)
|
||||
const [subcontractors, setSubcontractors] = useState(MOCK_SUBCONTRACTORS);
|
||||
const [subcontractorTasks, setSubcontractorTasks] = useState(MOCK_SUBCONTRACTOR_TASKS);
|
||||
const [notifications, setNotifications] = useState(MOCK_NOTIFICATIONS);
|
||||
|
||||
// Commission Settings — Org defaults + per-user overrides
|
||||
const [orgCommissionDefaults, setOrgCommissionDefaults] = useState({
|
||||
type: 'percent_gross',
|
||||
@@ -5211,6 +5386,161 @@ export const MockStoreProvider = ({ children }) => {
|
||||
return true;
|
||||
};
|
||||
|
||||
// ── Notifications ───────────────────────────────────────────────────────
|
||||
// Designed so a real-time transport (websocket, SSE, push) can later
|
||||
// subscribe to `addNotification` without changing call sites.
|
||||
const addNotification = (payload) => {
|
||||
const notif = {
|
||||
id: `notif_${Date.now()}_${Math.floor(Math.random() * 1000)}`,
|
||||
isRead: false,
|
||||
createdAt: new Date().toISOString(),
|
||||
...payload,
|
||||
};
|
||||
setNotifications(prev => [notif, ...prev]);
|
||||
logger.info('Notification dispatched', { notif });
|
||||
return notif;
|
||||
};
|
||||
|
||||
const markNotificationRead = (notificationId) => {
|
||||
setNotifications(prev => prev.map(n => n.id === notificationId ? { ...n, isRead: true } : n));
|
||||
};
|
||||
|
||||
// ── Subcontractor Tasks (Feature 8.2) ──────────────────────────────────
|
||||
const addSubcontractorTask = (input) => {
|
||||
const subcontractor = subcontractors.find(s => s.id === input.subcontractorId);
|
||||
if (!subcontractor) {
|
||||
toast.error('Failed to assign task: subcontractor not found.');
|
||||
return null;
|
||||
}
|
||||
|
||||
const now = new Date().toISOString();
|
||||
const task = {
|
||||
id: `sct_${Date.now()}`,
|
||||
companyId: input.companyId,
|
||||
companyName: input.companyName,
|
||||
subcontractorId: subcontractor.id,
|
||||
subcontractorName: subcontractor.name,
|
||||
assignedBy: input.assignedBy,
|
||||
assignedByName: input.assignedByName,
|
||||
title: input.title,
|
||||
location: input.location,
|
||||
description: input.description || '',
|
||||
dueDate: input.dueDate,
|
||||
priority: input.priority || 'medium',
|
||||
status: 'Assigned',
|
||||
photos: (input.photos || []).map((p, i) => ({
|
||||
id: p.id || `${Date.now()}_${i}`,
|
||||
name: p.name,
|
||||
url: p.url,
|
||||
annotation: p.annotation || '',
|
||||
})),
|
||||
createdAt: now,
|
||||
updatedAt: now,
|
||||
};
|
||||
|
||||
setSubcontractorTasks(prev => [task, ...prev]);
|
||||
|
||||
addNotification({
|
||||
recipientUserId: subcontractor.id,
|
||||
recipientRole: 'SUBCONTRACTOR',
|
||||
type: 'task_assigned',
|
||||
message: `You have been assigned a task by ${input.companyName}`,
|
||||
taskId: task.id,
|
||||
fromCompanyId: input.companyId,
|
||||
fromCompanyName: input.companyName,
|
||||
});
|
||||
|
||||
toast.success(`Task assigned to ${subcontractor.name}`);
|
||||
return task;
|
||||
};
|
||||
|
||||
const updateSubcontractorTask = (taskId, data) => {
|
||||
let updatedSubId = null;
|
||||
let prevSubId = null;
|
||||
let companyName = null;
|
||||
let companyId = null;
|
||||
|
||||
setSubcontractorTasks(prev => prev.map(t => {
|
||||
if (t.id !== taskId) return t;
|
||||
prevSubId = t.subcontractorId;
|
||||
companyName = t.companyName;
|
||||
companyId = t.companyId;
|
||||
const nextSubId = data.subcontractorId ?? t.subcontractorId;
|
||||
updatedSubId = nextSubId;
|
||||
const nextSub = subcontractors.find(s => s.id === nextSubId);
|
||||
return {
|
||||
...t,
|
||||
...data,
|
||||
subcontractorId: nextSubId,
|
||||
subcontractorName: nextSub?.name ?? t.subcontractorName,
|
||||
photos: data.photos
|
||||
? data.photos.map((p, i) => ({
|
||||
id: p.id || `${Date.now()}_${i}`,
|
||||
name: p.name,
|
||||
url: p.url,
|
||||
annotation: p.annotation || '',
|
||||
}))
|
||||
: t.photos,
|
||||
updatedAt: new Date().toISOString(),
|
||||
};
|
||||
}));
|
||||
|
||||
// If subcontractor changed, dispatch a fresh notification to the new one.
|
||||
if (updatedSubId && prevSubId && updatedSubId !== prevSubId) {
|
||||
addNotification({
|
||||
recipientUserId: updatedSubId,
|
||||
recipientRole: 'SUBCONTRACTOR',
|
||||
type: 'task_assigned',
|
||||
message: `You have been assigned a task by ${companyName}`,
|
||||
taskId,
|
||||
fromCompanyId: companyId,
|
||||
fromCompanyName: companyName,
|
||||
});
|
||||
}
|
||||
|
||||
toast.success('Task updated');
|
||||
};
|
||||
|
||||
const cancelSubcontractorTask = (taskId) => {
|
||||
setSubcontractorTasks(prev => prev.map(t => t.id === taskId
|
||||
? { ...t, status: 'Cancelled', updatedAt: new Date().toISOString() }
|
||||
: t,
|
||||
));
|
||||
toast.success('Task cancelled');
|
||||
};
|
||||
|
||||
const reassignSubcontractorTask = (taskId, newSubcontractorId) => {
|
||||
const subcontractor = subcontractors.find(s => s.id === newSubcontractorId);
|
||||
if (!subcontractor) {
|
||||
toast.error('Failed to reassign: subcontractor not found.');
|
||||
return;
|
||||
}
|
||||
let companyName = null;
|
||||
let companyId = null;
|
||||
setSubcontractorTasks(prev => prev.map(t => {
|
||||
if (t.id !== taskId) return t;
|
||||
companyName = t.companyName;
|
||||
companyId = t.companyId;
|
||||
return {
|
||||
...t,
|
||||
subcontractorId: subcontractor.id,
|
||||
subcontractorName: subcontractor.name,
|
||||
status: t.status === 'Cancelled' ? 'Assigned' : t.status,
|
||||
updatedAt: new Date().toISOString(),
|
||||
};
|
||||
}));
|
||||
addNotification({
|
||||
recipientUserId: subcontractor.id,
|
||||
recipientRole: 'SUBCONTRACTOR',
|
||||
type: 'task_assigned',
|
||||
message: `You have been assigned a task by ${companyName}`,
|
||||
taskId,
|
||||
fromCompanyId: companyId,
|
||||
fromCompanyName: companyName,
|
||||
});
|
||||
toast.success(`Task reassigned to ${subcontractor.name}`);
|
||||
};
|
||||
|
||||
return (
|
||||
<MockStoreContext.Provider value={{
|
||||
properties,
|
||||
@@ -5432,6 +5762,30 @@ export const MockStoreProvider = ({ children }) => {
|
||||
resendOrgInvite: (memberId) => {
|
||||
toast.success('Invite resent successfully');
|
||||
},
|
||||
|
||||
// Subcontractor Management (Feature 8.2)
|
||||
subcontractors,
|
||||
addSubcontractor: (sub) => {
|
||||
const newSub = {
|
||||
...sub,
|
||||
id: sub.id || `sub_${Date.now()}`,
|
||||
status: sub.status || 'active',
|
||||
companies: sub.companies || [],
|
||||
joinedAt: sub.joinedAt || new Date().toISOString().slice(0, 10),
|
||||
};
|
||||
setSubcontractors(prev => [...prev, newSub]);
|
||||
return newSub;
|
||||
},
|
||||
subcontractorTasks,
|
||||
taskPriorities: SUBCONTRACTOR_TASK_PRIORITIES,
|
||||
taskStatuses: SUBCONTRACTOR_TASK_STATUSES,
|
||||
addSubcontractorTask,
|
||||
updateSubcontractorTask,
|
||||
cancelSubcontractorTask,
|
||||
reassignSubcontractorTask,
|
||||
notifications,
|
||||
addNotification,
|
||||
markNotificationRead,
|
||||
}}>
|
||||
{children}
|
||||
</MockStoreContext.Provider>
|
||||
|
||||
Reference in New Issue
Block a user