mirror of
https://github.com/renee-png/acmcc.git
synced 2026-06-21 01:40:01 +00:00
Violations: fix auto-escalation on re-record + surface new vs old by date
Auto-escalation was broken: calculateNextNoticeLevel queried unit_id but the dialog passed the owner id, so it always returned "First Notice." Rewrote it to advance one step past the property's highest CURRENTLY-OPEN stage (First → Second → Third & Final, capped), identifying the property by unit, else owner, else association+address; the dialog now passes all of those and re-runs when the unit/address changes. So re-recording a violation on the same property auto-selects the next stage. New vs old by date: list now loads newest-first, and cards show a "New" badge for violations recorded in the last 7 days (violation_date was already captured and shown, plus the timeline group-by-date view). Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
@@ -190,6 +190,11 @@ const ViolationCard = ({ violation, onStatusChange, isSelected, onSelect, showSe
|
|||||||
<Badge className={cn("px-2 py-0.5 text-[10px] font-semibold border", getNoticeColor(stage))}>
|
<Badge className={cn("px-2 py-0.5 text-[10px] font-semibold border", getNoticeColor(stage))}>
|
||||||
{stage}
|
{stage}
|
||||||
</Badge>
|
</Badge>
|
||||||
|
{violation.created_at && (Date.now() - new Date(violation.created_at).getTime()) < 7 * 24 * 60 * 60 * 1000 && (
|
||||||
|
<Badge className="px-2 py-0.5 text-[10px] font-semibold border bg-emerald-100 text-emerald-700 border-emerald-200">
|
||||||
|
New
|
||||||
|
</Badge>
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<h3 className="font-bold text-foreground text-base leading-tight mb-1">{violation.violation_type || violation.title}</h3>
|
<h3 className="font-bold text-foreground text-base leading-tight mb-1">{violation.violation_type || violation.title}</h3>
|
||||||
|
|||||||
@@ -137,15 +137,21 @@ function ViolationDialog({ open, onOpenChange, violation, onSuccess, association
|
|||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const updateLevel = async () => {
|
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);
|
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 }));
|
setFormData(prev => ({ ...prev, stage: level }));
|
||||||
setCalculatingLevel(false);
|
setCalculatingLevel(false);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
updateLevel();
|
updateLevel();
|
||||||
}, [formData.property_id, violation]);
|
}, [formData.property_id, formData.unit_id, formData.address, formData.association_id, violation]);
|
||||||
|
|
||||||
const handleDefinedTypeChange = (typeId) => {
|
const handleDefinedTypeChange = (typeId) => {
|
||||||
const selected = definedTypes.find(t => t.id === typeId);
|
const selected = definedTypes.find(t => t.id === typeId);
|
||||||
|
|||||||
@@ -18,6 +18,16 @@ export const getNoticeLevel = (count) => {
|
|||||||
return NOTICE_LEVELS.FIRST;
|
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) => {
|
export const getNoticeColor = (level) => {
|
||||||
switch (level) {
|
switch (level) {
|
||||||
case NOTICE_LEVELS.THIRD:
|
case NOTICE_LEVELS.THIRD:
|
||||||
@@ -30,18 +40,34 @@ export const getNoticeColor = (level) => {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
export const calculateNextNoticeLevel = async (unitId) => {
|
// Determine the stage for a NEW violation on a property: advance one step past
|
||||||
if (!unitId) return NOTICE_LEVELS.FIRST;
|
// 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 {
|
try {
|
||||||
const { count, error } = await supabase
|
let query = supabase.from('violations').select('stage, notice_level, status');
|
||||||
.from('violations')
|
if (unitId) query = query.eq('unit_id', unitId);
|
||||||
.select('*', { count: 'exact', head: true })
|
else if (ownerId) query = query.eq('owner_id', ownerId);
|
||||||
.eq('unit_id', unitId);
|
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;
|
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) {
|
} catch (err) {
|
||||||
console.error('Error calculating notice level:', err);
|
console.error('Error calculating notice level:', err);
|
||||||
return NOTICE_LEVELS.FIRST;
|
return NOTICE_LEVELS.FIRST;
|
||||||
|
|||||||
@@ -229,7 +229,7 @@ export default function ViolationsPage({ boardAssociationIds }: { boardAssociati
|
|||||||
let violQuery = supabase
|
let violQuery = supabase
|
||||||
.from("violations")
|
.from("violations")
|
||||||
.select("*, units(unit_number), associations(name), owners(first_name, last_name, property_address, email, electronic_consent, status)")
|
.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");
|
let assocQuery = supabase.from("associations").select("*").eq("status", "active").order("name");
|
||||||
|
|
||||||
if (isBoardView) {
|
if (isBoardView) {
|
||||||
|
|||||||
Reference in New Issue
Block a user