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))}>
|
||||
{stage}
|
||||
</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>
|
||||
|
||||
<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(() => {
|
||||
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);
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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) {
|
||||
|
||||
Reference in New Issue
Block a user