Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
30 commits
Select commit Hold shift + click to select a range
7263f05
feat: implement PendingDailyCountsView with edit dialog and sticky he…
Pertempto Oct 17, 2025
fa8c274
chore: bump version to 2025.10.17.4
Pertempto Oct 17, 2025
202d2b6
fix: address PR feedback - fix Supabase queries and async handling
Pertempto Oct 17, 2025
0770bee
refactor: move send button to sticky header and rename saveCount to u…
Pertempto Oct 17, 2025
bf9aaf8
style: improve sticky header appearance
Pertempto Oct 17, 2025
a3a8dcd
feat: order daily counts by picker name alphabetically
Pertempto Oct 17, 2025
5a10dd5
style: match delete button height with count button
Pertempto Oct 17, 2025
74b4552
style: fix delete button height by removing padding and reducing icon…
Pertempto Oct 17, 2025
c7cc97d
style: fix sticky header positioning when scrolling
Pertempto Oct 17, 2025
c97ae61
feat: improve styling
Pertempto Oct 17, 2025
219b864
style: add shadow to sticky header for better visual separation
Pertempto Oct 17, 2025
02e2659
style: adjust sticky header padding and shadow
Pertempto Oct 17, 2025
b79f4f1
style: increase sticky header padding and use darker shadow
Pertempto Oct 17, 2025
7d6c369
feat: improving styling
Pertempto Oct 17, 2025
5126a44
style: fix shadow visibility with explicit color
Pertempto Oct 17, 2025
18af938
style: match edit dialog button styles with BinSetting component
Pertempto Oct 17, 2025
303eb4f
feat: disable Update button for invalid counts
Pertempto Oct 17, 2025
346ac40
feat: improve styling
Pertempto Oct 17, 2025
5082e24
fix: set minimum count to 1 instead of 0
Pertempto Oct 17, 2025
0f20f85
feat: more styling improvements
Pertempto Oct 17, 2025
1a08087
refactor: remove unused dayCount calculation
Pertempto Oct 17, 2025
3280fa1
refactor: remove unused dailyCountsFromDB and its query
Pertempto Oct 17, 2025
385c426
refactor: rename stuff
Pertempto Oct 17, 2025
7dadaba
docs: update spec checklist
Pertempto Oct 17, 2025
64c3a7e
refactor: replace .then with await in onMounted
Pertempto Oct 17, 2025
2c249ad
refactor: inline pickers variable in onMounted
Pertempto Oct 17, 2025
7ebd6ea
docs: add variable usage guidelines to AGENTS.md
Pertempto Oct 17, 2025
228467c
refactor: rename variable
Pertempto Oct 17, 2025
dab4250
refactor: use guard clause in updateCount and document pattern
Pertempto Oct 17, 2025
8e7ca85
feat: add ESLint curly rule and update guard clause documentation
Pertempto Oct 17, 2025
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
9 changes: 9 additions & 0 deletions AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,15 @@
- ✅ Good: `() => props.getComponent()`
- ❌ Avoid: `() => { return isEnabled ? EnabledComponent : DisabledComponent }`
- Use multi-line arrow functions only when necessary for complex logic
- **Variable Usage**: Avoid unnecessary variables and inline when appropriate
- ✅ Good: `settings.value = getSettings(await getPickers())`
- ❌ Avoid: `const pickers = await getPickers(); settings.value = getSettings(pickers)`
- Only create variables when the value is used multiple times or improves readability
- **Guard Clauses**: Use guard clauses instead of wrapping if blocks when possible
- ✅ Good: `if (!condition) { return; }` followed by main logic
- ❌ Avoid: `if (condition) { main logic }` when condition is a validation check
- Reduces nesting and improves readability for validation/error cases
- **IMPORTANT**: Always use braces for if blocks (enforced by ESLint `curly` rule)

### Git Workflow

Expand Down
8 changes: 7 additions & 1 deletion app/eslint.config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@ export default defineConfigWithVueTs(
files: ['**/*.{ts,mts,tsx,vue}'],
},

globalIgnores(['**/dist/**', '**/dist-ssr/**', '**/coverage/**']),
globalIgnores(['**/dist/**', '**/dist-ssr/**', '**/coverage/**', '**/.vite/**']),

