Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
99 changes: 99 additions & 0 deletions src/components/AssignItemModal.module.css
Original file line number Diff line number Diff line change
@@ -0,0 +1,99 @@
/* Modal Title*/
.modalTitle {
font-weight: bold;
}

/* People Selection */
.emptyState {
color: #999;
font-style: italic;
padding: 1rem;
text-align: center;
}

.peopleList {
display: flex;
flex-direction: column;
gap: 0.5rem;
margin-top: 2%;
}

.personOption {
display: flex;
align-items: center;
gap: 0.75rem;
padding: 0.75rem;
border-radius: 8px;
cursor: pointer;
border: 2px solid #ddd;
transition: all 0.2s ease;
background-color: white;
}

.personOption:hover {
border-color: var(--primary-pink);
background-color: #fef5f9;
}

.personOption.selected {
border-color: var(--primary-pink);
background-color: rgba(219, 25, 90, 0.08);
}

.checkbox {
cursor: pointer;
width: 18px;
height: 18px;
}

.colorDot {
width: 14px;
height: 14px;
border-radius: 50%;
flex-shrink: 0;
}

.personName {
flex: 1;
}

/* Split Preview */
.splitPreview {
margin-top: 1.5rem;
padding: 1.25rem;
background: linear-gradient(135deg, #fef5f9 0%, #f8f9fa 100%);
border-radius: 12px;
text-align: center;
border: 1px solid #ffe6f0;
}

.splitLabel {
font-size: 0.875rem;
color: #666;
margin-bottom: 0.5rem;
font-weight: 500;
}

.splitAmount {
font-size: 1.75rem;
font-weight: bold;
color: var(--primary-pink);
margin: 0;
}

/* Warning State */
.warning {
margin-top: 1.5rem;
padding: 1rem;
background-color: #fff3cd;
border-radius: 8px;
text-align: center;
color: #856404;
border: 1px solid #ffeeba;
font-size: 0.9rem;
}

.warning::before {
content: "⚠️ ";
margin-right: 0.5rem;
}
142 changes: 142 additions & 0 deletions src/components/AssignItemModal.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,142 @@
import Modal from "react-bootstrap/Modal";
import { useState, useEffect } from "react";
import type { Item, Person } from "../types";
import styles from "./AssignItemModal.module.css";

interface Props {
show: boolean;
item: Item | null;
people: Person[];
currentAssignees: string[]; // Array of person IDs currently assigned
onClose: () => void;
onSave: (itemId: string, personIds: string[]) => void;
}

export default function AssignItemModal({
show,
item,
people,
currentAssignees,
onClose,
onSave,
}: Props) {
const [selectedPeople, setSelectedPeople] = useState<string[]>([]);

useEffect(() => {
setSelectedPeople(currentAssignees);
}, [currentAssignees, show]);

const togglePerson = (personId: string) => {
setSelectedPeople((prev) => {
if (prev.includes(personId)) {
return prev.filter((id) => id !== personId);
} else {
return [...prev, personId];
}
});
};

const calculateSplitAmount = () => {
if (!item || selectedPeople.length === 0) return 0;
const itemTotal = item.price * item.quantity;
return itemTotal / selectedPeople.length;
};

const handleSave = () => {
if (!item) return;
onSave(item.id, selectedPeople);
onClose();
};

const handleCancel = () => {
// Reset to original assignments
setSelectedPeople(currentAssignees);
onClose();
};

if (!item) return null;

const splitAmount = calculateSplitAmount();

return (
<Modal show={show} onHide={handleCancel} centered backdrop>
<Modal.Header>
<Modal.Title className={styles.modalTitle}>
Assign {item.name} to...
</Modal.Title>
</Modal.Header>

<Modal.Body>
{/* People selection */}
<div>
{people.length === 0 ? (
<p className={styles.emptyState}>
No people added yet. Add people first to assign items.
</p>
) : (
<div className={styles.peopleList}>
{people.map((person) => {
const isSelected = selectedPeople.includes(person.id);
return (
<label
key={person.id}
className={`${styles.personOption} ${isSelected ? styles.selected : ""}`}
style={{
borderColor: isSelected ? person.color : undefined,
backgroundColor: isSelected
? `${person.color}15`
: undefined,
}}
>
<input
type="checkbox"
checked={isSelected}
onChange={() => togglePerson(person.id)}
className={styles.checkbox}
/>
<span
className={styles.colorDot}
style={{ backgroundColor: person.color }}
/>
<span className={styles.personName}>{person.name}</span>
</label>
);
})}
</div>
)}
</div>

{/* Split preview */}
{selectedPeople.length > 0 && (
<div className={styles.splitPreview}>
<div className={styles.splitLabel}>
Split between {selectedPeople.length}{" "}
{selectedPeople.length === 1 ? "person" : "people"}
</div>
<div className={styles.splitAmount}>
${splitAmount.toFixed(2)} each
</div>
</div>
)}

{selectedPeople.length === 0 && people.length > 0 && (
<div className={styles.warning}>
Select at least one person to assign this item to
</div>
)}
</Modal.Body>
<Modal.Footer>
<button className="whiteButton" onClick={handleCancel}>
Cancel
</button>
<button
className="pinkButton"
onClick={handleSave}
disabled={selectedPeople.length === 0}
>
Save Assignment
</button>
</Modal.Footer>
</Modal>
);
}
4 changes: 4 additions & 0 deletions src/components/PeopleManager/AddPersonModal.module.css
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
/* Modal Title*/
.modalTitle {
font-weight: bold;
}
6 changes: 3 additions & 3 deletions src/components/PeopleManager/AddPersonModal.tsx
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import Modal from "react-bootstrap/Modal";
import { useState } from "react";
import { DEFAULT_COLORS } from "./usePeople";
import styles from './AddPersonModal.module.css';

interface Props {
show: boolean;
Expand All @@ -23,16 +24,15 @@ export default function AddPersonModal({ show, onClose, onSubmit }: Props) {

return (
<Modal show={show} onHide={onClose} centered backdrop>
<Modal.Header closeButton>
<Modal.Title>Add person</Modal.Title>
<Modal.Header>
<Modal.Title className={styles.modalTitle}>Add person</Modal.Title>
</Modal.Header>

<Modal.Body>
<label>Name</label>
<input
value={name}
onChange={(e) => setName(e.target.value)}
placeholder="Enter name"
style={{ width: "100%", marginBottom: 16 }}
/>

Expand Down
2 changes: 1 addition & 1 deletion src/components/PeopleManager/PeopleChips.tsx
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import styles from "./styles.module.css";
import type { Person } from "./usePeople";
import type { Person } from "../../types";

interface Props {
people: Person[];
Expand Down
29 changes: 7 additions & 22 deletions src/components/PeopleManager/usePeople.ts
Original file line number Diff line number Diff line change
@@ -1,11 +1,5 @@
import { useState } from "react";

export type Person = {
id: string;
name: string;
color: string;
total: number;
};
import type { Person } from "../../types";

export const DEFAULT_COLORS = [
"#C6005A",
Expand All @@ -17,7 +11,10 @@ export const DEFAULT_COLORS = [
"#6A4C93",
"#6F1D1B",
"#D4A373",
"#6B7280"
"#6B7280",
"#4B5320",
"#3A86FF",
"#d80606d7"
];

export function usePeople(initial?: Person[]) {
Expand All @@ -29,28 +26,16 @@ export function usePeople(initial?: Person[]) {
? crypto.randomUUID()
: Date.now().toString();

setPeople(prev => [...prev, { id, name, color, total: 0 }]);
setPeople(prev => [...prev, { id, name, color, items: [] }]);
};

const removePerson = (id: string) => {
setPeople(prev => prev.filter(p => p.id !== id));
};

const updateTotal = (id: string, amount: number) => {
setPeople(prev =>
prev.map(p =>
p.id === id ? { ...p, total: amount } : p
)
);
};

const grandTotal = people.reduce((sum, p) => sum + p.total, 0);

return {
people,
addPerson,
removePerson,
updateTotal,
grandTotal
removePerson
};
}
Loading