diff --git a/src/components/ViolationCard.jsx b/src/components/ViolationCard.jsx
index 14463a7..58fb6c3 100644
--- a/src/components/ViolationCard.jsx
+++ b/src/components/ViolationCard.jsx
@@ -190,6 +190,11 @@ const ViolationCard = ({ violation, onStatusChange, isSelected, onSelect, showSe
{stage}
+ {violation.created_at && (Date.now() - new Date(violation.created_at).getTime()) < 7 * 24 * 60 * 60 * 1000 && (
+
+ New
+
+ )}
{violation.violation_type || violation.title}
diff --git a/src/components/ViolationDialog.jsx b/src/components/ViolationDialog.jsx
index 2a068d3..ae6813f 100644
--- a/src/components/ViolationDialog.jsx
+++ b/src/components/ViolationDialog.jsx
@@ -137,15 +137,21 @@ function ViolationDialog({ open, onOpenChange, violation, onSuccess, association
useEffect(() => {
const updateLevel = async () => {
- if (!violation && formData.property_id) {
+ // Only auto-set the stage for NEW violations once a property is chosen.
+ if (!violation && (formData.property_id || formData.unit_id || formData.address)) {
setCalculatingLevel(true);
- const level = await calculateNextNoticeLevel(formData.property_id);
+ const level = await calculateNextNoticeLevel({
+ ownerId: formData.property_id || null,
+ unitId: formData.unit_id || null,
+ associationId: formData.association_id || null,
+ address: formData.address || null,
+ });
setFormData(prev => ({ ...prev, stage: level }));
setCalculatingLevel(false);
}
};
updateLevel();
- }, [formData.property_id, violation]);
+ }, [formData.property_id, formData.unit_id, formData.address, formData.association_id, violation]);
const handleDefinedTypeChange = (typeId) => {
const selected = definedTypes.find(t => t.id === typeId);
diff --git a/src/lib/ViolationNoticeProgressionHelper.js b/src/lib/ViolationNoticeProgressionHelper.js
index 6b4f6a2..f84a733 100644
--- a/src/lib/ViolationNoticeProgressionHelper.js
+++ b/src/lib/ViolationNoticeProgressionHelper.js
@@ -18,6 +18,16 @@ export const getNoticeLevel = (count) => {
return NOTICE_LEVELS.FIRST;
};
+// Ordered stages, low → high. The stage after the last is the final (capped).
+export const STAGE_ORDER = [NOTICE_LEVELS.FIRST, NOTICE_LEVELS.SECOND, NOTICE_LEVELS.THIRD];
+
+// The next stage after `current` (caps at Third & Final). Unknown → First.
+export const getNextStage = (current) => {
+ const i = STAGE_ORDER.indexOf(current);
+ if (i === -1) return NOTICE_LEVELS.FIRST;
+ return STAGE_ORDER[Math.min(i + 1, STAGE_ORDER.length - 1)];
+};
+
export const getNoticeColor = (level) => {
switch (level) {
case NOTICE_LEVELS.THIRD:
@@ -30,18 +40,34 @@ export const getNoticeColor = (level) => {
}
};
-export const calculateNextNoticeLevel = async (unitId) => {
- if (!unitId) return NOTICE_LEVELS.FIRST;
-
+// Determine the stage for a NEW violation on a property: advance one step past
+// the property's highest CURRENTLY-OPEN stage (so re-recording the same property
+// escalates First → Second → Third & Final). No open violations → First Notice.
+// Identify the property by unit, else owner, else (association + address).
+export const calculateNextNoticeLevel = async ({ ownerId, unitId, associationId, address } = {}) => {
try {
- const { count, error } = await supabase
- .from('violations')
- .select('*', { count: 'exact', head: true })
- .eq('unit_id', unitId);
+ let query = supabase.from('violations').select('stage, notice_level, status');
+ if (unitId) query = query.eq('unit_id', unitId);
+ else if (ownerId) query = query.eq('owner_id', ownerId);
+ else if (address && associationId) query = query.eq('association_id', associationId).eq('address', address);
+ else return NOTICE_LEVELS.FIRST;
+ const { data, error } = await query;
if (error) throw error;
-
- return getNoticeLevel((count || 0) + 1);
+
+ const open = (data || []).filter(
+ (v) => !['resolved', 'closed'].includes(String(v.status || '').toLowerCase()),
+ );
+ if (open.length === 0) return NOTICE_LEVELS.FIRST;
+
+ let maxIdx = -1;
+ for (const v of open) {
+ const s = v.stage || v.notice_level || NOTICE_LEVELS.FIRST;
+ const idx = STAGE_ORDER.indexOf(s);
+ if (idx > maxIdx) maxIdx = idx;
+ }
+ if (maxIdx === -1) return NOTICE_LEVELS.FIRST;
+ return STAGE_ORDER[Math.min(maxIdx + 1, STAGE_ORDER.length - 1)];
} catch (err) {
console.error('Error calculating notice level:', err);
return NOTICE_LEVELS.FIRST;
diff --git a/src/pages/ViolationsPage.tsx b/src/pages/ViolationsPage.tsx
index 1169961..f8cb9ef 100644
--- a/src/pages/ViolationsPage.tsx
+++ b/src/pages/ViolationsPage.tsx
@@ -229,7 +229,7 @@ export default function ViolationsPage({ boardAssociationIds }: { boardAssociati
let violQuery = supabase
.from("violations")
.select("*, units(unit_number), associations(name), owners(first_name, last_name, property_address, email, electronic_consent, status)")
- .order("created_at", { ascending: true });
+ .order("created_at", { ascending: false });
let assocQuery = supabase.from("associations").select("*").eq("status", "active").order("name");
if (isBoardView) {