pluginVue.configs['flat/essential'],
vueTsConfigs.recommended,
Expand All @@ -29,4 +29,10 @@ export default defineConfigWithVueTs(
'prefer-arrow-callback': 'error',
},
},
{
name: 'app/code-style',
rules: {
'curly': 'error',
},
},
)
31 changes: 17 additions & 14 deletions app/specs/63-experimental-daily-count-ui.md
Original file line number Diff line number Diff line change
Expand Up @@ -19,20 +19,23 @@ Instead of inputting each individual bin, allow the user to input the daily coun
- [x] `/add` should use new `AddDailyCountView`
- [x] `/pending` should use new `PendingDailyCountsView`
- [x] `/history` should use new `DailyCountsHistoryView`
- [ ] `AddDailyCountView` should be the same as `AddBinView`, but instead should:
- [ ] Allow the user to input daily count rather than block, size, and bin ID
- [ ] Display the list of daily count options directly in the view to make the UX more efficient
- [ ] Use a 5 column grid from 1-25 to display the daily count options
- [ ] `PendingDailyCountsView` should be the same as `PendingBinsView`, but instaed should:
- [ ] Display the list of pending daily counts, with:
- [ ] Picker
- [ ] Count
- [ ] Delete button
- [ ] Should include the following in the SMS message
- [ ] Date
- [ ] Picker
- [ ] Daily count
- [ ] Weekly count
- [x] `AddDailyCountView` should be the same as `AddBinView`, but instead should:
- [x] Allow the user to input daily count rather than block, size, and bin ID
- [x] Display the list of daily count options directly in the view to make the UX more efficient
- [x] Use a 5 column grid from 1-25 to display the daily count options
- [x] `PendingDailyCountsView` should be the same as `PendingBinsView`, but instead should:
- [x] Display the list of pending daily counts, with:
- [x] Picker
- [x] Count
- [x] Tapping this should open dialog with number input to allow any count
- [x] Delete button
- [x] Include the total number of bins at the top of the page
- [x] This should be a sticky header at the top of the page
- [x] Should include the following in the SMS message
- [x] Date
- [x] Picker
- [x] Daily count
- [x] Weekly count
- [ ] `DailyCountsHistoryView` should be the same as `BinsHistoryView`, but instead should:
- [ ] Display the list of send daily count messages, with:
- [ ] Picker
Expand Down
2 changes: 1 addition & 1 deletion app/src/lib/utils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ import type { Picker } from '@/models/picker'
// Second number is month
// Third number is day
// Fourth number is release index for that day (starts at 0)
export const appVersion = '2025.10.17.3'
export const appVersion = '2025.10.17.4'

export const getUserProfile = async () => {
const {
Expand Down
232 changes: 229 additions & 3 deletions app/src/views/PendingDailyCountsView.vue
Original file line number Diff line number Diff line change
@@ -1,7 +1,233 @@
<script setup lang="ts">
import ComingSoon from '@/components/ComingSoon.vue'
import { supabase } from "@/lib/supabaseClient";
import { sendMessages } from "@/lib/smoketreeClient";
import { ref, onMounted, computed } from "vue";
import { getSettings, type Setting } from "@/models/settings";
import type { DailyCount } from "@/models/dailyCount";
import { getPickers } from "@/lib/utils";
import { Icon } from "@iconify/vue";

const dailyCounts = ref<DailyCount[]>([]);
const settings = ref<Setting[]>([]);
const isSending = ref(false);
const editingCount = ref<DailyCount | null>(null);
const tempCount = ref<number>(0);

const totalCount = computed(() =>
dailyCounts.value.reduce((sum, count) => sum + count.count, 0),
);

onMounted(async () => {
loadPendingDailyCounts();
settings.value = getSettings(await getPickers());
});

async function loadPendingDailyCounts() {
const { data } = await supabase
.from("dailyCount")
.select()
.eq("isPending", true)
.order("picker");
dailyCounts.value = data as DailyCount[];
}

async function updateDailyCount(dailyCount: DailyCount) {
await supabase
.from("dailyCount")
.update(dailyCount)
.eq("uuid", dailyCount.uuid)
.select();
}

async function deleteDailyCount(dailyCount: DailyCount) {
if (
confirm(
`Are you sure you want to delete daily count for ${dailyCount.picker}?`,
)
) {
await supabase.from("dailyCount").delete().eq("uuid", dailyCount.uuid);
await loadPendingDailyCounts();
}
}

function openEditDialog(dailyCount: DailyCount) {
editingCount.value = dailyCount;
tempCount.value = dailyCount.count;
}

async function updateCount() {
if (!editingCount.value) { return; }

editingCount.value.count = tempCount.value;
await updateDailyCount(editingCount.value);
await loadPendingDailyCounts();
editingCount.value = null;
}

function cancelEdit() {
editingCount.value = null;
tempCount.value = 0;
}

async function sendDailyCounts() {
isSending.value = true;

try {
const pickers = await getPickers();
const pickerNumbers = Object.fromEntries(
pickers.map((picker) => [picker.name, picker.phoneNumber]),
);

const startOfDay = new Date();
startOfDay.setHours(0, 0, 0, 0);
const startOfWeek = new Date();
startOfWeek.setDate(startOfWeek.getDate() - startOfWeek.getDay());
startOfWeek.setHours(0, 0, 0, 0);

// Fetch all non-pending counts from this week in bulk
const { data: dailyCountsFromThisWeek } = await supabase
.from("dailyCount")
.select("picker, count")
.gte("date", startOfWeek.toISOString())
.eq("isPending", false);

const weeklyCountsFromDB: Record<string, number> = {};
dailyCountsFromThisWeek?.forEach(
(count: { picker: string; count: number }) => {
weeklyCountsFromDB[count.picker] =
(weeklyCountsFromDB[count.picker] ?? 0) + count.count;
},
);

const messages = [];
// Add the counts that are getting sent now, because they won't be included
// in the counts from the DB.
const countAdjustments: Record<string, number> = {};
for (const dailyCount of dailyCounts.value) {
countAdjustments[dailyCount.picker] ??= 0;
countAdjustments[dailyCount.picker] += dailyCount.count;
const weeklyCount =
(weeklyCountsFromDB[dailyCount.picker] ?? 0) +
(countAdjustments[dailyCount.picker] ?? 0);

messages.push({
to: pickerNumbers[dailyCount.picker],
content: `Fecha: ${formatDate(new Date(dailyCount.date))}
Recogedor: ${dailyCount.picker}
Cajas hoy: ${dailyCount.count}
Cajas semana: ${weeklyCount}
`,
});
}
const results = await sendMessages(messages);
let i = 0;
for (const result of results) {
await supabase
.from("dailyCount")
.update({ isPending: false, messageUuid: result.uuid })
.eq("uuid", dailyCounts.value[i].uuid)
.select();
i += 1;
}
dailyCounts.value = [];
} finally {
isSending.value = false;
}
}

function formatDate(date: Date) {
const year = date.getFullYear();
const month = String(date.getMonth() + 1).padStart(2, "0");
const day = String(date.getDate()).padStart(2, "0");
return `${year}/${month}/${day}`;
}
</script>

<template>
<ComingSoon />
</template>
<div
v-if="isSending"
class="size-full flex items-center justify-center text-2xl"
>
<div class="p-2 flex items-center justify-center gap-2">
<Icon
icon="svg-spinners:90-ring-with-bg"
height="36"
class="text-white"
/>
<span class="ml-2 text-white">Sending...</span>
</div>
</div>
<div v-else-if="dailyCounts.length > 0" class="flex flex-col">
<!-- Sticky Header -->
<div class="sticky p-2 top-0">
<div class="bg-slate-800 p-4 rounded-lg z-10 shadow-lg shadow-black/50">
<div class="flex justify-between items-center">
<div class="text-white text-lg font-semibold">
Total Bins: {{ totalCount }}
</div>
<button @click="sendDailyCounts" class="bg-blue-800 rounded-md p-2">
Send
</button>
</div>
</div>
</div>

<ul class="flex flex-col gap-1 p-2">
<li
v-for="dailyCount in dailyCounts"
:key="dailyCount.uuid"
class="p-1 px-2 flex flex-row gap-3 justify-stretch items-center"
>
<div class="flex-1 text-white">{{ dailyCount.picker }}</div>
<button
@click="openEditDialog(dailyCount)"
class="flex-1 bg-gray-800 p-2 text-2xl rounded-lg flex items-center justify-center"
>
{{ dailyCount.count }}
</button>
<button
@click="deleteDailyCount(dailyCount)"
class="bg-red-700 w-16 p-2 md:w-20 flex items-center justify-center rounded-md"
>
<Icon icon="system-uicons:trash" height="32" />
</button>
</li>
</ul>
</div>
<div v-else class="size-full flex items-center justify-center text-2xl">
No pending daily counts
</div>

<!-- Edit Dialog -->
<div
v-if="editingCount"
class="fixed top-0 left-0 w-full h-full bg-gray-900/50 flex items-center justify-center z-50"
@click.self="cancelEdit"
>
<div class="min-w-[300px] p-4 rounded-md bg-gray-800">
<h2 class="text-lg font-bold mb-2">
Edit Count for {{ editingCount.picker }}
</h2>

<input
v-model.number="tempCount"
type="number"
min="1"
class="h-16 p-4 lg:h-12 w-full border rounded-md"
/>

<div class="flex justify-end mt-4 gap-2">
<button @click="cancelEdit" class="bg-gray-700 p-2 rounded-md">
Cancel
</button>
<button
@click="updateCount"
:disabled="tempCount < 1 || !Number.isInteger(tempCount)"
class="bg-blue-700 disabled:opacity-50 text-white p-2 rounded-md"
>
Update
</button>
</div>
</div>
</div>
</template>